@namzu/sdk 32.0.0 → 33.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +127 -0
- package/dist/authorization/gate.d.ts +1 -1
- package/dist/authorization/gate.js +1 -1
- package/dist/connector/index.d.ts +1 -1
- package/dist/connector/index.d.ts.map +1 -1
- package/dist/connector/index.js +1 -1
- package/dist/connector/index.js.map +1 -1
- package/dist/connector/mcp/adapter.d.ts.map +1 -1
- package/dist/connector/mcp/adapter.js +71 -16
- package/dist/connector/mcp/adapter.js.map +1 -1
- package/dist/connector/mcp/client.d.ts.map +1 -1
- package/dist/connector/mcp/client.js +3 -2
- package/dist/connector/mcp/client.js.map +1 -1
- package/dist/connector/mcp/http-redirect.d.ts +17 -0
- package/dist/connector/mcp/http-redirect.d.ts.map +1 -0
- package/dist/connector/mcp/http-redirect.js +31 -0
- package/dist/connector/mcp/http-redirect.js.map +1 -0
- package/dist/connector/mcp/http-sse.d.ts.map +1 -1
- package/dist/connector/mcp/http-sse.js +5 -0
- package/dist/connector/mcp/http-sse.js.map +1 -1
- package/dist/connector/mcp/image-admission.d.ts +16 -0
- package/dist/connector/mcp/image-admission.d.ts.map +1 -0
- package/dist/connector/mcp/image-admission.js +288 -0
- package/dist/connector/mcp/image-admission.js.map +1 -0
- package/dist/connector/mcp/stdio.d.ts +13 -0
- package/dist/connector/mcp/stdio.d.ts.map +1 -1
- package/dist/connector/mcp/stdio.js +66 -23
- package/dist/connector/mcp/stdio.js.map +1 -1
- package/dist/connector/mcp/streamable-http.d.ts.map +1 -1
- package/dist/connector/mcp/streamable-http.js +3 -0
- package/dist/connector/mcp/streamable-http.js.map +1 -1
- package/dist/constants/tools/index.js +1 -1
- package/dist/constants/tools/index.js.map +1 -1
- package/dist/execution/base.d.ts +13 -0
- package/dist/execution/base.d.ts.map +1 -1
- package/dist/execution/base.js +103 -8
- package/dist/execution/base.js.map +1 -1
- package/dist/execution/errors.d.ts +12 -0
- package/dist/execution/errors.d.ts.map +1 -0
- package/dist/execution/errors.js +21 -0
- package/dist/execution/errors.js.map +1 -0
- package/dist/execution/factory.d.ts.map +1 -1
- package/dist/execution/factory.js +1 -0
- package/dist/execution/factory.js.map +1 -1
- package/dist/execution/hybrid.d.ts +5 -0
- package/dist/execution/hybrid.d.ts.map +1 -1
- package/dist/execution/hybrid.js +36 -16
- package/dist/execution/hybrid.js.map +1 -1
- package/dist/execution/index.d.ts +1 -0
- package/dist/execution/index.d.ts.map +1 -1
- package/dist/execution/index.js +1 -0
- package/dist/execution/index.js.map +1 -1
- package/dist/execution/local.d.ts +13 -0
- package/dist/execution/local.d.ts.map +1 -1
- package/dist/execution/local.js +145 -19
- package/dist/execution/local.js.map +1 -1
- package/dist/execution/output.d.ts +27 -0
- package/dist/execution/output.d.ts.map +1 -0
- package/dist/execution/output.js +105 -0
- package/dist/execution/output.js.map +1 -0
- package/dist/execution/remote.d.ts +26 -2
- package/dist/execution/remote.d.ts.map +1 -1
- package/dist/execution/remote.js +86 -15
- package/dist/execution/remote.js.map +1 -1
- package/dist/plugin/lifecycle.d.ts.map +1 -1
- package/dist/plugin/lifecycle.js +9 -3
- package/dist/plugin/lifecycle.js.map +1 -1
- package/dist/process/kill-tree.d.ts +11 -13
- package/dist/process/kill-tree.d.ts.map +1 -1
- package/dist/process/kill-tree.js +11 -13
- package/dist/process/kill-tree.js.map +1 -1
- package/dist/provider/fallback.d.ts.map +1 -1
- package/dist/provider/fallback.js +31 -0
- package/dist/provider/fallback.js.map +1 -1
- package/dist/provider/idle-timeout.d.ts.map +1 -1
- package/dist/provider/idle-timeout.js +5 -0
- package/dist/provider/idle-timeout.js.map +1 -1
- package/dist/provider/retry.d.ts.map +1 -1
- package/dist/provider/retry.js +5 -0
- package/dist/provider/retry.js.map +1 -1
- package/dist/public-runtime.d.ts +2 -2
- package/dist/public-runtime.d.ts.map +1 -1
- package/dist/public-runtime.js +2 -2
- package/dist/public-runtime.js.map +1 -1
- package/dist/registry/tool/execute.d.ts +7 -1
- package/dist/registry/tool/execute.d.ts.map +1 -1
- package/dist/registry/tool/execute.js +201 -54
- package/dist/registry/tool/execute.js.map +1 -1
- package/dist/run/command-gate.d.ts.map +1 -1
- package/dist/run/command-gate.js +11 -0
- package/dist/run/command-gate.js.map +1 -1
- package/dist/run/workspace-fingerprint.d.ts +4 -4
- package/dist/run/workspace-fingerprint.d.ts.map +1 -1
- package/dist/run/workspace-fingerprint.js +6 -4
- package/dist/run/workspace-fingerprint.js.map +1 -1
- package/dist/runtime/bidi/index.d.ts +1 -1
- package/dist/runtime/bidi/index.d.ts.map +1 -1
- package/dist/runtime/bidi/index.js +1 -1
- package/dist/runtime/bidi/index.js.map +1 -1
- package/dist/runtime/bidi/session.d.ts +19 -0
- package/dist/runtime/bidi/session.d.ts.map +1 -1
- package/dist/runtime/bidi/session.js +150 -26
- package/dist/runtime/bidi/session.js.map +1 -1
- package/dist/runtime/query/executor.d.ts +43 -1
- package/dist/runtime/query/executor.d.ts.map +1 -1
- package/dist/runtime/query/executor.js +626 -161
- package/dist/runtime/query/executor.js.map +1 -1
- package/dist/runtime/query/index.d.ts.map +1 -1
- package/dist/runtime/query/index.js +46 -4
- package/dist/runtime/query/index.js.map +1 -1
- package/dist/runtime/query/iteration/phases/tool-review.d.ts.map +1 -1
- package/dist/runtime/query/iteration/phases/tool-review.js +84 -22
- package/dist/runtime/query/iteration/phases/tool-review.js.map +1 -1
- package/dist/runtime/query/request-rich-content.d.ts.map +1 -1
- package/dist/runtime/query/request-rich-content.js +33 -13
- package/dist/runtime/query/request-rich-content.js.map +1 -1
- package/dist/runtime/query/resume-pending.d.ts +5 -1
- package/dist/runtime/query/resume-pending.d.ts.map +1 -1
- package/dist/runtime/query/resume-pending.js +66 -1
- package/dist/runtime/query/resume-pending.js.map +1 -1
- package/dist/runtime/query/tool-output-budget.d.ts.map +1 -1
- package/dist/runtime/query/tool-output-budget.js +45 -16
- package/dist/runtime/query/tool-output-budget.js.map +1 -1
- package/dist/runtime/query/tooling.d.ts +6 -0
- package/dist/runtime/query/tooling.d.ts.map +1 -1
- package/dist/runtime/query/tooling.js +4 -0
- package/dist/runtime/query/tooling.js.map +1 -1
- package/dist/sandbox/provider/local.d.ts.map +1 -1
- package/dist/sandbox/provider/local.js +193 -41
- package/dist/sandbox/provider/local.js.map +1 -1
- package/dist/scheduler/delegating.d.ts +2 -1
- package/dist/scheduler/delegating.d.ts.map +1 -1
- package/dist/scheduler/delegating.js +6 -5
- package/dist/scheduler/delegating.js.map +1 -1
- package/dist/scheduler/local.d.ts +2 -1
- package/dist/scheduler/local.d.ts.map +1 -1
- package/dist/scheduler/local.js +2 -2
- package/dist/scheduler/local.js.map +1 -1
- package/dist/session/workspace/driver.d.ts +12 -4
- package/dist/session/workspace/driver.d.ts.map +1 -1
- package/dist/session/workspace/git-worktree.d.ts +17 -3
- package/dist/session/workspace/git-worktree.d.ts.map +1 -1
- package/dist/session/workspace/git-worktree.js +153 -34
- package/dist/session/workspace/git-worktree.js.map +1 -1
- package/dist/skills/registry.d.ts +15 -0
- package/dist/skills/registry.d.ts.map +1 -1
- package/dist/skills/registry.js +33 -0
- package/dist/skills/registry.js.map +1 -1
- package/dist/tools/builtins/computer-use.d.ts.map +1 -1
- package/dist/tools/builtins/computer-use.js +39 -2
- package/dist/tools/builtins/computer-use.js.map +1 -1
- package/dist/tools/builtins/run-code.d.ts.map +1 -1
- package/dist/tools/builtins/run-code.js +9 -11
- package/dist/tools/builtins/run-code.js.map +1 -1
- package/dist/tools/builtins/skill.d.ts +2 -1
- package/dist/tools/builtins/skill.d.ts.map +1 -1
- package/dist/tools/builtins/skill.js +250 -6
- package/dist/tools/builtins/skill.js.map +1 -1
- package/dist/tools/coordinator/agent.d.ts.map +1 -1
- package/dist/tools/coordinator/agent.js +100 -30
- package/dist/tools/coordinator/agent.js.map +1 -1
- package/dist/types/agent/manager.d.ts +1 -1
- package/dist/types/agent/manager.d.ts.map +1 -1
- package/dist/types/agent/scheduler.d.ts +10 -1
- package/dist/types/agent/scheduler.d.ts.map +1 -1
- package/dist/types/authorization/index.d.ts +12 -12
- package/dist/types/bidi/index.d.ts +19 -2
- package/dist/types/bidi/index.d.ts.map +1 -1
- package/dist/types/computer-use/index.d.ts +18 -0
- package/dist/types/computer-use/index.d.ts.map +1 -1
- package/dist/types/connector/execution-context.d.ts +3 -1
- package/dist/types/connector/execution-context.d.ts.map +1 -1
- package/dist/types/execution/index.d.ts +43 -1
- package/dist/types/execution/index.d.ts.map +1 -1
- package/dist/types/hitl/index.d.ts +11 -0
- package/dist/types/hitl/index.d.ts.map +1 -1
- package/dist/types/hitl/index.js.map +1 -1
- package/dist/types/message/index.d.ts +1 -1
- package/dist/types/message/index.d.ts.map +1 -1
- package/dist/types/message/index.js +2 -1
- package/dist/types/message/index.js.map +1 -1
- package/dist/types/plugin/index.d.ts +14 -1
- package/dist/types/plugin/index.d.ts.map +1 -1
- package/dist/types/plugin/index.js +1 -0
- package/dist/types/plugin/index.js.map +1 -1
- package/dist/types/provider/interface.d.ts +14 -0
- package/dist/types/provider/interface.d.ts.map +1 -1
- package/dist/types/sandbox/index.d.ts +15 -10
- package/dist/types/sandbox/index.d.ts.map +1 -1
- package/dist/types/sandbox/index.js.map +1 -1
- package/dist/types/tool/index.d.ts +77 -6
- package/dist/types/tool/index.d.ts.map +1 -1
- package/dist/types/tool/index.js.map +1 -1
- package/dist/types/workspace/ref.d.ts +5 -0
- package/dist/types/workspace/ref.d.ts.map +1 -1
- package/dist/utils/abort.d.ts +8 -0
- package/dist/utils/abort.d.ts.map +1 -1
- package/dist/utils/abort.js +16 -0
- package/dist/utils/abort.js.map +1 -1
- package/package.json +4 -4
- package/src/authorization/gate.ts +1 -1
- package/src/connector/index.ts +2 -0
- package/src/connector/mcp/adapter.ts +82 -21
- package/src/connector/mcp/client.ts +2 -1
- package/src/connector/mcp/http-redirect.ts +32 -0
- package/src/connector/mcp/http-sse.ts +5 -0
- package/src/connector/mcp/image-admission.ts +272 -0
- package/src/connector/mcp/stdio.ts +63 -21
- package/src/connector/mcp/streamable-http.ts +3 -0
- package/src/constants/tools/index.ts +1 -1
- package/src/execution/base.ts +124 -7
- package/src/execution/errors.ts +24 -0
- package/src/execution/factory.ts +1 -0
- package/src/execution/hybrid.ts +47 -16
- package/src/execution/index.ts +1 -0
- package/src/execution/local.ts +164 -19
- package/src/execution/output.ts +104 -0
- package/src/execution/remote.ts +122 -17
- package/src/plugin/lifecycle.ts +9 -3
- package/src/process/kill-tree.ts +11 -13
- package/src/provider/fallback.ts +39 -0
- package/src/provider/idle-timeout.ts +8 -0
- package/src/provider/retry.ts +8 -0
- package/src/public-runtime.ts +7 -1
- package/src/registry/tool/execute.ts +230 -64
- package/src/run/command-gate.ts +12 -0
- package/src/run/workspace-fingerprint.ts +5 -4
- package/src/runtime/bidi/index.ts +1 -1
- package/src/runtime/bidi/session.ts +177 -28
- package/src/runtime/query/executor.ts +769 -163
- package/src/runtime/query/index.ts +53 -6
- package/src/runtime/query/iteration/phases/tool-review.ts +87 -23
- package/src/runtime/query/request-rich-content.ts +39 -14
- package/src/runtime/query/resume-pending.ts +83 -1
- package/src/runtime/query/tool-output-budget.ts +52 -17
- package/src/runtime/query/tooling.ts +10 -0
- package/src/sandbox/provider/local.ts +213 -45
- package/src/scheduler/delegating.ts +6 -5
- package/src/scheduler/local.ts +3 -2
- package/src/session/workspace/driver.ts +12 -4
- package/src/session/workspace/git-worktree.ts +173 -34
- package/src/skills/registry.ts +47 -0
- package/src/tools/builtins/computer-use.ts +45 -2
- package/src/tools/builtins/run-code.ts +9 -11
- package/src/tools/builtins/skill.ts +325 -7
- package/src/tools/coordinator/agent.ts +108 -33
- package/src/types/agent/manager.ts +1 -1
- package/src/types/agent/scheduler.ts +10 -1
- package/src/types/bidi/index.ts +19 -2
- package/src/types/computer-use/index.ts +19 -0
- package/src/types/connector/execution-context.ts +3 -0
- package/src/types/execution/index.ts +46 -1
- package/src/types/hitl/index.ts +11 -0
- package/src/types/message/index.ts +3 -2
- package/src/types/plugin/index.ts +15 -0
- package/src/types/provider/interface.ts +18 -0
- package/src/types/sandbox/index.ts +15 -10
- package/src/types/tool/index.ts +85 -6
- package/src/types/workspace/ref.ts +5 -0
- package/src/utils/abort.ts +18 -0
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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.
|
|
152
|
+
this.log.info('MCP server process exited', { 'namzu.mcp.exit_code': code })
|
|
153
|
+
this.retire(generation, 'close')
|
|
138
154
|
})
|
|
139
155
|
|
|
140
|
-
|
|
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)
|
|
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('
|
|
204
|
+
child.once('close', finish)
|
|
189
205
|
child.once('error', finish)
|
|
190
|
-
child.
|
|
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
|
-
|
|
197
|
-
//
|
|
198
|
-
//
|
|
199
|
-
// `
|
|
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
|
|
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,
|
package/src/execution/base.ts
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
59
|
-
this.
|
|
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
|
-
|
|
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
|
+
}
|