@namzu/sdk 32.0.0 → 33.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +127 -0
- package/dist/authorization/gate.d.ts +1 -1
- package/dist/authorization/gate.js +1 -1
- package/dist/connector/index.d.ts +1 -1
- package/dist/connector/index.d.ts.map +1 -1
- package/dist/connector/index.js +1 -1
- package/dist/connector/index.js.map +1 -1
- package/dist/connector/mcp/adapter.d.ts.map +1 -1
- package/dist/connector/mcp/adapter.js +71 -16
- package/dist/connector/mcp/adapter.js.map +1 -1
- package/dist/connector/mcp/client.d.ts.map +1 -1
- package/dist/connector/mcp/client.js +3 -2
- package/dist/connector/mcp/client.js.map +1 -1
- package/dist/connector/mcp/http-redirect.d.ts +17 -0
- package/dist/connector/mcp/http-redirect.d.ts.map +1 -0
- package/dist/connector/mcp/http-redirect.js +31 -0
- package/dist/connector/mcp/http-redirect.js.map +1 -0
- package/dist/connector/mcp/http-sse.d.ts.map +1 -1
- package/dist/connector/mcp/http-sse.js +5 -0
- package/dist/connector/mcp/http-sse.js.map +1 -1
- package/dist/connector/mcp/image-admission.d.ts +16 -0
- package/dist/connector/mcp/image-admission.d.ts.map +1 -0
- package/dist/connector/mcp/image-admission.js +288 -0
- package/dist/connector/mcp/image-admission.js.map +1 -0
- package/dist/connector/mcp/stdio.d.ts +13 -0
- package/dist/connector/mcp/stdio.d.ts.map +1 -1
- package/dist/connector/mcp/stdio.js +66 -23
- package/dist/connector/mcp/stdio.js.map +1 -1
- package/dist/connector/mcp/streamable-http.d.ts.map +1 -1
- package/dist/connector/mcp/streamable-http.js +3 -0
- package/dist/connector/mcp/streamable-http.js.map +1 -1
- package/dist/constants/tools/index.js +1 -1
- package/dist/constants/tools/index.js.map +1 -1
- package/dist/execution/base.d.ts +13 -0
- package/dist/execution/base.d.ts.map +1 -1
- package/dist/execution/base.js +103 -8
- package/dist/execution/base.js.map +1 -1
- package/dist/execution/errors.d.ts +12 -0
- package/dist/execution/errors.d.ts.map +1 -0
- package/dist/execution/errors.js +21 -0
- package/dist/execution/errors.js.map +1 -0
- package/dist/execution/factory.d.ts.map +1 -1
- package/dist/execution/factory.js +1 -0
- package/dist/execution/factory.js.map +1 -1
- package/dist/execution/hybrid.d.ts +5 -0
- package/dist/execution/hybrid.d.ts.map +1 -1
- package/dist/execution/hybrid.js +36 -16
- package/dist/execution/hybrid.js.map +1 -1
- package/dist/execution/index.d.ts +1 -0
- package/dist/execution/index.d.ts.map +1 -1
- package/dist/execution/index.js +1 -0
- package/dist/execution/index.js.map +1 -1
- package/dist/execution/local.d.ts +13 -0
- package/dist/execution/local.d.ts.map +1 -1
- package/dist/execution/local.js +145 -19
- package/dist/execution/local.js.map +1 -1
- package/dist/execution/output.d.ts +27 -0
- package/dist/execution/output.d.ts.map +1 -0
- package/dist/execution/output.js +105 -0
- package/dist/execution/output.js.map +1 -0
- package/dist/execution/remote.d.ts +26 -2
- package/dist/execution/remote.d.ts.map +1 -1
- package/dist/execution/remote.js +86 -15
- package/dist/execution/remote.js.map +1 -1
- package/dist/plugin/lifecycle.d.ts.map +1 -1
- package/dist/plugin/lifecycle.js +9 -3
- package/dist/plugin/lifecycle.js.map +1 -1
- package/dist/process/kill-tree.d.ts +11 -13
- package/dist/process/kill-tree.d.ts.map +1 -1
- package/dist/process/kill-tree.js +11 -13
- package/dist/process/kill-tree.js.map +1 -1
- package/dist/provider/fallback.d.ts.map +1 -1
- package/dist/provider/fallback.js +31 -0
- package/dist/provider/fallback.js.map +1 -1
- package/dist/provider/idle-timeout.d.ts.map +1 -1
- package/dist/provider/idle-timeout.js +5 -0
- package/dist/provider/idle-timeout.js.map +1 -1
- package/dist/provider/retry.d.ts.map +1 -1
- package/dist/provider/retry.js +5 -0
- package/dist/provider/retry.js.map +1 -1
- package/dist/public-runtime.d.ts +2 -2
- package/dist/public-runtime.d.ts.map +1 -1
- package/dist/public-runtime.js +2 -2
- package/dist/public-runtime.js.map +1 -1
- package/dist/registry/tool/execute.d.ts +7 -1
- package/dist/registry/tool/execute.d.ts.map +1 -1
- package/dist/registry/tool/execute.js +201 -54
- package/dist/registry/tool/execute.js.map +1 -1
- package/dist/run/command-gate.d.ts.map +1 -1
- package/dist/run/command-gate.js +11 -0
- package/dist/run/command-gate.js.map +1 -1
- package/dist/run/workspace-fingerprint.d.ts +4 -4
- package/dist/run/workspace-fingerprint.d.ts.map +1 -1
- package/dist/run/workspace-fingerprint.js +6 -4
- package/dist/run/workspace-fingerprint.js.map +1 -1
- package/dist/runtime/bidi/index.d.ts +1 -1
- package/dist/runtime/bidi/index.d.ts.map +1 -1
- package/dist/runtime/bidi/index.js +1 -1
- package/dist/runtime/bidi/index.js.map +1 -1
- package/dist/runtime/bidi/session.d.ts +19 -0
- package/dist/runtime/bidi/session.d.ts.map +1 -1
- package/dist/runtime/bidi/session.js +150 -26
- package/dist/runtime/bidi/session.js.map +1 -1
- package/dist/runtime/query/executor.d.ts +43 -1
- package/dist/runtime/query/executor.d.ts.map +1 -1
- package/dist/runtime/query/executor.js +626 -161
- package/dist/runtime/query/executor.js.map +1 -1
- package/dist/runtime/query/index.d.ts.map +1 -1
- package/dist/runtime/query/index.js +46 -4
- package/dist/runtime/query/index.js.map +1 -1
- package/dist/runtime/query/iteration/phases/tool-review.d.ts.map +1 -1
- package/dist/runtime/query/iteration/phases/tool-review.js +84 -22
- package/dist/runtime/query/iteration/phases/tool-review.js.map +1 -1
- package/dist/runtime/query/request-rich-content.d.ts.map +1 -1
- package/dist/runtime/query/request-rich-content.js +33 -13
- package/dist/runtime/query/request-rich-content.js.map +1 -1
- package/dist/runtime/query/resume-pending.d.ts +5 -1
- package/dist/runtime/query/resume-pending.d.ts.map +1 -1
- package/dist/runtime/query/resume-pending.js +66 -1
- package/dist/runtime/query/resume-pending.js.map +1 -1
- package/dist/runtime/query/tool-output-budget.d.ts.map +1 -1
- package/dist/runtime/query/tool-output-budget.js +45 -16
- package/dist/runtime/query/tool-output-budget.js.map +1 -1
- package/dist/runtime/query/tooling.d.ts +6 -0
- package/dist/runtime/query/tooling.d.ts.map +1 -1
- package/dist/runtime/query/tooling.js +4 -0
- package/dist/runtime/query/tooling.js.map +1 -1
- package/dist/sandbox/provider/local.d.ts.map +1 -1
- package/dist/sandbox/provider/local.js +193 -41
- package/dist/sandbox/provider/local.js.map +1 -1
- package/dist/scheduler/delegating.d.ts +2 -1
- package/dist/scheduler/delegating.d.ts.map +1 -1
- package/dist/scheduler/delegating.js +6 -5
- package/dist/scheduler/delegating.js.map +1 -1
- package/dist/scheduler/local.d.ts +2 -1
- package/dist/scheduler/local.d.ts.map +1 -1
- package/dist/scheduler/local.js +2 -2
- package/dist/scheduler/local.js.map +1 -1
- package/dist/session/workspace/driver.d.ts +12 -4
- package/dist/session/workspace/driver.d.ts.map +1 -1
- package/dist/session/workspace/git-worktree.d.ts +17 -3
- package/dist/session/workspace/git-worktree.d.ts.map +1 -1
- package/dist/session/workspace/git-worktree.js +153 -34
- package/dist/session/workspace/git-worktree.js.map +1 -1
- package/dist/skills/registry.d.ts +15 -0
- package/dist/skills/registry.d.ts.map +1 -1
- package/dist/skills/registry.js +33 -0
- package/dist/skills/registry.js.map +1 -1
- package/dist/tools/builtins/computer-use.d.ts.map +1 -1
- package/dist/tools/builtins/computer-use.js +39 -2
- package/dist/tools/builtins/computer-use.js.map +1 -1
- package/dist/tools/builtins/run-code.d.ts.map +1 -1
- package/dist/tools/builtins/run-code.js +9 -11
- package/dist/tools/builtins/run-code.js.map +1 -1
- package/dist/tools/builtins/skill.d.ts +2 -1
- package/dist/tools/builtins/skill.d.ts.map +1 -1
- package/dist/tools/builtins/skill.js +250 -6
- package/dist/tools/builtins/skill.js.map +1 -1
- package/dist/tools/coordinator/agent.d.ts.map +1 -1
- package/dist/tools/coordinator/agent.js +100 -30
- package/dist/tools/coordinator/agent.js.map +1 -1
- package/dist/types/agent/manager.d.ts +1 -1
- package/dist/types/agent/manager.d.ts.map +1 -1
- package/dist/types/agent/scheduler.d.ts +10 -1
- package/dist/types/agent/scheduler.d.ts.map +1 -1
- package/dist/types/authorization/index.d.ts +12 -12
- package/dist/types/bidi/index.d.ts +19 -2
- package/dist/types/bidi/index.d.ts.map +1 -1
- package/dist/types/computer-use/index.d.ts +18 -0
- package/dist/types/computer-use/index.d.ts.map +1 -1
- package/dist/types/connector/execution-context.d.ts +3 -1
- package/dist/types/connector/execution-context.d.ts.map +1 -1
- package/dist/types/execution/index.d.ts +43 -1
- package/dist/types/execution/index.d.ts.map +1 -1
- package/dist/types/hitl/index.d.ts +11 -0
- package/dist/types/hitl/index.d.ts.map +1 -1
- package/dist/types/hitl/index.js.map +1 -1
- package/dist/types/message/index.d.ts +1 -1
- package/dist/types/message/index.d.ts.map +1 -1
- package/dist/types/message/index.js +2 -1
- package/dist/types/message/index.js.map +1 -1
- package/dist/types/plugin/index.d.ts +14 -1
- package/dist/types/plugin/index.d.ts.map +1 -1
- package/dist/types/plugin/index.js +1 -0
- package/dist/types/plugin/index.js.map +1 -1
- package/dist/types/provider/interface.d.ts +14 -0
- package/dist/types/provider/interface.d.ts.map +1 -1
- package/dist/types/sandbox/index.d.ts +15 -10
- package/dist/types/sandbox/index.d.ts.map +1 -1
- package/dist/types/sandbox/index.js.map +1 -1
- package/dist/types/tool/index.d.ts +77 -6
- package/dist/types/tool/index.d.ts.map +1 -1
- package/dist/types/tool/index.js.map +1 -1
- package/dist/types/workspace/ref.d.ts +5 -0
- package/dist/types/workspace/ref.d.ts.map +1 -1
- package/dist/utils/abort.d.ts +8 -0
- package/dist/utils/abort.d.ts.map +1 -1
- package/dist/utils/abort.js +16 -0
- package/dist/utils/abort.js.map +1 -1
- package/package.json +4 -4
- package/src/authorization/gate.ts +1 -1
- package/src/connector/index.ts +2 -0
- package/src/connector/mcp/adapter.ts +82 -21
- package/src/connector/mcp/client.ts +2 -1
- package/src/connector/mcp/http-redirect.ts +32 -0
- package/src/connector/mcp/http-sse.ts +5 -0
- package/src/connector/mcp/image-admission.ts +272 -0
- package/src/connector/mcp/stdio.ts +63 -21
- package/src/connector/mcp/streamable-http.ts +3 -0
- package/src/constants/tools/index.ts +1 -1
- package/src/execution/base.ts +124 -7
- package/src/execution/errors.ts +24 -0
- package/src/execution/factory.ts +1 -0
- package/src/execution/hybrid.ts +47 -16
- package/src/execution/index.ts +1 -0
- package/src/execution/local.ts +164 -19
- package/src/execution/output.ts +104 -0
- package/src/execution/remote.ts +122 -17
- package/src/plugin/lifecycle.ts +9 -3
- package/src/process/kill-tree.ts +11 -13
- package/src/provider/fallback.ts +39 -0
- package/src/provider/idle-timeout.ts +8 -0
- package/src/provider/retry.ts +8 -0
- package/src/public-runtime.ts +7 -1
- package/src/registry/tool/execute.ts +230 -64
- package/src/run/command-gate.ts +12 -0
- package/src/run/workspace-fingerprint.ts +5 -4
- package/src/runtime/bidi/index.ts +1 -1
- package/src/runtime/bidi/session.ts +177 -28
- package/src/runtime/query/executor.ts +769 -163
- package/src/runtime/query/index.ts +53 -6
- package/src/runtime/query/iteration/phases/tool-review.ts +87 -23
- package/src/runtime/query/request-rich-content.ts +39 -14
- package/src/runtime/query/resume-pending.ts +83 -1
- package/src/runtime/query/tool-output-budget.ts +52 -17
- package/src/runtime/query/tooling.ts +10 -0
- package/src/sandbox/provider/local.ts +213 -45
- package/src/scheduler/delegating.ts +6 -5
- package/src/scheduler/local.ts +3 -2
- package/src/session/workspace/driver.ts +12 -4
- package/src/session/workspace/git-worktree.ts +173 -34
- package/src/skills/registry.ts +47 -0
- package/src/tools/builtins/computer-use.ts +45 -2
- package/src/tools/builtins/run-code.ts +9 -11
- package/src/tools/builtins/skill.ts +325 -7
- package/src/tools/coordinator/agent.ts +108 -33
- package/src/types/agent/manager.ts +1 -1
- package/src/types/agent/scheduler.ts +10 -1
- package/src/types/bidi/index.ts +19 -2
- package/src/types/computer-use/index.ts +19 -0
- package/src/types/connector/execution-context.ts +3 -0
- package/src/types/execution/index.ts +46 -1
- package/src/types/hitl/index.ts +11 -0
- package/src/types/message/index.ts +3 -2
- package/src/types/plugin/index.ts +15 -0
- package/src/types/provider/interface.ts +18 -0
- package/src/types/sandbox/index.ts +15 -10
- package/src/types/tool/index.ts +85 -6
- package/src/types/workspace/ref.ts +5 -0
- package/src/utils/abort.ts +18 -0
|
@@ -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 &&
|
|
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(
|
|
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
|
|
590
|
-
//
|
|
591
|
-
//
|
|
592
|
-
//
|
|
593
|
-
//
|
|
594
|
-
//
|
|
595
|
-
//
|
|
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(
|
|
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
|
-
): {
|
|
689
|
-
|
|
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
|
-
*
|
|
704
|
-
*
|
|
705
|
-
*
|
|
706
|
-
*
|
|
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
|
-
|
|
717
|
-
|
|
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:
|
|
726
|
-
|
|
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
|
|
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
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
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.
|
|
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
|
}
|
package/src/scheduler/local.ts
CHANGED
|
@@ -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
|
|
27
|
-
*
|
|
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
|