@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.
Files changed (200) hide show
  1. package/CHANGELOG.md +12 -1
  2. package/README.md +66 -3
  3. package/dist/agents/ReactiveAgent.js +12 -12
  4. package/dist/agents/ReactiveAgent.js.map +1 -1
  5. package/dist/agents/SupervisorAgent.js +11 -11
  6. package/dist/agents/SupervisorAgent.js.map +1 -1
  7. package/dist/bridge/a2a/mapper.d.ts +1 -0
  8. package/dist/bridge/a2a/mapper.d.ts.map +1 -1
  9. package/dist/bridge/a2a/mapper.js +5 -1
  10. package/dist/bridge/a2a/mapper.js.map +1 -1
  11. package/dist/bridge/sse/mapper.d.ts +1 -0
  12. package/dist/bridge/sse/mapper.d.ts.map +1 -1
  13. package/dist/bridge/sse/mapper.js +23 -0
  14. package/dist/bridge/sse/mapper.js.map +1 -1
  15. package/dist/config/runtime.d.ts +40 -0
  16. package/dist/config/runtime.d.ts.map +1 -1
  17. package/dist/config/runtime.js +3 -0
  18. package/dist/config/runtime.js.map +1 -1
  19. package/dist/constants/index.d.ts +1 -0
  20. package/dist/constants/index.d.ts.map +1 -1
  21. package/dist/constants/index.js +1 -0
  22. package/dist/constants/index.js.map +1 -1
  23. package/dist/constants/sandbox/index.d.ts +18 -0
  24. package/dist/constants/sandbox/index.d.ts.map +1 -0
  25. package/dist/constants/sandbox/index.js +26 -0
  26. package/dist/constants/sandbox/index.js.map +1 -0
  27. package/dist/constants/telemetry/index.d.ts +2 -2
  28. package/dist/constants/telemetry/index.js +2 -2
  29. package/dist/constants/telemetry/index.js.map +1 -1
  30. package/dist/contracts/api.d.ts +1 -1
  31. package/dist/contracts/api.d.ts.map +1 -1
  32. package/dist/index.d.ts +7 -5
  33. package/dist/index.d.ts.map +1 -1
  34. package/dist/index.js +4 -2
  35. package/dist/index.js.map +1 -1
  36. package/dist/manager/run/emergency.d.ts +2 -2
  37. package/dist/manager/run/emergency.d.ts.map +1 -1
  38. package/dist/manager/run/emergency.js +7 -7
  39. package/dist/manager/run/emergency.js.map +1 -1
  40. package/dist/manager/run/persistence.js +1 -1
  41. package/dist/manager/run/persistence.js.map +1 -1
  42. package/dist/run/reporter.d.ts.map +1 -1
  43. package/dist/run/reporter.js +22 -0
  44. package/dist/run/reporter.js.map +1 -1
  45. package/dist/runtime/query/checkpoint.d.ts +2 -2
  46. package/dist/runtime/query/checkpoint.d.ts.map +1 -1
  47. package/dist/runtime/query/checkpoint.js +12 -12
  48. package/dist/runtime/query/checkpoint.js.map +1 -1
  49. package/dist/runtime/query/context.d.ts +2 -2
  50. package/dist/runtime/query/context.d.ts.map +1 -1
  51. package/dist/runtime/query/context.js +4 -4
  52. package/dist/runtime/query/context.js.map +1 -1
  53. package/dist/runtime/query/events.d.ts +2 -2
  54. package/dist/runtime/query/events.d.ts.map +1 -1
  55. package/dist/runtime/query/events.js +4 -4
  56. package/dist/runtime/query/events.js.map +1 -1
  57. package/dist/runtime/query/executor.d.ts +3 -0
  58. package/dist/runtime/query/executor.d.ts.map +1 -1
  59. package/dist/runtime/query/executor.js +5 -1
  60. package/dist/runtime/query/executor.js.map +1 -1
  61. package/dist/runtime/query/guard.d.ts +1 -1
  62. package/dist/runtime/query/guard.d.ts.map +1 -1
  63. package/dist/runtime/query/guard.js +4 -4
  64. package/dist/runtime/query/guard.js.map +1 -1
  65. package/dist/runtime/query/index.d.ts +3 -1
  66. package/dist/runtime/query/index.d.ts.map +1 -1
  67. package/dist/runtime/query/index.js +68 -27
  68. package/dist/runtime/query/index.js.map +1 -1
  69. package/dist/runtime/query/iteration/index.d.ts +2 -2
  70. package/dist/runtime/query/iteration/index.d.ts.map +1 -1
  71. package/dist/runtime/query/iteration/index.js +51 -51
  72. package/dist/runtime/query/iteration/index.js.map +1 -1
  73. package/dist/runtime/query/iteration/phases/advisory.js +14 -14
  74. package/dist/runtime/query/iteration/phases/advisory.js.map +1 -1
  75. package/dist/runtime/query/iteration/phases/checkpoint.js +4 -4
  76. package/dist/runtime/query/iteration/phases/checkpoint.js.map +1 -1
  77. package/dist/runtime/query/iteration/phases/compaction.js +5 -5
  78. package/dist/runtime/query/iteration/phases/compaction.js.map +1 -1
  79. package/dist/runtime/query/iteration/phases/context.d.ts +2 -2
  80. package/dist/runtime/query/iteration/phases/context.d.ts.map +1 -1
  81. package/dist/runtime/query/iteration/phases/context.js +11 -11
  82. package/dist/runtime/query/iteration/phases/context.js.map +1 -1
  83. package/dist/runtime/query/iteration/phases/plan.js +3 -3
  84. package/dist/runtime/query/iteration/phases/plan.js.map +1 -1
  85. package/dist/runtime/query/iteration/phases/tool-review.js +19 -19
  86. package/dist/runtime/query/iteration/phases/tool-review.js.map +1 -1
  87. package/dist/runtime/query/prompt.d.ts +1 -1
  88. package/dist/runtime/query/prompt.d.ts.map +1 -1
  89. package/dist/runtime/query/result.d.ts +1 -1
  90. package/dist/runtime/query/result.d.ts.map +1 -1
  91. package/dist/runtime/query/result.js +20 -20
  92. package/dist/runtime/query/result.js.map +1 -1
  93. package/dist/sandbox/factory.d.ts +6 -0
  94. package/dist/sandbox/factory.d.ts.map +1 -0
  95. package/dist/sandbox/factory.js +14 -0
  96. package/dist/sandbox/factory.js.map +1 -0
  97. package/dist/sandbox/index.d.ts +3 -0
  98. package/dist/sandbox/index.d.ts.map +1 -0
  99. package/dist/sandbox/index.js +3 -0
  100. package/dist/sandbox/index.js.map +1 -0
  101. package/dist/sandbox/provider/local.d.ts +11 -0
  102. package/dist/sandbox/provider/local.d.ts.map +1 -0
  103. package/dist/sandbox/provider/local.js +366 -0
  104. package/dist/sandbox/provider/local.js.map +1 -0
  105. package/dist/telemetry/attributes.d.ts +1 -1
  106. package/dist/telemetry/attributes.d.ts.map +1 -1
  107. package/dist/telemetry/attributes.js +2 -2
  108. package/dist/telemetry/attributes.js.map +1 -1
  109. package/dist/telemetry/metrics.d.ts +1 -1
  110. package/dist/telemetry/metrics.d.ts.map +1 -1
  111. package/dist/telemetry/metrics.js +5 -5
  112. package/dist/telemetry/metrics.js.map +1 -1
  113. package/dist/tools/builtins/bash.d.ts.map +1 -1
  114. package/dist/tools/builtins/bash.js +27 -0
  115. package/dist/tools/builtins/bash.js.map +1 -1
  116. package/dist/tools/builtins/edit.d.ts +7 -0
  117. package/dist/tools/builtins/edit.d.ts.map +1 -0
  118. package/dist/tools/builtins/edit.js +97 -0
  119. package/dist/tools/builtins/edit.js.map +1 -0
  120. package/dist/tools/builtins/grep.d.ts +9 -0
  121. package/dist/tools/builtins/grep.d.ts.map +1 -0
  122. package/dist/tools/builtins/grep.js +138 -0
  123. package/dist/tools/builtins/grep.js.map +1 -0
  124. package/dist/tools/builtins/index.d.ts +3 -0
  125. package/dist/tools/builtins/index.d.ts.map +1 -1
  126. package/dist/tools/builtins/index.js +16 -1
  127. package/dist/tools/builtins/index.js.map +1 -1
  128. package/dist/tools/builtins/ls.d.ts +7 -0
  129. package/dist/tools/builtins/ls.d.ts.map +1 -0
  130. package/dist/tools/builtins/ls.js +114 -0
  131. package/dist/tools/builtins/ls.js.map +1 -0
  132. package/dist/tools/builtins/read-file.d.ts.map +1 -1
  133. package/dist/tools/builtins/read-file.js +20 -0
  134. package/dist/tools/builtins/read-file.js.map +1 -1
  135. package/dist/tools/builtins/write-file.d.ts.map +1 -1
  136. package/dist/tools/builtins/write-file.js +9 -0
  137. package/dist/tools/builtins/write-file.js.map +1 -1
  138. package/dist/types/ids/index.d.ts +1 -0
  139. package/dist/types/ids/index.d.ts.map +1 -1
  140. package/dist/types/run/config.d.ts +9 -1
  141. package/dist/types/run/config.d.ts.map +1 -1
  142. package/dist/types/run/events.d.ts +17 -1
  143. package/dist/types/run/events.d.ts.map +1 -1
  144. package/dist/types/sandbox/index.d.ts +66 -0
  145. package/dist/types/sandbox/index.d.ts.map +1 -0
  146. package/dist/types/sandbox/index.js +39 -0
  147. package/dist/types/sandbox/index.js.map +1 -0
  148. package/dist/types/tool/index.d.ts +3 -1
  149. package/dist/types/tool/index.d.ts.map +1 -1
  150. package/dist/utils/id.d.ts +3 -1
  151. package/dist/utils/id.d.ts.map +1 -1
  152. package/dist/utils/id.js +6 -0
  153. package/dist/utils/id.js.map +1 -1
  154. package/package.json +2 -2
  155. package/src/agents/ReactiveAgent.ts +12 -12
  156. package/src/agents/SupervisorAgent.ts +11 -11
  157. package/src/bridge/a2a/mapper.ts +6 -1
  158. package/src/bridge/sse/mapper.ts +26 -0
  159. package/src/config/runtime.ts +4 -0
  160. package/src/constants/index.ts +1 -0
  161. package/src/constants/sandbox/index.ts +31 -0
  162. package/src/constants/telemetry/index.ts +2 -2
  163. package/src/contracts/api.ts +3 -0
  164. package/src/index.ts +24 -4
  165. package/src/manager/run/emergency.ts +7 -7
  166. package/src/manager/run/persistence.ts +1 -1
  167. package/src/run/reporter.ts +25 -0
  168. package/src/runtime/query/checkpoint.ts +12 -12
  169. package/src/runtime/query/context.ts +6 -6
  170. package/src/runtime/query/events.ts +4 -4
  171. package/src/runtime/query/executor.ts +8 -1
  172. package/src/runtime/query/guard.ts +4 -4
  173. package/src/runtime/query/index.ts +76 -28
  174. package/src/runtime/query/iteration/index.ts +52 -55
  175. package/src/runtime/query/iteration/phases/advisory.ts +14 -14
  176. package/src/runtime/query/iteration/phases/checkpoint.ts +4 -4
  177. package/src/runtime/query/iteration/phases/compaction.ts +5 -5
  178. package/src/runtime/query/iteration/phases/context.ts +13 -13
  179. package/src/runtime/query/iteration/phases/plan.ts +3 -3
  180. package/src/runtime/query/iteration/phases/tool-review.ts +19 -19
  181. package/src/runtime/query/prompt.ts +1 -1
  182. package/src/runtime/query/result.ts +21 -21
  183. package/src/sandbox/factory.ts +16 -0
  184. package/src/sandbox/index.ts +2 -0
  185. package/src/sandbox/provider/local.ts +478 -0
  186. package/src/telemetry/attributes.ts +2 -2
  187. package/src/telemetry/metrics.ts +6 -6
  188. package/src/tools/builtins/bash.ts +31 -0
  189. package/src/tools/builtins/edit.ts +118 -0
  190. package/src/tools/builtins/grep.ts +151 -0
  191. package/src/tools/builtins/index.ts +16 -1
  192. package/src/tools/builtins/ls.ts +156 -0
  193. package/src/tools/builtins/read-file.ts +24 -0
  194. package/src/tools/builtins/write-file.ts +10 -0
  195. package/src/types/ids/index.ts +1 -0
  196. package/src/types/run/config.ts +9 -1
  197. package/src/types/run/events.ts +16 -1
  198. package/src/types/sandbox/index.ts +122 -0
  199. package/src/types/tool/index.ts +3 -1
  200. 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 agentSessionSpanName(agentName: string): string {
4
- return `namzu.agent.session ${agentName}`
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 {
@@ -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
- recordSessionDuration(status: string, durationSec: number): void
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 sessionDurationHistogram = meter.createHistogram('namzu.session.duration', {
29
- description: 'Agent session duration',
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
- recordSessionDuration(status: string, durationSec: number): void {
58
- sessionDurationHistogram.record(durationSec, {
59
- 'namzu.session.status': status,
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
+ }