@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
@@ -0,0 +1,272 @@
1
+ import type { MCPContentBlock } from '../../types/connector/index.js'
2
+
3
+ const MAX_ENCODED_BYTES = 24 * 1024 * 1024
4
+ const MAX_DIMENSION = 16_384
5
+ const MAX_PIXELS = 40_000_000
6
+
7
+ type MCPImageBlock = Extract<MCPContentBlock, { type: 'image' }>
8
+ type ImageMediaType = 'image/png' | 'image/jpeg' | 'image/webp' | 'image/gif'
9
+
10
+ const IMAGE_MEDIA_TYPES = new Set<ImageMediaType>([
11
+ 'image/png',
12
+ 'image/jpeg',
13
+ 'image/webp',
14
+ 'image/gif',
15
+ ])
16
+
17
+ function isImageMediaType(value: string): value is ImageMediaType {
18
+ return IMAGE_MEDIA_TYPES.has(value as ImageMediaType)
19
+ }
20
+
21
+ function decodeCanonicalBase64(value: string): Uint8Array | null {
22
+ if (value.length === 0 || value.length > MAX_ENCODED_BYTES) return null
23
+ if (!/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(value)) {
24
+ return null
25
+ }
26
+ const decoded = Buffer.from(value, 'base64')
27
+ return decoded.toString('base64') === value ? decoded : null
28
+ }
29
+
30
+ function dimensionsAreSafe(width: number, height: number): boolean {
31
+ return (
32
+ Number.isSafeInteger(width) &&
33
+ Number.isSafeInteger(height) &&
34
+ width > 0 &&
35
+ height > 0 &&
36
+ width <= MAX_DIMENSION &&
37
+ height <= MAX_DIMENSION &&
38
+ width * height <= MAX_PIXELS
39
+ )
40
+ }
41
+
42
+ const crcTable = (() => {
43
+ const table = new Uint32Array(256)
44
+ for (let n = 0; n < 256; n += 1) {
45
+ let value = n
46
+ for (let bit = 0; bit < 8; bit += 1) {
47
+ value = value & 1 ? 0xedb88320 ^ (value >>> 1) : value >>> 1
48
+ }
49
+ table[n] = value >>> 0
50
+ }
51
+ return table
52
+ })()
53
+
54
+ function crc32(bytes: Uint8Array, start: number, end: number): number {
55
+ let crc = 0xffffffff
56
+ for (let offset = start; offset < end; offset += 1) {
57
+ crc = (crcTable[(crc ^ (bytes[offset] ?? 0)) & 0xff] ?? 0) ^ (crc >>> 8)
58
+ }
59
+ return (crc ^ 0xffffffff) >>> 0
60
+ }
61
+
62
+ function isPng(bytes: Uint8Array): boolean {
63
+ const buffer = Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength)
64
+ if (bytes.length < 45 || !buffer.subarray(0, 8).equals(Buffer.from('89504e470d0a1a0a', 'hex'))) {
65
+ return false
66
+ }
67
+ let offset = 8
68
+ let sawHeader = false
69
+ let sawData = false
70
+ while (offset + 12 <= bytes.length) {
71
+ const length = buffer.readUInt32BE(offset)
72
+ const typeStart = offset + 4
73
+ const dataStart = typeStart + 4
74
+ const dataEnd = dataStart + length
75
+ const chunkEnd = dataEnd + 4
76
+ if (dataEnd < dataStart || chunkEnd > bytes.length) return false
77
+ const type = buffer.toString('ascii', typeStart, dataStart)
78
+ if (buffer.readUInt32BE(dataEnd) !== crc32(bytes, typeStart, dataEnd)) return false
79
+ if (!sawHeader) {
80
+ if (type !== 'IHDR' || length !== 13) return false
81
+ const width = buffer.readUInt32BE(dataStart)
82
+ const height = buffer.readUInt32BE(dataStart + 4)
83
+ if (!dimensionsAreSafe(width, height)) return false
84
+ sawHeader = true
85
+ } else if (type === 'IHDR') return false
86
+ if (type === 'IDAT') sawData = true
87
+ if (type === 'IEND') return length === 0 && sawHeader && sawData && chunkEnd === bytes.length
88
+ offset = chunkEnd
89
+ }
90
+ return false
91
+ }
92
+
93
+ const JPEG_SOF_MARKERS = new Set([
94
+ 0xc0, 0xc1, 0xc2, 0xc3, 0xc5, 0xc6, 0xc7, 0xc9, 0xca, 0xcb, 0xcd, 0xce, 0xcf,
95
+ ])
96
+
97
+ function isJpeg(bytes: Uint8Array): boolean {
98
+ const buffer = Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength)
99
+ if (bytes.length < 11 || bytes[0] !== 0xff || bytes[1] !== 0xd8) return false
100
+ let offset = 2
101
+ let sawFrame = false
102
+ let sawScan = false
103
+ let inScan = false
104
+ while (offset < bytes.length) {
105
+ if (inScan && bytes[offset] !== 0xff) {
106
+ offset += 1
107
+ continue
108
+ }
109
+ if (bytes[offset] !== 0xff) return false
110
+ while (offset < bytes.length && bytes[offset] === 0xff) offset += 1
111
+ if (offset >= bytes.length) return false
112
+ const marker = bytes[offset] ?? 0
113
+ offset += 1
114
+ if (inScan && (marker === 0x00 || (marker >= 0xd0 && marker <= 0xd7))) continue
115
+ inScan = false
116
+ if (marker === 0xd9) return sawFrame && sawScan && offset === bytes.length
117
+ if (marker === 0xd8 || marker === 0x01 || (marker >= 0xd0 && marker <= 0xd7)) continue
118
+ if (offset + 2 > bytes.length) return false
119
+ const length = buffer.readUInt16BE(offset)
120
+ if (length < 2 || offset + length > bytes.length) return false
121
+ if (JPEG_SOF_MARKERS.has(marker)) {
122
+ if (length < 8) return false
123
+ const height = buffer.readUInt16BE(offset + 3)
124
+ const width = buffer.readUInt16BE(offset + 5)
125
+ if (!dimensionsAreSafe(width, height)) return false
126
+ sawFrame = true
127
+ }
128
+ offset += length
129
+ if (marker === 0xda) {
130
+ sawScan = true
131
+ inScan = true
132
+ }
133
+ }
134
+ return false
135
+ }
136
+
137
+ function skipGifSubBlocks(bytes: Uint8Array, start: number): number | null {
138
+ let offset = start
139
+ while (offset < bytes.length) {
140
+ const length = bytes[offset] ?? 0
141
+ offset += 1
142
+ if (length === 0) return offset
143
+ if (offset + length > bytes.length) return null
144
+ offset += length
145
+ }
146
+ return null
147
+ }
148
+
149
+ function isGif(bytes: Uint8Array): boolean {
150
+ const buffer = Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength)
151
+ if (bytes.length < 14) return false
152
+ const signature = buffer.toString('ascii', 0, 6)
153
+ if (signature !== 'GIF87a' && signature !== 'GIF89a') return false
154
+ if (!dimensionsAreSafe(buffer.readUInt16LE(6), buffer.readUInt16LE(8))) return false
155
+ let offset = 13
156
+ const packed = bytes[10] ?? 0
157
+ if ((packed & 0x80) !== 0) offset += 3 * 2 ** ((packed & 0x07) + 1)
158
+ if (offset > bytes.length) return false
159
+ let sawImage = false
160
+ while (offset < bytes.length) {
161
+ const marker = bytes[offset] ?? 0
162
+ offset += 1
163
+ if (marker === 0x3b) return sawImage && offset === bytes.length
164
+ if (marker === 0x21) {
165
+ if (offset >= bytes.length) return false
166
+ offset += 1
167
+ const next = skipGifSubBlocks(bytes, offset)
168
+ if (next === null) return false
169
+ offset = next
170
+ continue
171
+ }
172
+ if (marker !== 0x2c || offset + 9 > bytes.length) return false
173
+ const width = buffer.readUInt16LE(offset + 4)
174
+ const height = buffer.readUInt16LE(offset + 6)
175
+ if (!dimensionsAreSafe(width, height)) return false
176
+ const imagePacked = bytes[offset + 8] ?? 0
177
+ offset += 9
178
+ if ((imagePacked & 0x80) !== 0) offset += 3 * 2 ** ((imagePacked & 0x07) + 1)
179
+ if (offset >= bytes.length) return false
180
+ offset += 1
181
+ const next = skipGifSubBlocks(bytes, offset)
182
+ if (next === null) return false
183
+ offset = next
184
+ sawImage = true
185
+ }
186
+ return false
187
+ }
188
+
189
+ function readUInt24LE(bytes: Uint8Array, offset: number): number {
190
+ return (bytes[offset] ?? 0) | ((bytes[offset + 1] ?? 0) << 8) | ((bytes[offset + 2] ?? 0) << 16)
191
+ }
192
+
193
+ function webpDimensions(type: string, data: Uint8Array): readonly [number, number] | null {
194
+ const buffer = Buffer.from(data.buffer, data.byteOffset, data.byteLength)
195
+ if (type === 'VP8X') {
196
+ if (data.length < 10) return null
197
+ return [readUInt24LE(data, 4) + 1, readUInt24LE(data, 7) + 1]
198
+ }
199
+ if (type === 'VP8L') {
200
+ if (data.length < 5 || data[0] !== 0x2f) return null
201
+ const bits = buffer.readUInt32LE(1)
202
+ return [(bits & 0x3fff) + 1, ((bits >>> 14) & 0x3fff) + 1]
203
+ }
204
+ if (type === 'VP8 ') {
205
+ if (data.length < 10 || data[3] !== 0x9d || data[4] !== 0x01 || data[5] !== 0x2a) {
206
+ return null
207
+ }
208
+ return [buffer.readUInt16LE(6) & 0x3fff, buffer.readUInt16LE(8) & 0x3fff]
209
+ }
210
+ return null
211
+ }
212
+
213
+ function isWebp(bytes: Uint8Array): boolean {
214
+ const buffer = Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength)
215
+ if (
216
+ bytes.length < 20 ||
217
+ buffer.toString('ascii', 0, 4) !== 'RIFF' ||
218
+ buffer.readUInt32LE(4) + 8 !== bytes.length ||
219
+ buffer.toString('ascii', 8, 12) !== 'WEBP'
220
+ ) {
221
+ return false
222
+ }
223
+ let offset = 12
224
+ let dimensions: readonly [number, number] | null = null
225
+ let sawImagePayload = false
226
+ while (offset + 8 <= bytes.length) {
227
+ const type = buffer.toString('ascii', offset, offset + 4)
228
+ const length = buffer.readUInt32LE(offset + 4)
229
+ const dataStart = offset + 8
230
+ const dataEnd = dataStart + length
231
+ if (dataEnd < dataStart || dataEnd > bytes.length) return false
232
+ const current = webpDimensions(type, bytes.subarray(dataStart, dataEnd))
233
+ if (current && dimensions === null) dimensions = current
234
+ if (type === 'VP8 ' || type === 'VP8L') sawImagePayload = current !== null
235
+ offset = dataEnd + (length & 1)
236
+ }
237
+ return (
238
+ offset === bytes.length &&
239
+ dimensions !== null &&
240
+ sawImagePayload &&
241
+ dimensionsAreSafe(dimensions[0], dimensions[1])
242
+ )
243
+ }
244
+
245
+ function imageIsAdmitted(block: MCPImageBlock): boolean {
246
+ if (!isImageMediaType(block.mimeType)) return false
247
+ const bytes = decodeCanonicalBase64(block.data)
248
+ if (!bytes) return false
249
+ switch (block.mimeType) {
250
+ case 'image/png':
251
+ return isPng(bytes)
252
+ case 'image/jpeg':
253
+ return isJpeg(bytes)
254
+ case 'image/gif':
255
+ return isGif(bytes)
256
+ case 'image/webp':
257
+ return isWebp(bytes)
258
+ }
259
+ }
260
+
261
+ /**
262
+ * Admit an MCP image batch atomically before any member becomes model input.
263
+ *
264
+ * A remote server controls the declared MIME and bytes. Canonical base64 is
265
+ * only an encoding property; the decoded value must also be a complete,
266
+ * bounded raster container whose format agrees with that declaration. One bad
267
+ * member withholds the complete batch so a model is never shown a partial
268
+ * result while the host retains every raw block for inspection.
269
+ */
270
+ export function admitMcpImageBatch(images: readonly MCPImageBlock[]): boolean {
271
+ return images.length === 0 || images.every(imageIsAdmitted)
272
+ }
@@ -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':