@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.
Files changed (305) hide show
  1. package/CHANGELOG.md +157 -0
  2. package/dist/authorization/gate.d.ts +1 -1
  3. package/dist/authorization/gate.js +1 -1
  4. package/dist/bridge/a2a/mapper.d.ts.map +1 -1
  5. package/dist/bridge/a2a/mapper.js +13 -0
  6. package/dist/bridge/a2a/mapper.js.map +1 -1
  7. package/dist/bridge/sse/mapper.d.ts.map +1 -1
  8. package/dist/bridge/sse/mapper.js +3 -0
  9. package/dist/bridge/sse/mapper.js.map +1 -1
  10. package/dist/connector/index.d.ts +1 -1
  11. package/dist/connector/index.d.ts.map +1 -1
  12. package/dist/connector/index.js +1 -1
  13. package/dist/connector/index.js.map +1 -1
  14. package/dist/connector/mcp/adapter.d.ts.map +1 -1
  15. package/dist/connector/mcp/adapter.js +71 -16
  16. package/dist/connector/mcp/adapter.js.map +1 -1
  17. package/dist/connector/mcp/client.d.ts.map +1 -1
  18. package/dist/connector/mcp/client.js +3 -2
  19. package/dist/connector/mcp/client.js.map +1 -1
  20. package/dist/connector/mcp/http-redirect.d.ts +17 -0
  21. package/dist/connector/mcp/http-redirect.d.ts.map +1 -0
  22. package/dist/connector/mcp/http-redirect.js +31 -0
  23. package/dist/connector/mcp/http-redirect.js.map +1 -0
  24. package/dist/connector/mcp/http-sse.d.ts.map +1 -1
  25. package/dist/connector/mcp/http-sse.js +5 -0
  26. package/dist/connector/mcp/http-sse.js.map +1 -1
  27. package/dist/connector/mcp/image-admission.d.ts +16 -0
  28. package/dist/connector/mcp/image-admission.d.ts.map +1 -0
  29. package/dist/connector/mcp/image-admission.js +288 -0
  30. package/dist/connector/mcp/image-admission.js.map +1 -0
  31. package/dist/connector/mcp/stdio.d.ts +13 -0
  32. package/dist/connector/mcp/stdio.d.ts.map +1 -1
  33. package/dist/connector/mcp/stdio.js +66 -23
  34. package/dist/connector/mcp/stdio.js.map +1 -1
  35. package/dist/connector/mcp/streamable-http.d.ts.map +1 -1
  36. package/dist/connector/mcp/streamable-http.js +3 -0
  37. package/dist/connector/mcp/streamable-http.js.map +1 -1
  38. package/dist/constants/tools/index.js +1 -1
  39. package/dist/constants/tools/index.js.map +1 -1
  40. package/dist/execution/base.d.ts +13 -0
  41. package/dist/execution/base.d.ts.map +1 -1
  42. package/dist/execution/base.js +103 -8
  43. package/dist/execution/base.js.map +1 -1
  44. package/dist/execution/errors.d.ts +12 -0
  45. package/dist/execution/errors.d.ts.map +1 -0
  46. package/dist/execution/errors.js +21 -0
  47. package/dist/execution/errors.js.map +1 -0
  48. package/dist/execution/factory.d.ts.map +1 -1
  49. package/dist/execution/factory.js +1 -0
  50. package/dist/execution/factory.js.map +1 -1
  51. package/dist/execution/hybrid.d.ts +5 -0
  52. package/dist/execution/hybrid.d.ts.map +1 -1
  53. package/dist/execution/hybrid.js +36 -16
  54. package/dist/execution/hybrid.js.map +1 -1
  55. package/dist/execution/index.d.ts +1 -0
  56. package/dist/execution/index.d.ts.map +1 -1
  57. package/dist/execution/index.js +1 -0
  58. package/dist/execution/index.js.map +1 -1
  59. package/dist/execution/local.d.ts +13 -0
  60. package/dist/execution/local.d.ts.map +1 -1
  61. package/dist/execution/local.js +145 -19
  62. package/dist/execution/local.js.map +1 -1
  63. package/dist/execution/output.d.ts +27 -0
  64. package/dist/execution/output.d.ts.map +1 -0
  65. package/dist/execution/output.js +105 -0
  66. package/dist/execution/output.js.map +1 -0
  67. package/dist/execution/remote.d.ts +26 -2
  68. package/dist/execution/remote.d.ts.map +1 -1
  69. package/dist/execution/remote.js +86 -15
  70. package/dist/execution/remote.js.map +1 -1
  71. package/dist/plugin/lifecycle.d.ts.map +1 -1
  72. package/dist/plugin/lifecycle.js +9 -3
  73. package/dist/plugin/lifecycle.js.map +1 -1
  74. package/dist/process/kill-tree.d.ts +11 -13
  75. package/dist/process/kill-tree.d.ts.map +1 -1
  76. package/dist/process/kill-tree.js +11 -13
  77. package/dist/process/kill-tree.js.map +1 -1
  78. package/dist/provider/capabilities.d.ts +2 -0
  79. package/dist/provider/capabilities.d.ts.map +1 -1
  80. package/dist/provider/capabilities.js +6 -0
  81. package/dist/provider/capabilities.js.map +1 -1
  82. package/dist/provider/fallback.d.ts.map +1 -1
  83. package/dist/provider/fallback.js +31 -0
  84. package/dist/provider/fallback.js.map +1 -1
  85. package/dist/provider/idle-timeout.d.ts.map +1 -1
  86. package/dist/provider/idle-timeout.js +5 -0
  87. package/dist/provider/idle-timeout.js.map +1 -1
  88. package/dist/provider/retry.d.ts.map +1 -1
  89. package/dist/provider/retry.js +5 -0
  90. package/dist/provider/retry.js.map +1 -1
  91. package/dist/public-runtime.d.ts +2 -2
  92. package/dist/public-runtime.d.ts.map +1 -1
  93. package/dist/public-runtime.js +2 -2
  94. package/dist/public-runtime.js.map +1 -1
  95. package/dist/registry/tool/execute.d.ts +7 -1
  96. package/dist/registry/tool/execute.d.ts.map +1 -1
  97. package/dist/registry/tool/execute.js +201 -54
  98. package/dist/registry/tool/execute.js.map +1 -1
  99. package/dist/run/command-gate.d.ts.map +1 -1
  100. package/dist/run/command-gate.js +11 -0
  101. package/dist/run/command-gate.js.map +1 -1
  102. package/dist/run/workspace-fingerprint.d.ts +4 -4
  103. package/dist/run/workspace-fingerprint.d.ts.map +1 -1
  104. package/dist/run/workspace-fingerprint.js +6 -4
  105. package/dist/run/workspace-fingerprint.js.map +1 -1
  106. package/dist/runtime/bidi/index.d.ts +1 -1
  107. package/dist/runtime/bidi/index.d.ts.map +1 -1
  108. package/dist/runtime/bidi/index.js +1 -1
  109. package/dist/runtime/bidi/index.js.map +1 -1
  110. package/dist/runtime/bidi/session.d.ts +19 -0
  111. package/dist/runtime/bidi/session.d.ts.map +1 -1
  112. package/dist/runtime/bidi/session.js +150 -26
  113. package/dist/runtime/bidi/session.js.map +1 -1
  114. package/dist/runtime/query/executor.d.ts +43 -1
  115. package/dist/runtime/query/executor.d.ts.map +1 -1
  116. package/dist/runtime/query/executor.js +626 -161
  117. package/dist/runtime/query/executor.js.map +1 -1
  118. package/dist/runtime/query/index.d.ts +3 -1
  119. package/dist/runtime/query/index.d.ts.map +1 -1
  120. package/dist/runtime/query/index.js +48 -4
  121. package/dist/runtime/query/index.js.map +1 -1
  122. package/dist/runtime/query/iteration/index.d.ts +10 -0
  123. package/dist/runtime/query/iteration/index.d.ts.map +1 -1
  124. package/dist/runtime/query/iteration/index.js +67 -0
  125. package/dist/runtime/query/iteration/index.js.map +1 -1
  126. package/dist/runtime/query/iteration/phases/context.d.ts +5 -0
  127. package/dist/runtime/query/iteration/phases/context.d.ts.map +1 -1
  128. package/dist/runtime/query/iteration/phases/context.js.map +1 -1
  129. package/dist/runtime/query/iteration/phases/tool-review.d.ts.map +1 -1
  130. package/dist/runtime/query/iteration/phases/tool-review.js +84 -22
  131. package/dist/runtime/query/iteration/phases/tool-review.js.map +1 -1
  132. package/dist/runtime/query/request-rich-content.d.ts.map +1 -1
  133. package/dist/runtime/query/request-rich-content.js +33 -13
  134. package/dist/runtime/query/request-rich-content.js.map +1 -1
  135. package/dist/runtime/query/result.d.ts.map +1 -1
  136. package/dist/runtime/query/result.js +25 -22
  137. package/dist/runtime/query/result.js.map +1 -1
  138. package/dist/runtime/query/resume-pending.d.ts +5 -1
  139. package/dist/runtime/query/resume-pending.d.ts.map +1 -1
  140. package/dist/runtime/query/resume-pending.js +66 -1
  141. package/dist/runtime/query/resume-pending.js.map +1 -1
  142. package/dist/runtime/query/tool-output-budget.d.ts.map +1 -1
  143. package/dist/runtime/query/tool-output-budget.js +45 -16
  144. package/dist/runtime/query/tool-output-budget.js.map +1 -1
  145. package/dist/runtime/query/tooling.d.ts +6 -0
  146. package/dist/runtime/query/tooling.d.ts.map +1 -1
  147. package/dist/runtime/query/tooling.js +4 -0
  148. package/dist/runtime/query/tooling.js.map +1 -1
  149. package/dist/sandbox/provider/local.d.ts.map +1 -1
  150. package/dist/sandbox/provider/local.js +193 -41
  151. package/dist/sandbox/provider/local.js.map +1 -1
  152. package/dist/scheduler/delegating.d.ts +2 -1
  153. package/dist/scheduler/delegating.d.ts.map +1 -1
  154. package/dist/scheduler/delegating.js +6 -5
  155. package/dist/scheduler/delegating.js.map +1 -1
  156. package/dist/scheduler/local.d.ts +2 -1
  157. package/dist/scheduler/local.d.ts.map +1 -1
  158. package/dist/scheduler/local.js +2 -2
  159. package/dist/scheduler/local.js.map +1 -1
  160. package/dist/session/workspace/driver.d.ts +12 -4
  161. package/dist/session/workspace/driver.d.ts.map +1 -1
  162. package/dist/session/workspace/git-worktree.d.ts +17 -3
  163. package/dist/session/workspace/git-worktree.d.ts.map +1 -1
  164. package/dist/session/workspace/git-worktree.js +153 -34
  165. package/dist/session/workspace/git-worktree.js.map +1 -1
  166. package/dist/skills/registry.d.ts +15 -0
  167. package/dist/skills/registry.d.ts.map +1 -1
  168. package/dist/skills/registry.js +33 -0
  169. package/dist/skills/registry.js.map +1 -1
  170. package/dist/tools/builtins/computer-use.d.ts.map +1 -1
  171. package/dist/tools/builtins/computer-use.js +76 -2
  172. package/dist/tools/builtins/computer-use.js.map +1 -1
  173. package/dist/tools/builtins/run-code.d.ts.map +1 -1
  174. package/dist/tools/builtins/run-code.js +9 -11
  175. package/dist/tools/builtins/run-code.js.map +1 -1
  176. package/dist/tools/builtins/skill.d.ts +2 -1
  177. package/dist/tools/builtins/skill.d.ts.map +1 -1
  178. package/dist/tools/builtins/skill.js +250 -6
  179. package/dist/tools/builtins/skill.js.map +1 -1
  180. package/dist/tools/coordinator/agent.d.ts.map +1 -1
  181. package/dist/tools/coordinator/agent.js +100 -30
  182. package/dist/tools/coordinator/agent.js.map +1 -1
  183. package/dist/types/agent/manager.d.ts +1 -1
  184. package/dist/types/agent/manager.d.ts.map +1 -1
  185. package/dist/types/agent/scheduler.d.ts +10 -1
  186. package/dist/types/agent/scheduler.d.ts.map +1 -1
  187. package/dist/types/authorization/index.d.ts +12 -12
  188. package/dist/types/bidi/index.d.ts +19 -2
  189. package/dist/types/bidi/index.d.ts.map +1 -1
  190. package/dist/types/computer-use/index.d.ts +18 -0
  191. package/dist/types/computer-use/index.d.ts.map +1 -1
  192. package/dist/types/connector/execution-context.d.ts +3 -1
  193. package/dist/types/connector/execution-context.d.ts.map +1 -1
  194. package/dist/types/errors/catalog.d.ts.map +1 -1
  195. package/dist/types/errors/catalog.js +13 -6
  196. package/dist/types/errors/catalog.js.map +1 -1
  197. package/dist/types/errors/index.d.ts.map +1 -1
  198. package/dist/types/errors/index.js +21 -8
  199. package/dist/types/errors/index.js.map +1 -1
  200. package/dist/types/execution/index.d.ts +43 -1
  201. package/dist/types/execution/index.d.ts.map +1 -1
  202. package/dist/types/hitl/index.d.ts +11 -0
  203. package/dist/types/hitl/index.d.ts.map +1 -1
  204. package/dist/types/hitl/index.js.map +1 -1
  205. package/dist/types/message/index.d.ts +1 -1
  206. package/dist/types/message/index.d.ts.map +1 -1
  207. package/dist/types/message/index.js +2 -1
  208. package/dist/types/message/index.js.map +1 -1
  209. package/dist/types/plugin/index.d.ts +14 -1
  210. package/dist/types/plugin/index.d.ts.map +1 -1
  211. package/dist/types/plugin/index.js +1 -0
  212. package/dist/types/plugin/index.js.map +1 -1
  213. package/dist/types/provider/config.d.ts +9 -0
  214. package/dist/types/provider/config.d.ts.map +1 -1
  215. package/dist/types/provider/interface.d.ts +14 -0
  216. package/dist/types/provider/interface.d.ts.map +1 -1
  217. package/dist/types/run/events.d.ts +17 -0
  218. package/dist/types/run/events.d.ts.map +1 -1
  219. package/dist/types/run/events.js.map +1 -1
  220. package/dist/types/sandbox/index.d.ts +15 -10
  221. package/dist/types/sandbox/index.d.ts.map +1 -1
  222. package/dist/types/sandbox/index.js.map +1 -1
  223. package/dist/types/tool/index.d.ts +77 -6
  224. package/dist/types/tool/index.d.ts.map +1 -1
  225. package/dist/types/tool/index.js.map +1 -1
  226. package/dist/types/tool/presentation.d.ts +6 -2
  227. package/dist/types/tool/presentation.d.ts.map +1 -1
  228. package/dist/types/workspace/ref.d.ts +5 -0
  229. package/dist/types/workspace/ref.d.ts.map +1 -1
  230. package/dist/utils/abort.d.ts +8 -0
  231. package/dist/utils/abort.d.ts.map +1 -1
  232. package/dist/utils/abort.js +16 -0
  233. package/dist/utils/abort.js.map +1 -1
  234. package/package.json +4 -4
  235. package/src/authorization/gate.ts +1 -1
  236. package/src/bridge/a2a/mapper.ts +23 -4
  237. package/src/bridge/sse/mapper.ts +3 -0
  238. package/src/connector/index.ts +2 -0
  239. package/src/connector/mcp/adapter.ts +82 -21
  240. package/src/connector/mcp/client.ts +2 -1
  241. package/src/connector/mcp/http-redirect.ts +32 -0
  242. package/src/connector/mcp/http-sse.ts +5 -0
  243. package/src/connector/mcp/image-admission.ts +272 -0
  244. package/src/connector/mcp/stdio.ts +63 -21
  245. package/src/connector/mcp/streamable-http.ts +3 -0
  246. package/src/constants/tools/index.ts +1 -1
  247. package/src/execution/base.ts +124 -7
  248. package/src/execution/errors.ts +24 -0
  249. package/src/execution/factory.ts +1 -0
  250. package/src/execution/hybrid.ts +47 -16
  251. package/src/execution/index.ts +1 -0
  252. package/src/execution/local.ts +164 -19
  253. package/src/execution/output.ts +104 -0
  254. package/src/execution/remote.ts +122 -17
  255. package/src/plugin/lifecycle.ts +9 -3
  256. package/src/process/kill-tree.ts +11 -13
  257. package/src/provider/capabilities.ts +10 -0
  258. package/src/provider/fallback.ts +39 -0
  259. package/src/provider/idle-timeout.ts +8 -0
  260. package/src/provider/retry.ts +8 -0
  261. package/src/public-runtime.ts +7 -1
  262. package/src/registry/tool/execute.ts +230 -64
  263. package/src/run/command-gate.ts +12 -0
  264. package/src/run/workspace-fingerprint.ts +5 -4
  265. package/src/runtime/bidi/index.ts +1 -1
  266. package/src/runtime/bidi/session.ts +177 -28
  267. package/src/runtime/query/executor.ts +769 -163
  268. package/src/runtime/query/index.ts +58 -7
  269. package/src/runtime/query/iteration/index.ts +72 -0
  270. package/src/runtime/query/iteration/phases/context.ts +5 -0
  271. package/src/runtime/query/iteration/phases/tool-review.ts +87 -23
  272. package/src/runtime/query/request-rich-content.ts +39 -14
  273. package/src/runtime/query/result.ts +25 -23
  274. package/src/runtime/query/resume-pending.ts +83 -1
  275. package/src/runtime/query/tool-output-budget.ts +52 -17
  276. package/src/runtime/query/tooling.ts +10 -0
  277. package/src/sandbox/provider/local.ts +213 -45
  278. package/src/scheduler/delegating.ts +6 -5
  279. package/src/scheduler/local.ts +3 -2
  280. package/src/session/workspace/driver.ts +12 -4
  281. package/src/session/workspace/git-worktree.ts +173 -34
  282. package/src/skills/registry.ts +47 -0
  283. package/src/tools/builtins/computer-use.ts +86 -2
  284. package/src/tools/builtins/run-code.ts +9 -11
  285. package/src/tools/builtins/skill.ts +325 -7
  286. package/src/tools/coordinator/agent.ts +108 -33
  287. package/src/types/agent/manager.ts +1 -1
  288. package/src/types/agent/scheduler.ts +10 -1
  289. package/src/types/bidi/index.ts +19 -2
  290. package/src/types/computer-use/index.ts +19 -0
  291. package/src/types/connector/execution-context.ts +3 -0
  292. package/src/types/errors/catalog.ts +14 -6
  293. package/src/types/errors/index.ts +22 -8
  294. package/src/types/execution/index.ts +46 -1
  295. package/src/types/hitl/index.ts +11 -0
  296. package/src/types/message/index.ts +3 -2
  297. package/src/types/plugin/index.ts +15 -0
  298. package/src/types/provider/config.ts +9 -0
  299. package/src/types/provider/interface.ts +18 -0
  300. package/src/types/run/events.ts +17 -3
  301. package/src/types/sandbox/index.ts +15 -10
  302. package/src/types/tool/index.ts +85 -6
  303. package/src/types/tool/presentation.ts +10 -3
  304. package/src/types/workspace/ref.ts +5 -0
  305. package/src/utils/abort.ts +18 -0
@@ -95,6 +95,10 @@ export class StdioTransport implements MCPTransport {
95
95
  private closeHandlers: Array<() => void> = []
96
96
  private errorHandlers: Array<(error: Error) => void> = []
97
97
  private connected = false
98
+ /** The response channel ended for this process generation. */
99
+ private retired = false
100
+ /** Prevents a late event from an old child retiring its replacement. */
101
+ private generation = 0
98
102
  /** True between kill and the process's own `close` event. */
99
103
  private exitPending = false
100
104
  private buffer = ''
@@ -109,38 +113,47 @@ export class StdioTransport implements MCPTransport {
109
113
 
110
114
  async connect(): Promise<void> {
111
115
  if (this.connected) return
116
+ // A protocol channel can end while its process stays alive. Reconnect
117
+ // must reap that old owner before publishing a new generation; otherwise
118
+ // both children remain live and teardown owns only the newest one.
119
+ if (this.process) await this.close()
120
+ const generation = ++this.generation
121
+ this.retired = false
122
+ this.buffer = ''
112
123
 
113
- this.process = spawn(this.config.command, this.config.args ?? [], {
124
+ const child = spawn(this.config.command, this.config.args ?? [], {
114
125
  env: buildChildEnv(this.config),
115
126
  cwd: this.config.cwd,
116
127
  stdio: ['pipe', 'pipe', 'pipe'],
117
128
  })
129
+ this.process = child
118
130
 
119
- this.process.stdout?.on('data', (chunk: Buffer) => {
131
+ child.stdout?.on('data', (chunk: Buffer) => {
132
+ if (generation !== this.generation || this.retired) return
120
133
  this.buffer += chunk.toString('utf-8')
121
134
  this.processBuffer()
122
135
  })
136
+ child.stdout?.once('end', () => this.retire(generation, 'close'))
137
+ child.stdout?.once('close', () => this.retire(generation, 'close'))
138
+ child.stdout?.once('error', (err) => this.retire(generation, 'error', err))
123
139
 
124
- this.process.stderr?.on('data', (chunk: Buffer) => {
140
+ child.stderr?.on('data', (chunk: Buffer) => {
125
141
  this.log.warn('MCP server stderr', {
126
142
  'namzu.mcp.stderr': chunk.toString('utf-8').trim(),
127
143
  })
128
144
  })
129
145
 
130
- this.process.on('close', (code) => {
131
- this.connected = false
132
- this.log.info('MCP server process exited', { 'namzu.mcp.exit_code': code })
133
- for (const handler of this.closeHandlers) handler()
134
- // After the notification, not before it: this event is what tells
135
- // `MCPClient` the session ended.
146
+ child.stdin?.once('close', () => this.retire(generation, 'close'))
147
+ child.stdin?.once('error', (err) => this.retire(generation, 'error', err))
148
+
149
+ child.on('close', (code) => {
150
+ if (this.process === child) this.process = null
136
151
  this.exitPending = false
137
- this.clearHandlers()
152
+ this.log.info('MCP server process exited', { 'namzu.mcp.exit_code': code })
153
+ this.retire(generation, 'close')
138
154
  })
139
155
 
140
- this.process.on('error', (err) => {
141
- this.connected = false
142
- for (const handler of this.errorHandlers) handler(err)
143
- })
156
+ child.on('error', (err) => this.retire(generation, 'error', err))
144
157
 
145
158
  this.connected = true
146
159
  this.log.info('StdioTransport connected', {
@@ -175,7 +188,10 @@ export class StdioTransport implements MCPTransport {
175
188
  // so both settle this, and two timers make a hang impossible: the
176
189
  // first escalates to SIGKILL for a child ignoring SIGTERM, the second
177
190
  // gives up waiting. Neither holds the event loop open.
178
- if (child.pid === undefined) return
191
+ if (child.pid === undefined) {
192
+ this.exitPending = false
193
+ return
194
+ }
179
195
  await new Promise<void>((resolve) => {
180
196
  let settled = false
181
197
  const finish = (): void => {
@@ -185,18 +201,20 @@ export class StdioTransport implements MCPTransport {
185
201
  clearTimeout(giveUp)
186
202
  resolve()
187
203
  }
188
- child.once('exit', finish)
204
+ child.once('close', finish)
189
205
  child.once('error', finish)
190
- child.kill('SIGTERM')
206
+ if (child.exitCode === null && child.signalCode === null) {
207
+ child.kill('SIGTERM')
208
+ }
191
209
  const escalate = setTimeout(() => child.kill('SIGKILL'), TERMINATE_GRACE_MS)
192
210
  const giveUp = setTimeout(finish, TERMINATE_GRACE_MS * 2)
193
211
  escalate.unref?.()
194
212
  giveUp.unref?.()
195
213
  })
196
- // The handlers deliberately survive this call. `process.on('close')`
197
- // fires them when the process actually exits, and dropping them here
198
- // would leave `MCPClient` believing it is still connected — so its next
199
- // `connect()` would be refused with "already connected".
214
+ this.exitPending = false
215
+ // The active generation's terminal stream/process event owns handler
216
+ // retirement. A reconnect can register its handlers before `connect()`
217
+ // reaps an already-retired child, so `close()` must not clear them here.
200
218
  }
201
219
 
202
220
  /**
@@ -214,6 +232,30 @@ export class StdioTransport implements MCPTransport {
214
232
  this.errorHandlers = []
215
233
  }
216
234
 
235
+ /**
236
+ * Publish one terminal result for the current response channel.
237
+ *
238
+ * A child may close stdout and keep running. That is just as terminal for
239
+ * JSON-RPC as process exit: no pending or future request can receive a
240
+ * response. Keeping the child handle is intentional; `close()` still owns
241
+ * and reaps that process after the protocol session has been retired.
242
+ */
243
+ private retire(generation: number, kind: 'close' | 'error', error?: Error): void {
244
+ if (generation !== this.generation || this.retired) return
245
+ this.retired = true
246
+ this.connected = false
247
+ this.buffer = ''
248
+ const closeHandlers = this.closeHandlers
249
+ const errorHandlers = this.errorHandlers
250
+ this.clearHandlers()
251
+ if (kind === 'error') {
252
+ const reason = error ?? new Error('MCP stdio transport failed')
253
+ for (const handler of errorHandlers) handler(reason)
254
+ return
255
+ }
256
+ for (const handler of closeHandlers) handler()
257
+ }
258
+
217
259
  async send(message: MCPJsonRpcMessage, options?: MCPTransportSendOptions): Promise<void> {
218
260
  options?.signal?.throwIfAborted()
219
261
  if (!this.process?.stdin?.writable) {
@@ -7,6 +7,7 @@ import type {
7
7
  import { SCOPE_ATTRIBUTE } from '../../utils/log/types.js'
8
8
  import { type Logger, resolveLogger } from '../../utils/logger.js'
9
9
  import { ConnectorHttpOperation, validateConnectorTimeoutMs } from '../http-operation.js'
10
+ import { refuseMcpHttpRedirect } from './http-redirect.js'
10
11
 
11
12
  const DEFAULT_TIMEOUT_MS = 30_000
12
13
 
@@ -83,10 +84,12 @@ export class StreamableHttpTransport implements MCPTransport {
83
84
  method: 'POST',
84
85
  headers: this.buildHeaders(),
85
86
  body: JSON.stringify(message),
87
+ redirect: 'manual',
86
88
  signal: operation.signal,
87
89
  }),
88
90
  )
89
91
 
92
+ refuseMcpHttpRedirect(response, message.method)
90
93
  if (!response.ok) {
91
94
  throw new Error(`StreamableHttpTransport: HTTP ${response.status}: ${response.statusText}`)
92
95
  }
@@ -1,4 +1,4 @@
1
- // Patterns the verification gate's `deny_dangerous_patterns` rule
1
+ // Patterns the authorization gate's `deny_dangerous_patterns` rule
2
2
  // matches against the JSON-serialised tool input. The list is
3
3
  // intentionally short and high-signal: the goal is to catch the
4
4
  // canonical "I will brick the host" mistakes (filesystem wipes,
@@ -12,6 +12,21 @@ export interface BaseExecutionEvent {
12
12
 
13
13
  export type ExecutionEventListener = (event: BaseExecutionEvent) => void
14
14
 
15
+ type ExecutionLifecycleState =
16
+ | 'idle'
17
+ | 'initializing'
18
+ | 'ready'
19
+ | 'tearing-down'
20
+ | 'teardown-failed'
21
+ | 'torn-down'
22
+
23
+ class InitializationSupersededError extends Error {
24
+ constructor(contextId: string) {
25
+ super(`Execution context "${contextId}" initialization was superseded by teardown`)
26
+ this.name = 'InitializationSupersededError'
27
+ }
28
+ }
29
+
15
30
  export abstract class BaseExecutionContext implements ExecutionContextLifecycle {
16
31
  abstract readonly id: string
17
32
  abstract readonly environment: ExecutionEnvironment
@@ -19,6 +34,9 @@ export abstract class BaseExecutionContext implements ExecutionContextLifecycle
19
34
  protected log: Logger
20
35
  protected ready = false
21
36
  private listeners: ExecutionEventListener[] = []
37
+ private lifecycleState: ExecutionLifecycleState = 'idle'
38
+ private initializationOperation: Promise<void> | undefined
39
+ private teardownOperation: Promise<void> | undefined
22
40
 
23
41
  constructor(log?: Logger) {
24
42
  this.log = resolveLogger(log).child({
@@ -27,17 +45,62 @@ export abstract class BaseExecutionContext implements ExecutionContextLifecycle
27
45
  })
28
46
  }
29
47
 
30
- async initialize(): Promise<void> {
48
+ initialize(): Promise<void> {
49
+ if (this.lifecycleState === 'ready') return Promise.resolve()
50
+ if (this.lifecycleState === 'initializing' && this.initializationOperation) {
51
+ return this.initializationOperation
52
+ }
53
+ if (this.lifecycleState === 'tearing-down') {
54
+ return Promise.reject(
55
+ new Error(`Execution context "${this.id}" cannot initialize while teardown is in progress`),
56
+ )
57
+ }
58
+ if (this.lifecycleState === 'teardown-failed') {
59
+ return Promise.reject(
60
+ new Error(
61
+ `Execution context "${this.id}" cannot initialize until its failed teardown is retried`,
62
+ ),
63
+ )
64
+ }
65
+
66
+ this.lifecycleState = 'initializing'
67
+ const operation = this.performInitialize()
68
+ this.initializationOperation = operation
69
+ void operation.then(
70
+ () => this.releaseInitialization(operation),
71
+ () => this.releaseInitialization(operation),
72
+ )
73
+ return operation
74
+ }
75
+
76
+ private async performInitialize(): Promise<void> {
31
77
  try {
78
+ this.onInitializationStarted()
32
79
  await this.doInitialize()
80
+ if (this.lifecycleState !== 'initializing') {
81
+ throw new InitializationSupersededError(this.id)
82
+ }
83
+ this.onInitializationCommitted()
84
+ if (this.lifecycleState !== 'initializing') {
85
+ throw new InitializationSupersededError(this.id)
86
+ }
33
87
  this.ready = true
34
- this.emit({ type: 'context_initialized', contextId: this.id, environment: this.environment })
35
- this.emit({ type: 'context_ready', contextId: this.id })
88
+ this.lifecycleState = 'ready'
36
89
  this.log.info('Execution context initialized', {
37
90
  'namzu.execution.context_id': this.id,
38
91
  'namzu.execution.environment': this.environment,
39
92
  })
93
+ this.emit({
94
+ type: 'context_initialized',
95
+ contextId: this.id,
96
+ environment: this.environment,
97
+ })
98
+ if (this.lifecycleState !== 'ready') return
99
+ this.emit({ type: 'context_ready', contextId: this.id })
40
100
  } catch (err) {
101
+ if (err instanceof InitializationSupersededError) throw err
102
+ this.ready = false
103
+ if (this.lifecycleState === 'initializing') this.lifecycleState = 'idle'
41
104
  const message = toErrorMessage(err)
42
105
  this.emit({ type: 'context_error', contextId: this.id, error: message })
43
106
  this.log.error('Execution context initialization failed', {
@@ -48,20 +111,65 @@ export abstract class BaseExecutionContext implements ExecutionContextLifecycle
48
111
  }
49
112
  }
50
113
 
114
+ private releaseInitialization(operation: Promise<void>): void {
115
+ if (this.initializationOperation === operation) this.initializationOperation = undefined
116
+ }
117
+
51
118
  isReady(): boolean {
52
119
  return this.ready
53
120
  }
54
121
 
55
- async teardown(): Promise<void> {
122
+ teardown(): Promise<void> {
123
+ if (this.lifecycleState === 'tearing-down' && this.teardownOperation) {
124
+ return this.teardownOperation
125
+ }
126
+ if (this.lifecycleState === 'torn-down') return Promise.resolve()
127
+
128
+ this.ready = false
129
+ this.lifecycleState = 'tearing-down'
130
+ const operation = this.performTeardown(this.initializationOperation)
131
+ this.teardownOperation = operation
132
+ void operation.then(
133
+ () => this.releaseTeardown(operation),
134
+ () => this.releaseTeardown(operation),
135
+ )
136
+ return operation
137
+ }
138
+
139
+ private async performTeardown(pendingInitialization: Promise<void> | undefined): Promise<void> {
56
140
  try {
141
+ // Runs before the first await. Subclasses close public admission here so
142
+ // work cannot slip behind an initialization this teardown must await.
143
+ this.onTeardownRequested()
144
+ if (pendingInitialization) {
145
+ try {
146
+ await pendingInitialization
147
+ } catch {
148
+ // Initialization errors are reported by their owner. Cleanup still runs.
149
+ }
150
+ }
57
151
  await this.doTeardown()
58
- } finally {
59
- this.ready = false
152
+ this.lifecycleState = 'torn-down'
153
+ this.log.info('Execution context torn down', {
154
+ 'namzu.execution.context_id': this.id,
155
+ })
60
156
  this.emit({ type: 'context_teardown', contextId: this.id })
61
- this.log.info('Execution context torn down', { 'namzu.execution.context_id': this.id })
157
+ } catch (err) {
158
+ this.lifecycleState = 'teardown-failed'
159
+ const message = toErrorMessage(err)
160
+ this.emit({ type: 'context_error', contextId: this.id, error: message })
161
+ this.log.error('Execution context teardown failed', {
162
+ 'namzu.execution.context_id': this.id,
163
+ 'exception.message': message,
164
+ })
165
+ throw err
62
166
  }
63
167
  }
64
168
 
169
+ private releaseTeardown(operation: Promise<void>): void {
170
+ if (this.teardownOperation === operation) this.teardownOperation = undefined
171
+ }
172
+
65
173
  on(listener: ExecutionEventListener): void {
66
174
  this.listeners.push(listener)
67
175
  }
@@ -85,4 +193,13 @@ export abstract class BaseExecutionContext implements ExecutionContextLifecycle
85
193
 
86
194
  protected abstract doInitialize(): Promise<void>
87
195
  protected abstract doTeardown(): Promise<void>
196
+
197
+ /** Fence subclass-owned public admission before initialization begins. */
198
+ protected onInitializationStarted(): void {}
199
+
200
+ /** Reopen admission only after Base has proved initialization was not superseded. */
201
+ protected onInitializationCommitted(): void {}
202
+
203
+ /** Fence subclass-owned public admission synchronously when teardown is requested. */
204
+ protected onTeardownRequested(): void {}
88
205
  }
@@ -0,0 +1,24 @@
1
+ export class CommandCancellationUnsupportedError extends Error {
2
+ readonly code = 'command_cancellation_unsupported' as const
3
+
4
+ constructor(readonly contextId: string) {
5
+ super(
6
+ `Remote execution context "${contextId}" cannot accept command cancellation because its generic executor has no reservation and terminal-acknowledgement protocol. No remote command was started. Use the Sandbox.exec() contract for cancellable remote command execution.`,
7
+ )
8
+ this.name = 'CommandCancellationUnsupportedError'
9
+ }
10
+ }
11
+
12
+ export class RemoteExecutionBusyError extends Error {
13
+ readonly code = 'remote_execution_busy' as const
14
+
15
+ constructor(
16
+ readonly contextId: string,
17
+ readonly activeCommandCount: number,
18
+ ) {
19
+ super(
20
+ `Remote execution context "${contextId}" still owns ${activeCommandCount} active command${activeCommandCount === 1 ? '' : 's'}. Disconnect or teardown was refused; wait for the command${activeCommandCount === 1 ? '' : 's'} to settle, then retry.`,
21
+ )
22
+ this.name = 'RemoteExecutionBusyError'
23
+ }
24
+ }
@@ -15,6 +15,7 @@ export class ExecutionContextFactory {
15
15
  envVars: config.envVars,
16
16
  capabilities: config.capabilities,
17
17
  shell: config.shell,
18
+ maxOutputBytes: config.maxOutputBytes,
18
19
  })
19
20
 
20
21
  case 'remote':
@@ -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'