@namzu/sdk 32.0.0 → 33.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (260) hide show
  1. package/CHANGELOG.md +127 -0
  2. package/dist/authorization/gate.d.ts +1 -1
  3. package/dist/authorization/gate.js +1 -1
  4. package/dist/connector/index.d.ts +1 -1
  5. package/dist/connector/index.d.ts.map +1 -1
  6. package/dist/connector/index.js +1 -1
  7. package/dist/connector/index.js.map +1 -1
  8. package/dist/connector/mcp/adapter.d.ts.map +1 -1
  9. package/dist/connector/mcp/adapter.js +71 -16
  10. package/dist/connector/mcp/adapter.js.map +1 -1
  11. package/dist/connector/mcp/client.d.ts.map +1 -1
  12. package/dist/connector/mcp/client.js +3 -2
  13. package/dist/connector/mcp/client.js.map +1 -1
  14. package/dist/connector/mcp/http-redirect.d.ts +17 -0
  15. package/dist/connector/mcp/http-redirect.d.ts.map +1 -0
  16. package/dist/connector/mcp/http-redirect.js +31 -0
  17. package/dist/connector/mcp/http-redirect.js.map +1 -0
  18. package/dist/connector/mcp/http-sse.d.ts.map +1 -1
  19. package/dist/connector/mcp/http-sse.js +5 -0
  20. package/dist/connector/mcp/http-sse.js.map +1 -1
  21. package/dist/connector/mcp/image-admission.d.ts +16 -0
  22. package/dist/connector/mcp/image-admission.d.ts.map +1 -0
  23. package/dist/connector/mcp/image-admission.js +288 -0
  24. package/dist/connector/mcp/image-admission.js.map +1 -0
  25. package/dist/connector/mcp/stdio.d.ts +13 -0
  26. package/dist/connector/mcp/stdio.d.ts.map +1 -1
  27. package/dist/connector/mcp/stdio.js +66 -23
  28. package/dist/connector/mcp/stdio.js.map +1 -1
  29. package/dist/connector/mcp/streamable-http.d.ts.map +1 -1
  30. package/dist/connector/mcp/streamable-http.js +3 -0
  31. package/dist/connector/mcp/streamable-http.js.map +1 -1
  32. package/dist/constants/tools/index.js +1 -1
  33. package/dist/constants/tools/index.js.map +1 -1
  34. package/dist/execution/base.d.ts +13 -0
  35. package/dist/execution/base.d.ts.map +1 -1
  36. package/dist/execution/base.js +103 -8
  37. package/dist/execution/base.js.map +1 -1
  38. package/dist/execution/errors.d.ts +12 -0
  39. package/dist/execution/errors.d.ts.map +1 -0
  40. package/dist/execution/errors.js +21 -0
  41. package/dist/execution/errors.js.map +1 -0
  42. package/dist/execution/factory.d.ts.map +1 -1
  43. package/dist/execution/factory.js +1 -0
  44. package/dist/execution/factory.js.map +1 -1
  45. package/dist/execution/hybrid.d.ts +5 -0
  46. package/dist/execution/hybrid.d.ts.map +1 -1
  47. package/dist/execution/hybrid.js +36 -16
  48. package/dist/execution/hybrid.js.map +1 -1
  49. package/dist/execution/index.d.ts +1 -0
  50. package/dist/execution/index.d.ts.map +1 -1
  51. package/dist/execution/index.js +1 -0
  52. package/dist/execution/index.js.map +1 -1
  53. package/dist/execution/local.d.ts +13 -0
  54. package/dist/execution/local.d.ts.map +1 -1
  55. package/dist/execution/local.js +145 -19
  56. package/dist/execution/local.js.map +1 -1
  57. package/dist/execution/output.d.ts +27 -0
  58. package/dist/execution/output.d.ts.map +1 -0
  59. package/dist/execution/output.js +105 -0
  60. package/dist/execution/output.js.map +1 -0
  61. package/dist/execution/remote.d.ts +26 -2
  62. package/dist/execution/remote.d.ts.map +1 -1
  63. package/dist/execution/remote.js +86 -15
  64. package/dist/execution/remote.js.map +1 -1
  65. package/dist/plugin/lifecycle.d.ts.map +1 -1
  66. package/dist/plugin/lifecycle.js +9 -3
  67. package/dist/plugin/lifecycle.js.map +1 -1
  68. package/dist/process/kill-tree.d.ts +11 -13
  69. package/dist/process/kill-tree.d.ts.map +1 -1
  70. package/dist/process/kill-tree.js +11 -13
  71. package/dist/process/kill-tree.js.map +1 -1
  72. package/dist/provider/fallback.d.ts.map +1 -1
  73. package/dist/provider/fallback.js +31 -0
  74. package/dist/provider/fallback.js.map +1 -1
  75. package/dist/provider/idle-timeout.d.ts.map +1 -1
  76. package/dist/provider/idle-timeout.js +5 -0
  77. package/dist/provider/idle-timeout.js.map +1 -1
  78. package/dist/provider/retry.d.ts.map +1 -1
  79. package/dist/provider/retry.js +5 -0
  80. package/dist/provider/retry.js.map +1 -1
  81. package/dist/public-runtime.d.ts +2 -2
  82. package/dist/public-runtime.d.ts.map +1 -1
  83. package/dist/public-runtime.js +2 -2
  84. package/dist/public-runtime.js.map +1 -1
  85. package/dist/registry/tool/execute.d.ts +7 -1
  86. package/dist/registry/tool/execute.d.ts.map +1 -1
  87. package/dist/registry/tool/execute.js +201 -54
  88. package/dist/registry/tool/execute.js.map +1 -1
  89. package/dist/run/command-gate.d.ts.map +1 -1
  90. package/dist/run/command-gate.js +11 -0
  91. package/dist/run/command-gate.js.map +1 -1
  92. package/dist/run/workspace-fingerprint.d.ts +4 -4
  93. package/dist/run/workspace-fingerprint.d.ts.map +1 -1
  94. package/dist/run/workspace-fingerprint.js +6 -4
  95. package/dist/run/workspace-fingerprint.js.map +1 -1
  96. package/dist/runtime/bidi/index.d.ts +1 -1
  97. package/dist/runtime/bidi/index.d.ts.map +1 -1
  98. package/dist/runtime/bidi/index.js +1 -1
  99. package/dist/runtime/bidi/index.js.map +1 -1
  100. package/dist/runtime/bidi/session.d.ts +19 -0
  101. package/dist/runtime/bidi/session.d.ts.map +1 -1
  102. package/dist/runtime/bidi/session.js +150 -26
  103. package/dist/runtime/bidi/session.js.map +1 -1
  104. package/dist/runtime/query/executor.d.ts +43 -1
  105. package/dist/runtime/query/executor.d.ts.map +1 -1
  106. package/dist/runtime/query/executor.js +626 -161
  107. package/dist/runtime/query/executor.js.map +1 -1
  108. package/dist/runtime/query/index.d.ts.map +1 -1
  109. package/dist/runtime/query/index.js +46 -4
  110. package/dist/runtime/query/index.js.map +1 -1
  111. package/dist/runtime/query/iteration/phases/tool-review.d.ts.map +1 -1
  112. package/dist/runtime/query/iteration/phases/tool-review.js +84 -22
  113. package/dist/runtime/query/iteration/phases/tool-review.js.map +1 -1
  114. package/dist/runtime/query/request-rich-content.d.ts.map +1 -1
  115. package/dist/runtime/query/request-rich-content.js +33 -13
  116. package/dist/runtime/query/request-rich-content.js.map +1 -1
  117. package/dist/runtime/query/resume-pending.d.ts +5 -1
  118. package/dist/runtime/query/resume-pending.d.ts.map +1 -1
  119. package/dist/runtime/query/resume-pending.js +66 -1
  120. package/dist/runtime/query/resume-pending.js.map +1 -1
  121. package/dist/runtime/query/tool-output-budget.d.ts.map +1 -1
  122. package/dist/runtime/query/tool-output-budget.js +45 -16
  123. package/dist/runtime/query/tool-output-budget.js.map +1 -1
  124. package/dist/runtime/query/tooling.d.ts +6 -0
  125. package/dist/runtime/query/tooling.d.ts.map +1 -1
  126. package/dist/runtime/query/tooling.js +4 -0
  127. package/dist/runtime/query/tooling.js.map +1 -1
  128. package/dist/sandbox/provider/local.d.ts.map +1 -1
  129. package/dist/sandbox/provider/local.js +193 -41
  130. package/dist/sandbox/provider/local.js.map +1 -1
  131. package/dist/scheduler/delegating.d.ts +2 -1
  132. package/dist/scheduler/delegating.d.ts.map +1 -1
  133. package/dist/scheduler/delegating.js +6 -5
  134. package/dist/scheduler/delegating.js.map +1 -1
  135. package/dist/scheduler/local.d.ts +2 -1
  136. package/dist/scheduler/local.d.ts.map +1 -1
  137. package/dist/scheduler/local.js +2 -2
  138. package/dist/scheduler/local.js.map +1 -1
  139. package/dist/session/workspace/driver.d.ts +12 -4
  140. package/dist/session/workspace/driver.d.ts.map +1 -1
  141. package/dist/session/workspace/git-worktree.d.ts +17 -3
  142. package/dist/session/workspace/git-worktree.d.ts.map +1 -1
  143. package/dist/session/workspace/git-worktree.js +153 -34
  144. package/dist/session/workspace/git-worktree.js.map +1 -1
  145. package/dist/skills/registry.d.ts +15 -0
  146. package/dist/skills/registry.d.ts.map +1 -1
  147. package/dist/skills/registry.js +33 -0
  148. package/dist/skills/registry.js.map +1 -1
  149. package/dist/tools/builtins/computer-use.d.ts.map +1 -1
  150. package/dist/tools/builtins/computer-use.js +39 -2
  151. package/dist/tools/builtins/computer-use.js.map +1 -1
  152. package/dist/tools/builtins/run-code.d.ts.map +1 -1
  153. package/dist/tools/builtins/run-code.js +9 -11
  154. package/dist/tools/builtins/run-code.js.map +1 -1
  155. package/dist/tools/builtins/skill.d.ts +2 -1
  156. package/dist/tools/builtins/skill.d.ts.map +1 -1
  157. package/dist/tools/builtins/skill.js +250 -6
  158. package/dist/tools/builtins/skill.js.map +1 -1
  159. package/dist/tools/coordinator/agent.d.ts.map +1 -1
  160. package/dist/tools/coordinator/agent.js +100 -30
  161. package/dist/tools/coordinator/agent.js.map +1 -1
  162. package/dist/types/agent/manager.d.ts +1 -1
  163. package/dist/types/agent/manager.d.ts.map +1 -1
  164. package/dist/types/agent/scheduler.d.ts +10 -1
  165. package/dist/types/agent/scheduler.d.ts.map +1 -1
  166. package/dist/types/authorization/index.d.ts +12 -12
  167. package/dist/types/bidi/index.d.ts +19 -2
  168. package/dist/types/bidi/index.d.ts.map +1 -1
  169. package/dist/types/computer-use/index.d.ts +18 -0
  170. package/dist/types/computer-use/index.d.ts.map +1 -1
  171. package/dist/types/connector/execution-context.d.ts +3 -1
  172. package/dist/types/connector/execution-context.d.ts.map +1 -1
  173. package/dist/types/execution/index.d.ts +43 -1
  174. package/dist/types/execution/index.d.ts.map +1 -1
  175. package/dist/types/hitl/index.d.ts +11 -0
  176. package/dist/types/hitl/index.d.ts.map +1 -1
  177. package/dist/types/hitl/index.js.map +1 -1
  178. package/dist/types/message/index.d.ts +1 -1
  179. package/dist/types/message/index.d.ts.map +1 -1
  180. package/dist/types/message/index.js +2 -1
  181. package/dist/types/message/index.js.map +1 -1
  182. package/dist/types/plugin/index.d.ts +14 -1
  183. package/dist/types/plugin/index.d.ts.map +1 -1
  184. package/dist/types/plugin/index.js +1 -0
  185. package/dist/types/plugin/index.js.map +1 -1
  186. package/dist/types/provider/interface.d.ts +14 -0
  187. package/dist/types/provider/interface.d.ts.map +1 -1
  188. package/dist/types/sandbox/index.d.ts +15 -10
  189. package/dist/types/sandbox/index.d.ts.map +1 -1
  190. package/dist/types/sandbox/index.js.map +1 -1
  191. package/dist/types/tool/index.d.ts +77 -6
  192. package/dist/types/tool/index.d.ts.map +1 -1
  193. package/dist/types/tool/index.js.map +1 -1
  194. package/dist/types/workspace/ref.d.ts +5 -0
  195. package/dist/types/workspace/ref.d.ts.map +1 -1
  196. package/dist/utils/abort.d.ts +8 -0
  197. package/dist/utils/abort.d.ts.map +1 -1
  198. package/dist/utils/abort.js +16 -0
  199. package/dist/utils/abort.js.map +1 -1
  200. package/package.json +4 -4
  201. package/src/authorization/gate.ts +1 -1
  202. package/src/connector/index.ts +2 -0
  203. package/src/connector/mcp/adapter.ts +82 -21
  204. package/src/connector/mcp/client.ts +2 -1
  205. package/src/connector/mcp/http-redirect.ts +32 -0
  206. package/src/connector/mcp/http-sse.ts +5 -0
  207. package/src/connector/mcp/image-admission.ts +272 -0
  208. package/src/connector/mcp/stdio.ts +63 -21
  209. package/src/connector/mcp/streamable-http.ts +3 -0
  210. package/src/constants/tools/index.ts +1 -1
  211. package/src/execution/base.ts +124 -7
  212. package/src/execution/errors.ts +24 -0
  213. package/src/execution/factory.ts +1 -0
  214. package/src/execution/hybrid.ts +47 -16
  215. package/src/execution/index.ts +1 -0
  216. package/src/execution/local.ts +164 -19
  217. package/src/execution/output.ts +104 -0
  218. package/src/execution/remote.ts +122 -17
  219. package/src/plugin/lifecycle.ts +9 -3
  220. package/src/process/kill-tree.ts +11 -13
  221. package/src/provider/fallback.ts +39 -0
  222. package/src/provider/idle-timeout.ts +8 -0
  223. package/src/provider/retry.ts +8 -0
  224. package/src/public-runtime.ts +7 -1
  225. package/src/registry/tool/execute.ts +230 -64
  226. package/src/run/command-gate.ts +12 -0
  227. package/src/run/workspace-fingerprint.ts +5 -4
  228. package/src/runtime/bidi/index.ts +1 -1
  229. package/src/runtime/bidi/session.ts +177 -28
  230. package/src/runtime/query/executor.ts +769 -163
  231. package/src/runtime/query/index.ts +53 -6
  232. package/src/runtime/query/iteration/phases/tool-review.ts +87 -23
  233. package/src/runtime/query/request-rich-content.ts +39 -14
  234. package/src/runtime/query/resume-pending.ts +83 -1
  235. package/src/runtime/query/tool-output-budget.ts +52 -17
  236. package/src/runtime/query/tooling.ts +10 -0
  237. package/src/sandbox/provider/local.ts +213 -45
  238. package/src/scheduler/delegating.ts +6 -5
  239. package/src/scheduler/local.ts +3 -2
  240. package/src/session/workspace/driver.ts +12 -4
  241. package/src/session/workspace/git-worktree.ts +173 -34
  242. package/src/skills/registry.ts +47 -0
  243. package/src/tools/builtins/computer-use.ts +45 -2
  244. package/src/tools/builtins/run-code.ts +9 -11
  245. package/src/tools/builtins/skill.ts +325 -7
  246. package/src/tools/coordinator/agent.ts +108 -33
  247. package/src/types/agent/manager.ts +1 -1
  248. package/src/types/agent/scheduler.ts +10 -1
  249. package/src/types/bidi/index.ts +19 -2
  250. package/src/types/computer-use/index.ts +19 -0
  251. package/src/types/connector/execution-context.ts +3 -0
  252. package/src/types/execution/index.ts +46 -1
  253. package/src/types/hitl/index.ts +11 -0
  254. package/src/types/message/index.ts +3 -2
  255. package/src/types/plugin/index.ts +15 -0
  256. package/src/types/provider/interface.ts +18 -0
  257. package/src/types/sandbox/index.ts +15 -10
  258. package/src/types/tool/index.ts +85 -6
  259. package/src/types/workspace/ref.ts +5 -0
  260. package/src/utils/abort.ts +18 -0
@@ -38,6 +38,7 @@ import type {
38
38
  SandboxProvider,
39
39
  SandboxStatus,
40
40
  } from '../../types/sandbox/index.js'
41
+ import { subscribeToAbort } from '../../utils/abort.js'
41
42
  import { generateSandboxId } from '../../utils/id.js'
42
43
  import type { Logger } from '../../utils/logger.js'
43
44
  import {
@@ -72,6 +73,8 @@ function assertInsideSandbox(sandboxRoot: string, targetPath: string): string {
72
73
  const LINUX_UNSHARE_FLAGS = ['--mount', '--pid', '--fork', '--map-root-user', '--net']
73
74
 
74
75
  const SPAWN_PROBE_SENTINEL = 'namzu-sandbox-spawn-probe'
76
+ const BWRAP_INFO_FD = 3
77
+ const BWRAP_BLOCK_FD = 4
75
78
 
76
79
  interface SpawnProbeObservation {
77
80
  readonly error?: unknown
@@ -117,6 +120,48 @@ function probeSandboxSpawn(command: string, wrapperArgs: readonly string[]): boo
117
120
  return acceptsSandboxSpawnProbe(observation)
118
121
  }
119
122
 
123
+ function readBwrapChildPid(value: string): number | undefined {
124
+ try {
125
+ const record = JSON.parse(value) as { readonly 'child-pid'?: unknown }
126
+ const pid = record['child-pid']
127
+ return typeof pid === 'number' && Number.isSafeInteger(pid) && pid > 0 ? pid : undefined
128
+ } catch {
129
+ return undefined
130
+ }
131
+ }
132
+
133
+ /** The bwrap tier is usable only if its child can be addressed after startup. */
134
+ function probeBwrapSpawn(command: string, wrapperArgs: readonly string[]): boolean {
135
+ const observation = spawnSync(
136
+ command,
137
+ [
138
+ ...wrapperArgs,
139
+ '--info-fd',
140
+ String(BWRAP_INFO_FD),
141
+ '--block-fd',
142
+ String(BWRAP_BLOCK_FD),
143
+ '--',
144
+ process.execPath,
145
+ '-e',
146
+ `process.stdout.write(${JSON.stringify(SPAWN_PROBE_SENTINEL)})`,
147
+ ],
148
+ {
149
+ encoding: 'utf8',
150
+ timeout: 5_000,
151
+ // `/dev/null` proves the option is accepted and lets the synchronous
152
+ // probe proceed. Production supplies a live pipe and releases it only
153
+ // after the info record establishes ownership of the inner namespace.
154
+ stdio: ['ignore', 'pipe', 'pipe', 'pipe', 'ignore'],
155
+ },
156
+ )
157
+ const status = observation.output?.[BWRAP_INFO_FD]
158
+ return (
159
+ acceptsSandboxSpawnProbe(observation) &&
160
+ typeof status === 'string' &&
161
+ readBwrapChildPid(status) !== undefined
162
+ )
163
+ }
164
+
120
165
  const TRUSTED_WRAPPER_CANDIDATES = {
121
166
  bwrap: ['/usr/bin/bwrap', '/bin/bwrap', '/usr/sbin/bwrap'],
122
167
  unshare: ['/usr/bin/unshare', '/bin/unshare', '/usr/sbin/unshare'],
@@ -275,7 +320,7 @@ function detectEnvironment(): DetectedEnvironment {
275
320
  // hosts that mediate namespace helpers and can claim a tier production
276
321
  // cannot actually drive.
277
322
  const bwrap = resolveTrustedWrapper('bwrap')
278
- if (bwrap !== undefined && probeSandboxSpawn(bwrap, buildBwrapArgs(tmpdir()))) {
323
+ if (bwrap !== undefined && probeBwrapSpawn(bwrap, buildBwrapArgs(tmpdir()))) {
279
324
  return { environment: 'linux-bwrap', wrapperCommand: bwrap }
280
325
  }
281
326
 
@@ -522,6 +567,27 @@ function buildSafeEnv(
522
567
  return env
523
568
  }
524
569
 
570
+ /**
571
+ * Reach bwrap's PID-namespace reaper as well as its outer host wrapper.
572
+ *
573
+ * `--new-session` deliberately moves the inner reaper out of the outer
574
+ * detached process group. `--die-with-parent` normally bridges that boundary,
575
+ * but there is a startup interval before the inner reaper has armed its parent
576
+ * death signal. `--info-fd` gives us the host pid once it exists. Address both
577
+ * its eventual process group and the pid itself: before `setsid` only the pid
578
+ * is valid; afterwards the negative pid reaches the complete inner session.
579
+ */
580
+ function killBwrapChild(pid: number | undefined, signal: NodeJS.Signals): void {
581
+ if (pid === undefined || process.platform === 'win32') return
582
+ for (const target of [-pid, pid]) {
583
+ try {
584
+ process.kill(target, signal)
585
+ } catch {
586
+ // The other address may be the valid one, or the namespace is gone.
587
+ }
588
+ }
589
+ }
590
+
525
591
  // ---------------------------------------------------------------------------
526
592
  // LocalSandbox
527
593
  // ---------------------------------------------------------------------------
@@ -577,7 +643,10 @@ class LocalSandbox implements Sandbox {
577
643
 
578
644
  const cwd = opts?.cwd ? assertInsideSandbox(this.rootDir, opts.cwd) : this.rootDir
579
645
 
580
- const { spawnCommand, spawnArgs } = this.buildSpawnArgs(command, args)
646
+ const { spawnCommand, spawnArgs, bwrapInfoFd, bwrapBlockFd } = this.buildSpawnArgs(
647
+ command,
648
+ args,
649
+ )
581
650
 
582
651
  this.log.debug('Executing command', {
583
652
  'namzu.sandbox.command': command,
@@ -586,23 +655,27 @@ class LocalSandbox implements Sandbox {
586
655
  'namzu.execution.environment': this.environment,
587
656
  })
588
657
 
589
- // The caller's cancellation and this call's own deadline both have to
590
- // reach `spawn`, and `spawn` takes exactly one signal.
591
- //
592
- // Only the deadline used to. `SandboxExecOptions.signal` is declared,
593
- // documented, and exported, and every backend dropped it so a Stop
594
- // abandoned the *wait* and left the sandboxed process running, which is
595
- // verbatim the failure the option's own docstring says it exists to
596
- // prevent. `AbortSignal.any` is what makes both reach the child: it
597
- // aborts as soon as either does, and — unlike an `addEventListener`
598
- // bridge — it does not retain a listener on the caller's long-lived
599
- // signal after this call settles.
658
+ // The caller's cancellation and this call's own deadline are separate
659
+ // causes. `spawn({ signal })` cannot own either one here: Node sends that
660
+ // signal only to the direct wrapper pid and removes its abort listener on
661
+ // `exit`, while this promise must wait for `close` so inherited stdout and
662
+ // stderr have drained. A wrapper can therefore exit before a descendant;
663
+ // an abort in that interval would reach nobody and the descendant would
664
+ // keep both pipes and this call alive.
600
665
  const ac = new AbortController()
601
666
  const timeoutId = setTimeout(() => ac.abort(), timeout)
602
- const spawnSignal = opts?.signal ? AbortSignal.any([ac.signal, opts.signal]) : ac.signal
603
667
 
604
668
  try {
605
- const result = await this.spawnProcess(spawnCommand, spawnArgs, cwd, env, ac, spawnSignal)
669
+ const result = await this.spawnProcess(
670
+ spawnCommand,
671
+ spawnArgs,
672
+ cwd,
673
+ env,
674
+ ac.signal,
675
+ opts?.signal,
676
+ bwrapInfoFd,
677
+ bwrapBlockFd,
678
+ )
606
679
  return { ...result, durationMs: Date.now() - startTime }
607
680
  } finally {
608
681
  clearTimeout(timeoutId)
@@ -685,8 +758,13 @@ class LocalSandbox implements Sandbox {
685
758
  private buildSpawnArgs(
686
759
  command: string,
687
760
  args: string[],
688
- ): { spawnCommand: string; spawnArgs: string[] } {
689
- return buildLimitedSpawn({
761
+ ): {
762
+ spawnCommand: string
763
+ spawnArgs: string[]
764
+ bwrapInfoFd?: number
765
+ bwrapBlockFd?: number
766
+ } {
767
+ const built = buildLimitedSpawn({
690
768
  environment: this.environment,
691
769
  ...(this.wrapperCommand !== undefined ? { wrapperCommand: this.wrapperCommand } : {}),
692
770
  command,
@@ -697,24 +775,40 @@ class LocalSandbox implements Sandbox {
697
775
  : {}),
698
776
  ...(this.config.maxProcesses !== undefined ? { maxProcesses: this.config.maxProcesses } : {}),
699
777
  })
778
+ if (this.environment !== 'linux-bwrap') return built
779
+
780
+ const separator = built.spawnArgs.indexOf('--')
781
+ if (separator < 0) throw new Error('bwrap invocation has no command separator')
782
+ return {
783
+ spawnCommand: built.spawnCommand,
784
+ spawnArgs: [
785
+ ...built.spawnArgs.slice(0, separator),
786
+ '--info-fd',
787
+ String(BWRAP_INFO_FD),
788
+ '--block-fd',
789
+ String(BWRAP_BLOCK_FD),
790
+ ...built.spawnArgs.slice(separator),
791
+ ],
792
+ bwrapInfoFd: BWRAP_INFO_FD,
793
+ bwrapBlockFd: BWRAP_BLOCK_FD,
794
+ }
700
795
  }
701
796
 
702
797
  /**
703
- * @param ac this call's own deadline still the thing that decides
704
- * whether a termination is reported as `timedOut`.
705
- * @param signal what actually reaches `spawn`: the deadline, merged with
706
- * the caller's cancellation when one was passed. Two
707
- * parameters because they answer different questions —
708
- * "should this process die" and "did it die because it ran
709
- * too long" — and a cancelled run did not time out.
798
+ * The two signals stay distinct so the first accepted cause is latched.
799
+ * Reading `deadline.aborted` later would let a deadline that fired while a
800
+ * caller cancellation was draining retroactively turn that cancellation
801
+ * into a timeout.
710
802
  */
711
803
  private spawnProcess(
712
804
  command: string,
713
805
  args: string[],
714
806
  cwd: string,
715
807
  env: Record<string, string>,
716
- ac: AbortController,
717
- signal: AbortSignal = ac.signal,
808
+ deadlineSignal: AbortSignal,
809
+ callerSignal?: AbortSignal,
810
+ bwrapInfoFd?: number,
811
+ bwrapBlockFd?: number,
718
812
  ): Promise<Omit<SandboxExecResult, 'durationMs'>> {
719
813
  return new Promise((resolvePromise, rejectPromise) => {
720
814
  let child: ReturnType<typeof spawn>
@@ -722,8 +816,10 @@ class LocalSandbox implements Sandbox {
722
816
  child = spawn(command, args, {
723
817
  cwd,
724
818
  env,
725
- stdio: ['pipe', 'pipe', 'pipe'],
726
- signal,
819
+ stdio:
820
+ bwrapInfoFd === undefined
821
+ ? ['pipe', 'pipe', 'pipe']
822
+ : ['pipe', 'pipe', 'pipe', 'pipe', 'pipe'],
727
823
  // Leader of its own process group (POSIX only — Windows has
728
824
  // nothing to opt into here), not a member of this Node
729
825
  // process's. That is what lets `killTree` below reach `cmd`
@@ -738,30 +834,92 @@ class LocalSandbox implements Sandbox {
738
834
 
739
835
  const stdout = new CappedStream(SANDBOX_MAX_OUTPUT_BYTES)
740
836
  const stderr = new CappedStream(SANDBOX_MAX_OUTPUT_BYTES)
741
- let timedOut = false
837
+ let cancellation: 'caller' | 'deadline' | undefined
838
+ let bwrapChildPid: number | undefined
839
+ let terminationSignal: NodeJS.Signals | undefined
840
+ let settled = false
841
+ let escalation: ReturnType<typeof setTimeout> | undefined
842
+ const disposeAbortListeners: Array<() => void> = []
843
+
844
+ const finishSubscriptions = () => {
845
+ for (const dispose of disposeAbortListeners.splice(0)) dispose()
846
+ }
847
+ const terminate = (signal: NodeJS.Signals) => {
848
+ terminationSignal = signal
849
+ // Before bwrap publishes its inner reaper, killing only the outer
850
+ // wrapper can strand startup between fork() and PR_SET_PDEATHSIG. Its
851
+ // block fd guarantees the authored command is not admitted meanwhile;
852
+ // retain the wrapper until the addressable inner pid arrives.
853
+ if (bwrapBlockFd !== undefined && bwrapChildPid === undefined) return
854
+ killTree(child, signal)
855
+ killBwrapChild(bwrapChildPid, signal)
856
+ }
857
+ const cancel = (origin: 'caller' | 'deadline') => {
858
+ if (settled || cancellation !== undefined) return
859
+ cancellation = origin
860
+ // Own the group while its leader is still ours. Deferring this to
861
+ // Node's AbortError loses the wrapper-exit/stdio-close interval.
862
+ terminate('SIGTERM')
863
+ escalation = setTimeout(() => {
864
+ escalation = undefined
865
+ terminate('SIGKILL')
866
+ }, SANDBOX_KILL_GRACE_MS)
867
+ // A command that honoured SIGTERM must not leave the host alive just
868
+ // to deliver a no-op escalation. If shared stdio closes first, the
869
+ // close handler forces any remaining group before releasing its pid.
870
+ escalation.unref?.()
871
+ }
742
872
 
743
873
  child.stdout?.on('data', (chunk: Buffer) => stdout.push(chunk))
744
874
  child.stderr?.on('data', (chunk: Buffer) => stderr.push(chunk))
745
-
746
- child.on('error', (err: NodeJS.ErrnoException) => {
747
- if (err.code === 'ABORT_ERR' || signal.aborted) {
748
- // `timedOut` means the DEADLINE fired. A caller-cancelled
749
- // run is aborted but not late, and reporting it as a
750
- // timeout would tell the model to retry with a longer
751
- // budget for something a human just stopped.
752
- timedOut = ac.signal.aborted
753
- // `spawn`'s own `signal` handling already reached `child.pid`
754
- // the outermost wrapper — but that was never the process
755
- // this call exists to stop. Reach the whole tree, then give
756
- // it a grace period before forcing it.
757
- killTree(child, 'SIGTERM')
758
- setTimeout(() => killTree(child, 'SIGKILL'), SANDBOX_KILL_GRACE_MS)
759
- return
875
+ const bwrapInfo = bwrapInfoFd === undefined ? undefined : child.stdio[bwrapInfoFd]
876
+ const bwrapBlock = bwrapBlockFd === undefined ? undefined : child.stdio[bwrapBlockFd]
877
+ let bwrapInfoBuffer = ''
878
+ bwrapInfo?.on('data', (chunk: Buffer) => {
879
+ if (bwrapChildPid !== undefined) return
880
+ bwrapInfoBuffer += chunk.toString('utf8')
881
+ // `--info-fd` writes one JSON document, often pretty-printed across
882
+ // several lines and potentially split across stream chunks. Parse the
883
+ // whole accumulated document whenever more arrives.
884
+ const observed = readBwrapChildPid(bwrapInfoBuffer)
885
+ if (observed === undefined) return
886
+ bwrapChildPid = observed
887
+ // Cancellation may have won before bwrap published the inner reaper.
888
+ // Nothing authored has been admitted yet, so there is no graceful
889
+ // shutdown to preserve: kill startup atomically before it can cross the
890
+ // gate. Otherwise one byte admits the command now that it has an owner.
891
+ if (terminationSignal !== undefined) {
892
+ if (escalation !== undefined) {
893
+ clearTimeout(escalation)
894
+ escalation = undefined
895
+ }
896
+ terminate('SIGKILL')
897
+ } else if (bwrapBlock !== undefined && bwrapBlock !== null && 'end' in bwrapBlock) {
898
+ bwrapBlock.end(Buffer.from([1]))
760
899
  }
900
+ })
901
+
902
+ child.once('error', (err: NodeJS.ErrnoException) => {
903
+ if (settled) return
904
+ settled = true
905
+ finishSubscriptions()
906
+ if (escalation !== undefined) clearTimeout(escalation)
761
907
  rejectPromise(err)
762
908
  })
763
909
 
764
- child.on('close', (code, signal) => {
910
+ child.once('close', (code, signal) => {
911
+ if (settled) return
912
+ settled = true
913
+ finishSubscriptions()
914
+ if (escalation !== undefined) {
915
+ clearTimeout(escalation)
916
+ escalation = undefined
917
+ // `close` proves the wrapper and shared pipes are gone, not that a
918
+ // descendant which closed its copies has stopped. Force the old group
919
+ // now: retaining its numeric pid for a later timer risks pid reuse.
920
+ terminate('SIGKILL')
921
+ }
922
+ const timedOut = cancellation === 'deadline'
765
923
  resolvePromise({
766
924
  exitCode: code ?? (timedOut ? 124 : 1),
767
925
  stdout: stdout.text,
@@ -776,6 +934,16 @@ class LocalSandbox implements Sandbox {
776
934
  stderrTruncated: stderr.truncated,
777
935
  })
778
936
  })
937
+
938
+ // Register only after every terminal handler exists, then re-check.
939
+ // JavaScript cannot interleave an abort callback inside this synchronous
940
+ // block, and the check closes the already-aborted admission edge.
941
+ disposeAbortListeners.push(subscribeToAbort(deadlineSignal, () => cancel('deadline')))
942
+ if (callerSignal !== undefined) {
943
+ disposeAbortListeners.push(subscribeToAbort(callerSignal, () => cancel('caller')))
944
+ }
945
+ if (deadlineSignal.aborted) cancel('deadline')
946
+ else if (callerSignal?.aborted) cancel('caller')
779
947
  })
780
948
  }
781
949
  }
@@ -4,6 +4,7 @@ import type { CreateTaskOptions, TaskHandle, TaskScheduler } from '../types/agen
4
4
  import type { AgentTaskState } from '../types/agent/task.js'
5
5
  import type { RunExecutionStatus } from '../types/common/index.js'
6
6
  import type { TaskId } from '../types/ids/index.js'
7
+ import { type CancelCause, RunCancelled } from '../types/run/cancel-cause.js'
7
8
  import { asRunId, generateTaskId } from '../utils/id.js'
8
9
 
9
10
  /**
@@ -248,16 +249,16 @@ export class DelegatingTaskScheduler implements TaskScheduler {
248
249
  await entry.delegate.continue(message)
249
250
  }
250
251
 
251
- cancelTask(taskId: TaskId): void {
252
+ cancelTask(taskId: TaskId, cause?: CancelCause): void {
252
253
  const entry = this.tasks.get(taskId)
253
254
  if (!entry) {
254
- this.config.local?.cancelTask(taskId)
255
+ this.config.local?.cancelTask(taskId, cause)
255
256
  return
256
257
  }
257
258
  if (!entry.delegate.capabilities.cancel) {
258
259
  throw new DelegateCapabilityError({ id: entry.delegate.id, capability: 'cancel' })
259
260
  }
260
- controllerAbort(entry)
261
+ controllerAbort(entry, cause)
261
262
  }
262
263
 
263
264
  getTask(taskId: TaskId): TaskHandle | undefined {
@@ -292,6 +293,6 @@ export class DelegatingTaskScheduler implements TaskScheduler {
292
293
  * race that: a delegate that completed a microsecond before the abort would
293
294
  * be recorded as cancelled while its answer sat unread.
294
295
  */
295
- function controllerAbort(entry: Entry): void {
296
- entry.controller.abort()
296
+ function controllerAbort(entry: Entry, cause?: CancelCause): void {
297
+ entry.controller.abort(cause ? new RunCancelled(cause) : undefined)
297
298
  }
@@ -11,6 +11,7 @@ import type {
11
11
  import type { AgentTaskContext } from '../types/agent/task.js'
12
12
  import type { TaskId } from '../types/ids/index.js'
13
13
  import { createUserMessage } from '../types/message/index.js'
14
+ import type { CancelCause } from '../types/run/cancel-cause.js'
14
15
  import type { RunEventListener } from '../types/run/events.js'
15
16
  import { toErrorMessage } from '../utils/error.js'
16
17
  import { SCOPE_ATTRIBUTE } from '../utils/log/types.js'
@@ -244,8 +245,8 @@ export class LocalTaskScheduler implements TaskScheduler {
244
245
  await this.agentManager.continueTask(taskId, message)
245
246
  }
246
247
 
247
- cancelTask(taskId: TaskId): void {
248
- this.agentManager.cancel(taskId)
248
+ cancelTask(taskId: TaskId, cause?: CancelCause): void {
249
+ this.agentManager.cancel(taskId, cause)
249
250
  }
250
251
 
251
252
  getTask(taskId: TaskId): TaskHandle | undefined {
@@ -19,13 +19,19 @@ import type { WorkspaceBackendKind, WorkspaceRef } from '../../types/workspace/r
19
19
  */
20
20
  export interface CreateWorkspaceParams {
21
21
  baseRef?: string
22
+ /**
23
+ * Traceable name below the configured workspace root. Nested names are
24
+ * accepted; absolute paths, traversal, and paths rewritten by a filesystem
25
+ * link are refused by the reference Git driver.
26
+ */
22
27
  label?: string
23
28
  }
24
29
 
25
30
  /**
26
- * Params for {@link WorkspaceBackendDriver.branch}. `label` is a free-form
27
- * tag embedded into the new workspace name so operators can trace provenance
28
- * without reaching into the driver's internal naming scheme.
31
+ * Params for {@link WorkspaceBackendDriver.branch}. `label` is a traceable tag
32
+ * embedded into the new workspace name so operators can identify provenance
33
+ * without reaching into the driver's internal naming scheme. Backend path and
34
+ * identifier constraints still apply.
29
35
  */
30
36
  export interface BranchWorkspaceParams {
31
37
  label?: string
@@ -47,7 +53,9 @@ export interface WorkspaceInspection {
47
53
  * Backend driver contract. All methods are async and may throw
48
54
  * {@link WorkspaceBackendError} on I/O failures. `dispose` is idempotent —
49
55
  * calling it against an already-disposed ref is not an error (mitigates
50
- * roadmap Risk #3: broadcast rollback must not fail on partial state).
56
+ * roadmap Risk #3: broadcast rollback must not fail on partial state). A ref
57
+ * remains affiliated with the driver configuration that created it; drivers
58
+ * refuse foreign backend, repository, or ownership-root metadata.
51
59
  */
52
60
  export interface WorkspaceBackendDriver {
53
61
  readonly kind: WorkspaceBackendKind