@namzu/sdk 0.1.4-rc.3 → 0.1.4
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 +12 -1
- package/README.md +66 -3
- package/dist/agents/ReactiveAgent.js +12 -12
- package/dist/agents/ReactiveAgent.js.map +1 -1
- package/dist/agents/SupervisorAgent.js +11 -11
- package/dist/agents/SupervisorAgent.js.map +1 -1
- package/dist/bridge/a2a/mapper.d.ts +1 -0
- package/dist/bridge/a2a/mapper.d.ts.map +1 -1
- package/dist/bridge/a2a/mapper.js +5 -1
- package/dist/bridge/a2a/mapper.js.map +1 -1
- package/dist/bridge/sse/mapper.d.ts +1 -0
- package/dist/bridge/sse/mapper.d.ts.map +1 -1
- package/dist/bridge/sse/mapper.js +23 -0
- package/dist/bridge/sse/mapper.js.map +1 -1
- package/dist/config/runtime.d.ts +40 -0
- package/dist/config/runtime.d.ts.map +1 -1
- package/dist/config/runtime.js +3 -0
- package/dist/config/runtime.js.map +1 -1
- package/dist/constants/index.d.ts +1 -0
- package/dist/constants/index.d.ts.map +1 -1
- package/dist/constants/index.js +1 -0
- package/dist/constants/index.js.map +1 -1
- package/dist/constants/sandbox/index.d.ts +18 -0
- package/dist/constants/sandbox/index.d.ts.map +1 -0
- package/dist/constants/sandbox/index.js +26 -0
- package/dist/constants/sandbox/index.js.map +1 -0
- package/dist/constants/telemetry/index.d.ts +2 -2
- package/dist/constants/telemetry/index.js +2 -2
- package/dist/constants/telemetry/index.js.map +1 -1
- package/dist/contracts/api.d.ts +1 -1
- package/dist/contracts/api.d.ts.map +1 -1
- package/dist/index.d.ts +7 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -2
- package/dist/index.js.map +1 -1
- package/dist/manager/run/emergency.d.ts +2 -2
- package/dist/manager/run/emergency.d.ts.map +1 -1
- package/dist/manager/run/emergency.js +7 -7
- package/dist/manager/run/emergency.js.map +1 -1
- package/dist/manager/run/persistence.js +1 -1
- package/dist/manager/run/persistence.js.map +1 -1
- package/dist/run/reporter.d.ts.map +1 -1
- package/dist/run/reporter.js +22 -0
- package/dist/run/reporter.js.map +1 -1
- package/dist/runtime/query/checkpoint.d.ts +2 -2
- package/dist/runtime/query/checkpoint.d.ts.map +1 -1
- package/dist/runtime/query/checkpoint.js +12 -12
- package/dist/runtime/query/checkpoint.js.map +1 -1
- package/dist/runtime/query/context.d.ts +2 -2
- package/dist/runtime/query/context.d.ts.map +1 -1
- package/dist/runtime/query/context.js +4 -4
- package/dist/runtime/query/context.js.map +1 -1
- package/dist/runtime/query/events.d.ts +2 -2
- package/dist/runtime/query/events.d.ts.map +1 -1
- package/dist/runtime/query/events.js +4 -4
- package/dist/runtime/query/events.js.map +1 -1
- package/dist/runtime/query/executor.d.ts +3 -0
- package/dist/runtime/query/executor.d.ts.map +1 -1
- package/dist/runtime/query/executor.js +5 -1
- package/dist/runtime/query/executor.js.map +1 -1
- package/dist/runtime/query/guard.d.ts +1 -1
- package/dist/runtime/query/guard.d.ts.map +1 -1
- package/dist/runtime/query/guard.js +4 -4
- package/dist/runtime/query/guard.js.map +1 -1
- package/dist/runtime/query/index.d.ts +3 -1
- package/dist/runtime/query/index.d.ts.map +1 -1
- package/dist/runtime/query/index.js +68 -27
- package/dist/runtime/query/index.js.map +1 -1
- package/dist/runtime/query/iteration/index.d.ts +2 -2
- package/dist/runtime/query/iteration/index.d.ts.map +1 -1
- package/dist/runtime/query/iteration/index.js +51 -51
- package/dist/runtime/query/iteration/index.js.map +1 -1
- package/dist/runtime/query/iteration/phases/advisory.js +14 -14
- package/dist/runtime/query/iteration/phases/advisory.js.map +1 -1
- package/dist/runtime/query/iteration/phases/checkpoint.js +4 -4
- package/dist/runtime/query/iteration/phases/checkpoint.js.map +1 -1
- package/dist/runtime/query/iteration/phases/compaction.js +5 -5
- package/dist/runtime/query/iteration/phases/compaction.js.map +1 -1
- package/dist/runtime/query/iteration/phases/context.d.ts +2 -2
- package/dist/runtime/query/iteration/phases/context.d.ts.map +1 -1
- package/dist/runtime/query/iteration/phases/context.js +11 -11
- package/dist/runtime/query/iteration/phases/context.js.map +1 -1
- package/dist/runtime/query/iteration/phases/plan.js +3 -3
- package/dist/runtime/query/iteration/phases/plan.js.map +1 -1
- package/dist/runtime/query/iteration/phases/tool-review.js +19 -19
- package/dist/runtime/query/iteration/phases/tool-review.js.map +1 -1
- package/dist/runtime/query/prompt.d.ts +1 -1
- package/dist/runtime/query/prompt.d.ts.map +1 -1
- package/dist/runtime/query/result.d.ts +1 -1
- package/dist/runtime/query/result.d.ts.map +1 -1
- package/dist/runtime/query/result.js +20 -20
- package/dist/runtime/query/result.js.map +1 -1
- package/dist/sandbox/factory.d.ts +6 -0
- package/dist/sandbox/factory.d.ts.map +1 -0
- package/dist/sandbox/factory.js +14 -0
- package/dist/sandbox/factory.js.map +1 -0
- package/dist/sandbox/index.d.ts +3 -0
- package/dist/sandbox/index.d.ts.map +1 -0
- package/dist/sandbox/index.js +3 -0
- package/dist/sandbox/index.js.map +1 -0
- package/dist/sandbox/provider/local.d.ts +11 -0
- package/dist/sandbox/provider/local.d.ts.map +1 -0
- package/dist/sandbox/provider/local.js +366 -0
- package/dist/sandbox/provider/local.js.map +1 -0
- package/dist/telemetry/attributes.d.ts +1 -1
- package/dist/telemetry/attributes.d.ts.map +1 -1
- package/dist/telemetry/attributes.js +2 -2
- package/dist/telemetry/attributes.js.map +1 -1
- package/dist/telemetry/metrics.d.ts +1 -1
- package/dist/telemetry/metrics.d.ts.map +1 -1
- package/dist/telemetry/metrics.js +5 -5
- package/dist/telemetry/metrics.js.map +1 -1
- package/dist/tools/builtins/bash.d.ts.map +1 -1
- package/dist/tools/builtins/bash.js +27 -0
- package/dist/tools/builtins/bash.js.map +1 -1
- package/dist/tools/builtins/edit.d.ts +7 -0
- package/dist/tools/builtins/edit.d.ts.map +1 -0
- package/dist/tools/builtins/edit.js +97 -0
- package/dist/tools/builtins/edit.js.map +1 -0
- package/dist/tools/builtins/grep.d.ts +9 -0
- package/dist/tools/builtins/grep.d.ts.map +1 -0
- package/dist/tools/builtins/grep.js +138 -0
- package/dist/tools/builtins/grep.js.map +1 -0
- package/dist/tools/builtins/index.d.ts +3 -0
- package/dist/tools/builtins/index.d.ts.map +1 -1
- package/dist/tools/builtins/index.js +16 -1
- package/dist/tools/builtins/index.js.map +1 -1
- package/dist/tools/builtins/ls.d.ts +7 -0
- package/dist/tools/builtins/ls.d.ts.map +1 -0
- package/dist/tools/builtins/ls.js +114 -0
- package/dist/tools/builtins/ls.js.map +1 -0
- package/dist/tools/builtins/read-file.d.ts.map +1 -1
- package/dist/tools/builtins/read-file.js +20 -0
- package/dist/tools/builtins/read-file.js.map +1 -1
- package/dist/tools/builtins/write-file.d.ts.map +1 -1
- package/dist/tools/builtins/write-file.js +9 -0
- package/dist/tools/builtins/write-file.js.map +1 -1
- package/dist/types/ids/index.d.ts +1 -0
- package/dist/types/ids/index.d.ts.map +1 -1
- package/dist/types/run/config.d.ts +9 -1
- package/dist/types/run/config.d.ts.map +1 -1
- package/dist/types/run/events.d.ts +17 -1
- package/dist/types/run/events.d.ts.map +1 -1
- package/dist/types/sandbox/index.d.ts +66 -0
- package/dist/types/sandbox/index.d.ts.map +1 -0
- package/dist/types/sandbox/index.js +39 -0
- package/dist/types/sandbox/index.js.map +1 -0
- package/dist/types/tool/index.d.ts +3 -1
- package/dist/types/tool/index.d.ts.map +1 -1
- package/dist/utils/id.d.ts +3 -1
- package/dist/utils/id.d.ts.map +1 -1
- package/dist/utils/id.js +6 -0
- package/dist/utils/id.js.map +1 -1
- package/package.json +2 -2
- package/src/agents/ReactiveAgent.ts +12 -12
- package/src/agents/SupervisorAgent.ts +11 -11
- package/src/bridge/a2a/mapper.ts +6 -1
- package/src/bridge/sse/mapper.ts +26 -0
- package/src/config/runtime.ts +4 -0
- package/src/constants/index.ts +1 -0
- package/src/constants/sandbox/index.ts +31 -0
- package/src/constants/telemetry/index.ts +2 -2
- package/src/contracts/api.ts +3 -0
- package/src/index.ts +24 -4
- package/src/manager/run/emergency.ts +7 -7
- package/src/manager/run/persistence.ts +1 -1
- package/src/run/reporter.ts +25 -0
- package/src/runtime/query/checkpoint.ts +12 -12
- package/src/runtime/query/context.ts +6 -6
- package/src/runtime/query/events.ts +4 -4
- package/src/runtime/query/executor.ts +8 -1
- package/src/runtime/query/guard.ts +4 -4
- package/src/runtime/query/index.ts +76 -28
- package/src/runtime/query/iteration/index.ts +52 -55
- package/src/runtime/query/iteration/phases/advisory.ts +14 -14
- package/src/runtime/query/iteration/phases/checkpoint.ts +4 -4
- package/src/runtime/query/iteration/phases/compaction.ts +5 -5
- package/src/runtime/query/iteration/phases/context.ts +13 -13
- package/src/runtime/query/iteration/phases/plan.ts +3 -3
- package/src/runtime/query/iteration/phases/tool-review.ts +19 -19
- package/src/runtime/query/prompt.ts +1 -1
- package/src/runtime/query/result.ts +21 -21
- package/src/sandbox/factory.ts +16 -0
- package/src/sandbox/index.ts +2 -0
- package/src/sandbox/provider/local.ts +478 -0
- package/src/telemetry/attributes.ts +2 -2
- package/src/telemetry/metrics.ts +6 -6
- package/src/tools/builtins/bash.ts +31 -0
- package/src/tools/builtins/edit.ts +118 -0
- package/src/tools/builtins/grep.ts +151 -0
- package/src/tools/builtins/index.ts +16 -1
- package/src/tools/builtins/ls.ts +156 -0
- package/src/tools/builtins/read-file.ts +24 -0
- package/src/tools/builtins/write-file.ts +10 -0
- package/src/types/ids/index.ts +1 -0
- package/src/types/run/config.ts +9 -1
- package/src/types/run/events.ts +16 -1
- package/src/types/sandbox/index.ts +122 -0
- package/src/types/tool/index.ts +3 -1
- package/src/utils/id.ts +8 -0
|
@@ -0,0 +1,478 @@
|
|
|
1
|
+
import { execSync, spawn } from 'node:child_process'
|
|
2
|
+
import { realpathSync } from 'node:fs'
|
|
3
|
+
import {
|
|
4
|
+
readFile as fsReadFile,
|
|
5
|
+
writeFile as fsWriteFile,
|
|
6
|
+
mkdir,
|
|
7
|
+
rename,
|
|
8
|
+
rm,
|
|
9
|
+
} from 'node:fs/promises'
|
|
10
|
+
import { tmpdir } from 'node:os'
|
|
11
|
+
import { dirname, isAbsolute, join, relative, resolve } from 'node:path'
|
|
12
|
+
|
|
13
|
+
import {
|
|
14
|
+
SANDBOX_DEFAULT_TIMEOUT_MS,
|
|
15
|
+
SANDBOX_KILL_GRACE_MS,
|
|
16
|
+
SANDBOX_MAX_OUTPUT_BYTES,
|
|
17
|
+
SANDBOX_SAFE_ENV_KEYS,
|
|
18
|
+
SANDBOX_TEMP_DIR_PREFIX,
|
|
19
|
+
} from '../../constants/sandbox/index.js'
|
|
20
|
+
import type { SandboxId } from '../../types/ids/index.js'
|
|
21
|
+
import type {
|
|
22
|
+
Sandbox,
|
|
23
|
+
SandboxCreateConfig,
|
|
24
|
+
SandboxEnvironment,
|
|
25
|
+
SandboxExecOptions,
|
|
26
|
+
SandboxExecResult,
|
|
27
|
+
SandboxProvider,
|
|
28
|
+
SandboxStatus,
|
|
29
|
+
} from '../../types/sandbox/index.js'
|
|
30
|
+
import { generateSandboxId } from '../../utils/id.js'
|
|
31
|
+
import type { Logger } from '../../utils/logger.js'
|
|
32
|
+
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
// Path safety
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
|
|
37
|
+
function assertInsideSandbox(sandboxRoot: string, targetPath: string): string {
|
|
38
|
+
const resolved = resolve(sandboxRoot, targetPath)
|
|
39
|
+
const rel = relative(sandboxRoot, resolved)
|
|
40
|
+
if (rel.startsWith('..') || isAbsolute(rel)) {
|
|
41
|
+
throw new Error(`Path escapes sandbox: ${targetPath}`)
|
|
42
|
+
}
|
|
43
|
+
return resolved
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
// Platform detection
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
|
|
50
|
+
function detectEnvironment(): SandboxEnvironment {
|
|
51
|
+
const { platform } = process
|
|
52
|
+
|
|
53
|
+
if (platform === 'linux') {
|
|
54
|
+
try {
|
|
55
|
+
execSync('unshare --version', { stdio: 'ignore' })
|
|
56
|
+
return 'linux-namespace'
|
|
57
|
+
} catch {
|
|
58
|
+
// unshare not available
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (platform === 'darwin') {
|
|
63
|
+
try {
|
|
64
|
+
execSync('sandbox-exec -n no-network /usr/bin/true', { stdio: 'ignore' })
|
|
65
|
+
return 'macos-seatbelt'
|
|
66
|
+
} catch {
|
|
67
|
+
// sandbox-exec not available
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return 'basic'
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ---------------------------------------------------------------------------
|
|
75
|
+
// Seatbelt profile
|
|
76
|
+
// ---------------------------------------------------------------------------
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Resolve a path to its canonical form so seatbelt matches correctly.
|
|
80
|
+
* macOS symlinks like /var → /private/var must be resolved before use
|
|
81
|
+
* in SBPL rules, because the kernel evaluates real paths.
|
|
82
|
+
*
|
|
83
|
+
* Reference: Anthropic sandbox-runtime normalizePathForSandbox()
|
|
84
|
+
*/
|
|
85
|
+
function canonicalizePath(p: string): string {
|
|
86
|
+
try {
|
|
87
|
+
return realpathSync(p)
|
|
88
|
+
} catch {
|
|
89
|
+
// Path may not exist yet — resolve manually for known macOS symlinks
|
|
90
|
+
if (p.startsWith('/var/')) return `/private${p}`
|
|
91
|
+
if (p.startsWith('/tmp/')) return `/private${p}`
|
|
92
|
+
return p
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Build a macOS seatbelt (SBPL) profile for sandbox isolation.
|
|
98
|
+
*
|
|
99
|
+
* Reference: Anthropic sandbox-runtime generateSandboxProfile()
|
|
100
|
+
* Key principle: (deny default) + explicit allows. Network always denied.
|
|
101
|
+
*/
|
|
102
|
+
function buildSeatbeltProfile(sandboxRoot: string): string {
|
|
103
|
+
const root = canonicalizePath(sandboxRoot)
|
|
104
|
+
|
|
105
|
+
return [
|
|
106
|
+
'(version 1)',
|
|
107
|
+
'(deny default)',
|
|
108
|
+
|
|
109
|
+
// --- Process lifecycle ---
|
|
110
|
+
'(allow process-exec)',
|
|
111
|
+
'(allow process-fork)',
|
|
112
|
+
'(allow process-info* (target same-sandbox))',
|
|
113
|
+
'(allow signal (target same-sandbox))',
|
|
114
|
+
|
|
115
|
+
// --- Sandbox workspace — full read/write ---
|
|
116
|
+
`(allow file-read* (subpath "${root}"))`,
|
|
117
|
+
`(allow file-write* (subpath "${root}"))`,
|
|
118
|
+
|
|
119
|
+
// --- Root path literal — dyld needs this for path resolution ---
|
|
120
|
+
'(allow file-read* (literal "/"))',
|
|
121
|
+
|
|
122
|
+
// --- System binaries and libraries (read-only) ---
|
|
123
|
+
'(allow file-read* (subpath "/usr/lib"))',
|
|
124
|
+
'(allow file-read* (subpath "/usr/bin"))',
|
|
125
|
+
'(allow file-read* (subpath "/bin"))',
|
|
126
|
+
'(allow file-read* (subpath "/sbin"))',
|
|
127
|
+
'(allow file-read* (subpath "/usr/sbin"))',
|
|
128
|
+
'(allow file-read* (subpath "/usr/share"))',
|
|
129
|
+
'(allow file-read* (subpath "/usr/local"))',
|
|
130
|
+
|
|
131
|
+
// --- macOS system frameworks and dyld shared cache ---
|
|
132
|
+
'(allow file-read* (subpath "/System"))',
|
|
133
|
+
'(allow file-read* (subpath "/Library/Frameworks"))',
|
|
134
|
+
'(allow file-read* (subpath "/private/var/db/dyld"))',
|
|
135
|
+
'(allow file-read* (subpath "/private/var/select"))',
|
|
136
|
+
|
|
137
|
+
// --- Device files ---
|
|
138
|
+
'(allow file-read* (subpath "/dev"))',
|
|
139
|
+
'(allow file-write* (literal "/dev/null"))',
|
|
140
|
+
'(allow file-ioctl (literal "/dev/null"))',
|
|
141
|
+
'(allow file-ioctl (literal "/dev/zero"))',
|
|
142
|
+
'(allow file-ioctl (literal "/dev/random"))',
|
|
143
|
+
'(allow file-ioctl (literal "/dev/urandom"))',
|
|
144
|
+
'(allow file-ioctl (literal "/dev/tty"))',
|
|
145
|
+
|
|
146
|
+
// --- Temp directories (canonical paths) ---
|
|
147
|
+
'(allow file-read* (subpath "/private/tmp"))',
|
|
148
|
+
'(allow file-read* (subpath "/private/var/tmp"))',
|
|
149
|
+
'(allow file-write* (subpath "/private/tmp"))',
|
|
150
|
+
'(allow file-write* (subpath "/private/var/tmp"))',
|
|
151
|
+
|
|
152
|
+
// --- File metadata — needed for realpath() traversal ---
|
|
153
|
+
'(allow file-read-metadata)',
|
|
154
|
+
|
|
155
|
+
// --- System info ---
|
|
156
|
+
'(allow sysctl-read)',
|
|
157
|
+
'(allow user-preference-read)',
|
|
158
|
+
|
|
159
|
+
// --- Mach IPC — essential services only ---
|
|
160
|
+
'(allow mach-lookup',
|
|
161
|
+
' (global-name "com.apple.logd")',
|
|
162
|
+
' (global-name "com.apple.system.logger")',
|
|
163
|
+
' (global-name "com.apple.system.notification_center")',
|
|
164
|
+
' (global-name "com.apple.system.opendirectoryd.libinfo")',
|
|
165
|
+
' (global-name "com.apple.system.opendirectoryd.membership")',
|
|
166
|
+
' (global-name "com.apple.bsd.dirhelper")',
|
|
167
|
+
' (global-name "com.apple.SecurityServer")',
|
|
168
|
+
' (global-name "com.apple.securityd.xpc")',
|
|
169
|
+
' (global-name "com.apple.coreservices.launchservicesd")',
|
|
170
|
+
' (global-name "com.apple.fonts")',
|
|
171
|
+
' (global-name "com.apple.FontObjectsServer")',
|
|
172
|
+
' (global-name "com.apple.lsd.mapdb")',
|
|
173
|
+
')',
|
|
174
|
+
|
|
175
|
+
// --- POSIX IPC ---
|
|
176
|
+
'(allow ipc-posix-shm)',
|
|
177
|
+
'(allow ipc-posix-sem)',
|
|
178
|
+
|
|
179
|
+
// --- Network — deny all ---
|
|
180
|
+
'(deny network*)',
|
|
181
|
+
].join('\n')
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// ---------------------------------------------------------------------------
|
|
185
|
+
// Environment building
|
|
186
|
+
// ---------------------------------------------------------------------------
|
|
187
|
+
|
|
188
|
+
function buildSafeEnv(
|
|
189
|
+
configEnv?: Record<string, string>,
|
|
190
|
+
optsEnv?: Record<string, string>,
|
|
191
|
+
): Record<string, string> {
|
|
192
|
+
const env: Record<string, string> = {}
|
|
193
|
+
|
|
194
|
+
for (const key of SANDBOX_SAFE_ENV_KEYS) {
|
|
195
|
+
const value = process.env[key]
|
|
196
|
+
if (value !== undefined) {
|
|
197
|
+
env[key] = value
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (configEnv) {
|
|
202
|
+
Object.assign(env, configEnv)
|
|
203
|
+
}
|
|
204
|
+
if (optsEnv) {
|
|
205
|
+
Object.assign(env, optsEnv)
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return env
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// ---------------------------------------------------------------------------
|
|
212
|
+
// LocalSandbox
|
|
213
|
+
// ---------------------------------------------------------------------------
|
|
214
|
+
|
|
215
|
+
class LocalSandbox implements Sandbox {
|
|
216
|
+
readonly id: SandboxId
|
|
217
|
+
readonly rootDir: string
|
|
218
|
+
readonly environment: SandboxEnvironment
|
|
219
|
+
|
|
220
|
+
private _status: SandboxStatus
|
|
221
|
+
private readonly config: SandboxCreateConfig
|
|
222
|
+
private readonly log: Logger
|
|
223
|
+
|
|
224
|
+
get status(): SandboxStatus {
|
|
225
|
+
return this._status
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
constructor(
|
|
229
|
+
id: SandboxId,
|
|
230
|
+
rootDir: string,
|
|
231
|
+
environment: SandboxEnvironment,
|
|
232
|
+
config: SandboxCreateConfig,
|
|
233
|
+
log: Logger,
|
|
234
|
+
) {
|
|
235
|
+
this.id = id
|
|
236
|
+
this.rootDir = rootDir
|
|
237
|
+
this.environment = environment
|
|
238
|
+
this.config = config
|
|
239
|
+
this._status = 'ready'
|
|
240
|
+
this.log = log.child({ component: 'LocalSandbox', sandboxId: id })
|
|
241
|
+
|
|
242
|
+
this.log.info('Sandbox created', { rootDir, environment })
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
async exec(
|
|
246
|
+
command: string,
|
|
247
|
+
args: string[] = [],
|
|
248
|
+
opts?: SandboxExecOptions,
|
|
249
|
+
): Promise<SandboxExecResult> {
|
|
250
|
+
if (this._status === 'destroyed') {
|
|
251
|
+
throw new Error(`Sandbox ${this.id} is destroyed`)
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
this._status = 'busy'
|
|
255
|
+
const startTime = Date.now()
|
|
256
|
+
|
|
257
|
+
const env = buildSafeEnv(this.config.env, opts?.env)
|
|
258
|
+
const timeout = opts?.timeout ?? this.config.timeoutMs ?? SANDBOX_DEFAULT_TIMEOUT_MS
|
|
259
|
+
|
|
260
|
+
const cwd = opts?.cwd ? assertInsideSandbox(this.rootDir, opts.cwd) : this.rootDir
|
|
261
|
+
|
|
262
|
+
const { spawnCommand, spawnArgs } = this.buildSpawnArgs(command, args)
|
|
263
|
+
|
|
264
|
+
this.log.debug('Executing command', { command, args, timeout, environment: this.environment })
|
|
265
|
+
|
|
266
|
+
const ac = new AbortController()
|
|
267
|
+
const timeoutId = setTimeout(() => ac.abort(), timeout)
|
|
268
|
+
|
|
269
|
+
try {
|
|
270
|
+
const result = await this.spawnProcess(spawnCommand, spawnArgs, cwd, env, ac)
|
|
271
|
+
return { ...result, durationMs: Date.now() - startTime }
|
|
272
|
+
} finally {
|
|
273
|
+
clearTimeout(timeoutId)
|
|
274
|
+
if ((this._status as SandboxStatus) !== 'destroyed') {
|
|
275
|
+
this._status = 'ready'
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
async writeFile(path: string, content: string | Buffer): Promise<void> {
|
|
281
|
+
if (this._status === 'destroyed') {
|
|
282
|
+
throw new Error(`Sandbox ${this.id} is destroyed`)
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const resolved = assertInsideSandbox(this.rootDir, path)
|
|
286
|
+
await mkdir(dirname(resolved), { recursive: true })
|
|
287
|
+
|
|
288
|
+
// Convention 8: Atomic write (write-tmp-rename)
|
|
289
|
+
const tmpPath = `${resolved}.tmp.${Date.now()}`
|
|
290
|
+
await fsWriteFile(tmpPath, content)
|
|
291
|
+
await rename(tmpPath, resolved)
|
|
292
|
+
|
|
293
|
+
this.log.debug('File written', { path: resolved })
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
async readFile(path: string): Promise<Buffer> {
|
|
297
|
+
if (this._status === 'destroyed') {
|
|
298
|
+
throw new Error(`Sandbox ${this.id} is destroyed`)
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const resolved = assertInsideSandbox(this.rootDir, path)
|
|
302
|
+
return fsReadFile(resolved)
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
async destroy(): Promise<void> {
|
|
306
|
+
if (this._status === 'destroyed') {
|
|
307
|
+
return
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
this._status = 'destroyed'
|
|
311
|
+
await rm(this.rootDir, { recursive: true, force: true })
|
|
312
|
+
|
|
313
|
+
this.log.info('Sandbox destroyed', { sandboxId: this.id })
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// -----------------------------------------------------------------------
|
|
317
|
+
// Private helpers
|
|
318
|
+
// -----------------------------------------------------------------------
|
|
319
|
+
|
|
320
|
+
private buildSpawnArgs(
|
|
321
|
+
command: string,
|
|
322
|
+
args: string[],
|
|
323
|
+
): { spawnCommand: string; spawnArgs: string[] } {
|
|
324
|
+
switch (this.environment) {
|
|
325
|
+
case 'linux-namespace':
|
|
326
|
+
return {
|
|
327
|
+
spawnCommand: 'unshare',
|
|
328
|
+
spawnArgs: ['--mount', '--pid', '--fork', '--map-root-user', '--', command, ...args],
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
case 'macos-seatbelt': {
|
|
332
|
+
const profile = buildSeatbeltProfile(this.rootDir)
|
|
333
|
+
return {
|
|
334
|
+
spawnCommand: 'sandbox-exec',
|
|
335
|
+
spawnArgs: ['-p', profile, '--', command, ...args],
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
case 'basic': {
|
|
340
|
+
const limits: string[] = []
|
|
341
|
+
|
|
342
|
+
const memoryMb = this.config.memoryLimitMb
|
|
343
|
+
if (memoryMb !== undefined) {
|
|
344
|
+
const memoryKb = memoryMb * 1024
|
|
345
|
+
limits.push(`ulimit -v ${memoryKb}`)
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const maxProcs = this.config.maxProcesses
|
|
349
|
+
if (maxProcs !== undefined) {
|
|
350
|
+
limits.push(`ulimit -u ${maxProcs}`)
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
if (limits.length > 0) {
|
|
354
|
+
const prefix = limits.join(' && ')
|
|
355
|
+
const fullCommand = `${prefix} && ${command} ${args.map((a) => `'${a.replace(/'/g, "'\\''")}'`).join(' ')}`
|
|
356
|
+
return {
|
|
357
|
+
spawnCommand: '/bin/sh',
|
|
358
|
+
spawnArgs: ['-c', fullCommand],
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
return { spawnCommand: command, spawnArgs: args }
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
default: {
|
|
366
|
+
const _exhaustive: never = this.environment
|
|
367
|
+
throw new Error(`Unknown sandbox environment: ${_exhaustive}`)
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
private spawnProcess(
|
|
373
|
+
command: string,
|
|
374
|
+
args: string[],
|
|
375
|
+
cwd: string,
|
|
376
|
+
env: Record<string, string>,
|
|
377
|
+
ac: AbortController,
|
|
378
|
+
): Promise<Omit<SandboxExecResult, 'durationMs'>> {
|
|
379
|
+
return new Promise((resolvePromise, rejectPromise) => {
|
|
380
|
+
let child: ReturnType<typeof spawn>
|
|
381
|
+
try {
|
|
382
|
+
child = spawn(command, args, {
|
|
383
|
+
cwd,
|
|
384
|
+
env,
|
|
385
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
386
|
+
signal: ac.signal,
|
|
387
|
+
})
|
|
388
|
+
} catch (err) {
|
|
389
|
+
rejectPromise(err)
|
|
390
|
+
return
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
let stdout = ''
|
|
394
|
+
let stderr = ''
|
|
395
|
+
let stdoutBytes = 0
|
|
396
|
+
let stderrBytes = 0
|
|
397
|
+
let timedOut = false
|
|
398
|
+
|
|
399
|
+
child.stdout?.on('data', (chunk: Buffer) => {
|
|
400
|
+
if (stdoutBytes < SANDBOX_MAX_OUTPUT_BYTES) {
|
|
401
|
+
const remaining = SANDBOX_MAX_OUTPUT_BYTES - stdoutBytes
|
|
402
|
+
stdout += chunk.subarray(0, remaining).toString('utf-8')
|
|
403
|
+
}
|
|
404
|
+
stdoutBytes += chunk.length
|
|
405
|
+
})
|
|
406
|
+
|
|
407
|
+
child.stderr?.on('data', (chunk: Buffer) => {
|
|
408
|
+
if (stderrBytes < SANDBOX_MAX_OUTPUT_BYTES) {
|
|
409
|
+
const remaining = SANDBOX_MAX_OUTPUT_BYTES - stderrBytes
|
|
410
|
+
stderr += chunk.subarray(0, remaining).toString('utf-8')
|
|
411
|
+
}
|
|
412
|
+
stderrBytes += chunk.length
|
|
413
|
+
})
|
|
414
|
+
|
|
415
|
+
child.on('error', (err: NodeJS.ErrnoException) => {
|
|
416
|
+
if (err.code === 'ABORT_ERR' || ac.signal.aborted) {
|
|
417
|
+
timedOut = true
|
|
418
|
+
// Give process a grace period, then SIGKILL
|
|
419
|
+
if (child.pid) {
|
|
420
|
+
setTimeout(() => {
|
|
421
|
+
try {
|
|
422
|
+
child.kill('SIGKILL')
|
|
423
|
+
} catch {
|
|
424
|
+
// Process may have already exited
|
|
425
|
+
}
|
|
426
|
+
}, SANDBOX_KILL_GRACE_MS)
|
|
427
|
+
}
|
|
428
|
+
return
|
|
429
|
+
}
|
|
430
|
+
rejectPromise(err)
|
|
431
|
+
})
|
|
432
|
+
|
|
433
|
+
child.on('close', (code, signal) => {
|
|
434
|
+
resolvePromise({
|
|
435
|
+
exitCode: code ?? (timedOut ? 124 : 1),
|
|
436
|
+
stdout,
|
|
437
|
+
stderr,
|
|
438
|
+
signal: signal ?? undefined,
|
|
439
|
+
timedOut,
|
|
440
|
+
})
|
|
441
|
+
})
|
|
442
|
+
})
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// ---------------------------------------------------------------------------
|
|
447
|
+
// LocalSandboxProvider
|
|
448
|
+
// ---------------------------------------------------------------------------
|
|
449
|
+
|
|
450
|
+
export class LocalSandboxProvider implements SandboxProvider {
|
|
451
|
+
readonly id = 'local'
|
|
452
|
+
readonly name = 'Local Sandbox'
|
|
453
|
+
readonly environment: SandboxEnvironment
|
|
454
|
+
|
|
455
|
+
private readonly log: Logger
|
|
456
|
+
|
|
457
|
+
constructor(log: Logger) {
|
|
458
|
+
this.environment = detectEnvironment()
|
|
459
|
+
this.log = log.child({ component: 'LocalSandboxProvider' })
|
|
460
|
+
|
|
461
|
+
this.log.info('Initialized', { environment: this.environment })
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
async create(config?: SandboxCreateConfig): Promise<Sandbox> {
|
|
465
|
+
const id = generateSandboxId()
|
|
466
|
+
|
|
467
|
+
// mkdtemp is in node:fs/promises but requires an async import-style usage.
|
|
468
|
+
// We use the same pattern: create a unique dir under os.tmpdir().
|
|
469
|
+
const { mkdtemp } = await import('node:fs/promises')
|
|
470
|
+
const rawDir = await mkdtemp(join(tmpdir(), SANDBOX_TEMP_DIR_PREFIX))
|
|
471
|
+
// Canonicalize — macOS symlinks like /var → /private/var must be resolved
|
|
472
|
+
const rootDir = canonicalizePath(rawDir)
|
|
473
|
+
|
|
474
|
+
this.log.info('Creating sandbox', { sandboxId: id, rootDir })
|
|
475
|
+
|
|
476
|
+
return new LocalSandbox(id, rootDir, this.environment, config ?? {}, this.log)
|
|
477
|
+
}
|
|
478
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export { GENAI, NAMZU } from '../constants/telemetry/index.js'
|
|
2
2
|
|
|
3
|
-
export function
|
|
4
|
-
return `namzu.agent.
|
|
3
|
+
export function agentRunSpanName(agentName: string): string {
|
|
4
|
+
return `namzu.agent.run ${agentName}`
|
|
5
5
|
}
|
|
6
6
|
|
|
7
7
|
export function agentIterationSpanName(iteration: number): string {
|
package/src/telemetry/metrics.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { getMeter } from '../provider/telemetry/setup.js'
|
|
|
3
3
|
export interface PlatformMetrics {
|
|
4
4
|
recordTokenUsage(model: string, inputTokens: number, outputTokens: number): void
|
|
5
5
|
recordToolCall(toolName: string, success: boolean): void
|
|
6
|
-
|
|
6
|
+
recordRunDuration(status: string, durationSec: number): void
|
|
7
7
|
recordLLMLatency(model: string, durationSec: number): void
|
|
8
8
|
}
|
|
9
9
|
|
|
@@ -25,8 +25,8 @@ export function createPlatformMetrics(): PlatformMetrics {
|
|
|
25
25
|
unit: '{call}',
|
|
26
26
|
})
|
|
27
27
|
|
|
28
|
-
const
|
|
29
|
-
description: 'Agent
|
|
28
|
+
const runDurationHistogram = meter.createHistogram('namzu.run.duration', {
|
|
29
|
+
description: 'Agent run duration',
|
|
30
30
|
unit: 's',
|
|
31
31
|
})
|
|
32
32
|
|
|
@@ -54,9 +54,9 @@ export function createPlatformMetrics(): PlatformMetrics {
|
|
|
54
54
|
})
|
|
55
55
|
},
|
|
56
56
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
'namzu.
|
|
57
|
+
recordRunDuration(status: string, durationSec: number): void {
|
|
58
|
+
runDurationHistogram.record(durationSec, {
|
|
59
|
+
'namzu.run.status': status,
|
|
60
60
|
})
|
|
61
61
|
},
|
|
62
62
|
|
|
@@ -39,6 +39,37 @@ export const BashTool = defineTool({
|
|
|
39
39
|
}
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
// Sandbox-aware: route through sandbox.exec() when available
|
|
43
|
+
if (context.sandbox) {
|
|
44
|
+
const result = await context.sandbox.exec('/bin/sh', ['-c', input.command], {
|
|
45
|
+
timeout: input.timeout,
|
|
46
|
+
cwd: context.workingDirectory,
|
|
47
|
+
env: context.env,
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
if (result.timedOut) {
|
|
51
|
+
return {
|
|
52
|
+
success: false,
|
|
53
|
+
output: '',
|
|
54
|
+
error: `Command timed out after ${input.timeout}ms`,
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const output = [
|
|
59
|
+
result.stdout ? `STDOUT:\n${result.stdout}` : '',
|
|
60
|
+
result.stderr ? `STDERR:\n${result.stderr}` : '',
|
|
61
|
+
]
|
|
62
|
+
.filter(Boolean)
|
|
63
|
+
.join('\n\n')
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
success: result.exitCode === 0,
|
|
67
|
+
output: output || '(no output)',
|
|
68
|
+
data: { exitCode: result.exitCode, sandboxed: true },
|
|
69
|
+
error: result.exitCode !== 0 ? `Command exited with code ${result.exitCode}` : undefined,
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
42
73
|
const { stdout, stderr } = await execAsync(input.command, {
|
|
43
74
|
cwd: context.workingDirectory,
|
|
44
75
|
timeout: input.timeout,
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { readFile, writeFile } from 'node:fs/promises'
|
|
2
|
+
import { resolve } from 'node:path'
|
|
3
|
+
import { z } from 'zod'
|
|
4
|
+
import { defineTool } from '../defineTool.js'
|
|
5
|
+
|
|
6
|
+
const inputSchema = z.object({
|
|
7
|
+
path: z.string().describe('Path to the file to edit'),
|
|
8
|
+
old_string: z
|
|
9
|
+
.string()
|
|
10
|
+
.describe('The exact string to find and replace. Must be unique in the file.'),
|
|
11
|
+
new_string: z.string().describe('The replacement string'),
|
|
12
|
+
replace_all: z
|
|
13
|
+
.boolean()
|
|
14
|
+
.default(false)
|
|
15
|
+
.describe('Replace all occurrences instead of just the first unique match'),
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
type EditInput = z.infer<typeof inputSchema>
|
|
19
|
+
|
|
20
|
+
export const EditTool = defineTool({
|
|
21
|
+
name: 'edit',
|
|
22
|
+
description:
|
|
23
|
+
'Makes targeted edits to a file using exact string find-and-replace. The old_string must be unique in the file unless replace_all is true. Preserves file formatting and indentation.',
|
|
24
|
+
inputSchema,
|
|
25
|
+
category: 'filesystem',
|
|
26
|
+
permissions: ['file_write'],
|
|
27
|
+
readOnly: false,
|
|
28
|
+
destructive: false,
|
|
29
|
+
concurrencySafe: false,
|
|
30
|
+
|
|
31
|
+
async execute(input: EditInput, context) {
|
|
32
|
+
if (input.old_string === input.new_string) {
|
|
33
|
+
return {
|
|
34
|
+
success: false,
|
|
35
|
+
output: '',
|
|
36
|
+
error: 'old_string and new_string are identical — no change needed',
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Sandbox-aware: route through sandbox when available
|
|
41
|
+
if (context.sandbox) {
|
|
42
|
+
const buffer = await context.sandbox.readFile(input.path)
|
|
43
|
+
const content = buffer.toString('utf-8')
|
|
44
|
+
|
|
45
|
+
const result = applyEdit(content, input)
|
|
46
|
+
if (!result.success) {
|
|
47
|
+
return { success: false, output: '', error: result.error }
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
await context.sandbox.writeFile(input.path, result.content)
|
|
51
|
+
return {
|
|
52
|
+
success: true,
|
|
53
|
+
output: `Edited ${input.path}: ${result.replacements} replacement(s) [sandboxed]`,
|
|
54
|
+
data: { path: input.path, replacements: result.replacements, sandboxed: true },
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const filePath = resolve(context.workingDirectory, input.path)
|
|
59
|
+
const content = await readFile(filePath, 'utf-8')
|
|
60
|
+
|
|
61
|
+
const result = applyEdit(content, input)
|
|
62
|
+
if (!result.success) {
|
|
63
|
+
return { success: false, output: '', error: result.error }
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
await writeFile(filePath, result.content, 'utf-8')
|
|
67
|
+
return {
|
|
68
|
+
success: true,
|
|
69
|
+
output: `Edited ${filePath}: ${result.replacements} replacement(s)`,
|
|
70
|
+
data: { path: filePath, replacements: result.replacements },
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
function applyEdit(
|
|
76
|
+
content: string,
|
|
77
|
+
input: EditInput,
|
|
78
|
+
): { success: true; content: string; replacements: number } | { success: false; error: string } {
|
|
79
|
+
if (!content.includes(input.old_string)) {
|
|
80
|
+
return {
|
|
81
|
+
success: false,
|
|
82
|
+
error:
|
|
83
|
+
'old_string not found in file. Make sure the string matches exactly, including whitespace and indentation.',
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (input.replace_all) {
|
|
88
|
+
const parts = content.split(input.old_string)
|
|
89
|
+
const replacements = parts.length - 1
|
|
90
|
+
return {
|
|
91
|
+
success: true,
|
|
92
|
+
content: parts.join(input.new_string),
|
|
93
|
+
replacements,
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Uniqueness check: old_string must appear exactly once
|
|
98
|
+
const firstIndex = content.indexOf(input.old_string)
|
|
99
|
+
const secondIndex = content.indexOf(input.old_string, firstIndex + 1)
|
|
100
|
+
|
|
101
|
+
if (secondIndex !== -1) {
|
|
102
|
+
const lineNumber = content.slice(0, firstIndex).split('\n').length
|
|
103
|
+
const secondLine = content.slice(0, secondIndex).split('\n').length
|
|
104
|
+
return {
|
|
105
|
+
success: false,
|
|
106
|
+
error: `old_string is not unique — found at lines ${lineNumber} and ${secondLine}. Provide more surrounding context to make it unique, or use replace_all: true.`,
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
success: true,
|
|
112
|
+
content:
|
|
113
|
+
content.slice(0, firstIndex) +
|
|
114
|
+
input.new_string +
|
|
115
|
+
content.slice(firstIndex + input.old_string.length),
|
|
116
|
+
replacements: 1,
|
|
117
|
+
}
|
|
118
|
+
}
|