@tbrandenburg/node-red-agents 0.1.3 → 0.1.5
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/nodes/agent/agent.js +401 -368
- package/nodes/agent/lib/agents/base.js +31 -31
- package/nodes/agent/lib/agents/opencode.js +127 -120
- package/nodes/agent/lib/agents/pi.js +195 -175
- package/nodes/agent/lib/execution/lifecycle.js +41 -41
- package/nodes/agent/lib/execution/scheduler.js +74 -73
- package/nodes/agent/lib/execution/status.js +10 -10
- package/nodes/agent/lib/mcp/normalize.js +16 -16
- package/nodes/agent/lib/runtimes/base.js +10 -10
- package/nodes/agent/lib/runtimes/direct.js +19 -19
- package/nodes/agent/lib/runtimes/process-exec.js +71 -71
- package/nodes/agent/lib/runtimes/srt.js +40 -40
- package/nodes/agent-server/agent-server.js +495 -458
- package/nodes/agent-server/lib/daemon.js +99 -84
- package/nodes/agent-server/lib/http.js +45 -43
- package/nodes/agent-server/lib/model.js +10 -8
- package/nodes/agent-server/lib/port.js +14 -14
- package/nodes/agent-server/lib/registry.js +54 -54
- package/nodes/agent-server/lib/status.js +6 -6
- package/nodes/gh/README.md +11 -11
- package/nodes/gh/examples/list-pull-requests.json +46 -46
- package/nodes/gh/examples/run-workflow.json +40 -40
- package/nodes/gh/gh.js +220 -211
- package/nodes/gh/lib/parse-args.js +43 -43
- package/package.json +1 -1
- package/shared/srt-settings.js +32 -29
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
"use strict";
|
|
2
2
|
|
|
3
3
|
// Framework-agnostic bounded-concurrency FIFO scheduler. Deliberately has no
|
|
4
4
|
// Node-RED dependency (same reasoning as lib/execution/lifecycle.js) so it's
|
|
@@ -9,89 +9,90 @@
|
|
|
9
9
|
// model already gives independent executions for free (see AGENTS fan-out
|
|
10
10
|
// spec); this class only adds the bound + the waiting line on top of that.
|
|
11
11
|
class ExecutionScheduler {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
12
|
+
constructor({ concurrency, onStart, onQueued, onSettled } = {}) {
|
|
13
|
+
this.concurrency =
|
|
14
|
+
Number.isFinite(concurrency) && concurrency > 0 ? Math.floor(concurrency) : 1;
|
|
15
|
+
this.onStart = onStart; // (item) => Promise -- required
|
|
16
|
+
this.onQueued = onQueued; // (item) => void -- optional
|
|
17
|
+
// Called after this item is removed from `active` AND after any
|
|
18
|
+
// queued item that became eligible to start has already been
|
|
19
|
+
// started -- i.e. the scheduler's own bookkeeping is fully
|
|
20
|
+
// settled, so a status render triggered from here is never stale.
|
|
21
|
+
this.onSettled = onSettled; // (item) => void -- optional
|
|
22
|
+
this.queue = [];
|
|
23
|
+
this.active = new Map();
|
|
24
|
+
}
|
|
24
25
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
26
|
+
get activeCount() {
|
|
27
|
+
return this.active.size;
|
|
28
|
+
}
|
|
28
29
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
30
|
+
get queuedCount() {
|
|
31
|
+
return this.queue.length;
|
|
32
|
+
}
|
|
32
33
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
34
|
+
activeIds() {
|
|
35
|
+
return Array.from(this.active.keys());
|
|
36
|
+
}
|
|
36
37
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
}
|
|
38
|
+
// item must have a unique `executionId` property; anything else on it
|
|
39
|
+
// is opaque to the scheduler (agent.js stores msg/send/done/resolved).
|
|
40
|
+
submit(item) {
|
|
41
|
+
if (this.active.size < this.concurrency) {
|
|
42
|
+
this._start(item);
|
|
43
|
+
} else {
|
|
44
|
+
this.queue.push(item);
|
|
45
|
+
if (this.onQueued) this.onQueued(item);
|
|
46
46
|
}
|
|
47
|
+
}
|
|
47
48
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
49
|
+
_start(item) {
|
|
50
|
+
this.active.set(item.executionId, item);
|
|
51
|
+
Promise.resolve(this.onStart(item)).finally(() => {
|
|
52
|
+
this.active.delete(item.executionId);
|
|
53
|
+
this._advance();
|
|
54
|
+
if (this.onSettled) this.onSettled(item);
|
|
55
|
+
});
|
|
56
|
+
}
|
|
56
57
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
}
|
|
58
|
+
_advance() {
|
|
59
|
+
while (this.active.size < this.concurrency && this.queue.length > 0) {
|
|
60
|
+
this._start(this.queue.shift());
|
|
61
61
|
}
|
|
62
|
+
}
|
|
62
63
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
64
|
+
// Removes every still-queued item (FIFO order) without ever starting
|
|
65
|
+
// them, calling onCancel for each. Used on node close/redeploy so
|
|
66
|
+
// queued-but-not-yet-started messages get a clean done() instead of
|
|
67
|
+
// hanging forever. Does not touch active executions -- that's the
|
|
68
|
+
// caller's responsibility (terminate() belongs to the runtime layer).
|
|
69
|
+
drainQueue(onCancel) {
|
|
70
|
+
const remaining = this.queue.splice(0, this.queue.length);
|
|
71
|
+
if (onCancel) remaining.forEach((item) => onCancel(item));
|
|
72
|
+
return remaining;
|
|
73
|
+
}
|
|
73
74
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
}
|
|
90
|
-
const idx = this.queue.findIndex((item) => item.executionId === executionId);
|
|
91
|
-
if (idx === -1) return null;
|
|
92
|
-
const [item] = this.queue.splice(idx, 1);
|
|
93
|
-
return { status: 'queued', item };
|
|
75
|
+
// On-demand single-item cancellation (the `terminate` operation), as
|
|
76
|
+
// opposed to drainQueue's "everything, node is closing" semantics.
|
|
77
|
+
// Returns:
|
|
78
|
+
// { status: 'active', item } -- still running; caller must kill the
|
|
79
|
+
// actual process/runtime itself, this
|
|
80
|
+
// scheduler has no handle on that.
|
|
81
|
+
// { status: 'queued', item } -- removed from the queue before it ever
|
|
82
|
+
// started; caller must still settle the
|
|
83
|
+
// item's own done()/send() itself, same
|
|
84
|
+
// as drainQueue's onCancel.
|
|
85
|
+
// null -- unknown executionId (already finished,
|
|
86
|
+
// or never existed).
|
|
87
|
+
cancel(executionId) {
|
|
88
|
+
if (this.active.has(executionId)) {
|
|
89
|
+
return { status: "active", item: this.active.get(executionId) };
|
|
94
90
|
}
|
|
91
|
+
const idx = this.queue.findIndex((item) => item.executionId === executionId);
|
|
92
|
+
if (idx === -1) return null;
|
|
93
|
+
const [item] = this.queue.splice(idx, 1);
|
|
94
|
+
return { status: "queued", item };
|
|
95
|
+
}
|
|
95
96
|
}
|
|
96
97
|
|
|
97
98
|
module.exports = { ExecutionScheduler };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
"use strict";
|
|
2
2
|
|
|
3
3
|
// Pure function computing the node.status() shape from current scheduler
|
|
4
4
|
// counts plus the last terminal outcome. Kept separate from agent.js so the
|
|
@@ -8,17 +8,17 @@
|
|
|
8
8
|
// lastTerminal: undefined (never run) | 'completed' | 'failed' | 'timeout'
|
|
9
9
|
// lastText: optional override for the failed-state text (e.g. 'bad config')
|
|
10
10
|
function computeNodeStatus({ active, queued, lastTerminal, lastText }) {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
11
|
+
if (active > 0 || queued > 0) {
|
|
12
|
+
let text = `${active} running`;
|
|
13
|
+
if (queued > 0) text += ` \u00b7 ${queued} queued`;
|
|
14
|
+
return { fill: "blue", shape: "dot", text };
|
|
15
|
+
}
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
17
|
+
if (lastTerminal === "completed") return { fill: "green", shape: "dot", text: "completed" };
|
|
18
|
+
if (lastTerminal === "failed") return { fill: "red", shape: "ring", text: lastText || "failed" };
|
|
19
|
+
if (lastTerminal === "timeout") return { fill: "yellow", shape: "ring", text: "timeout" };
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
return {}; // idle, never run -- no status
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
module.exports = { computeNodeStatus };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
"use strict";
|
|
2
2
|
|
|
3
3
|
// Generic mcpServers[] (see AGENTS node schema) -> OpenCode's keyed `mcp`
|
|
4
4
|
// config object, as verified against opencode's real config schema:
|
|
@@ -7,25 +7,25 @@
|
|
|
7
7
|
//
|
|
8
8
|
// Each agent adapter owns its own translation; this module is OpenCode's.
|
|
9
9
|
function toOpenCodeMcp(mcpServers) {
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
const out = {};
|
|
11
|
+
if (!Array.isArray(mcpServers)) return out;
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
for (const server of mcpServers) {
|
|
14
|
+
if (!server || typeof server.name !== "string" || !server.name.trim()) continue;
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
}
|
|
24
|
-
// Unknown types are silently skipped -- validate() at the adapter
|
|
25
|
-
// level is responsible for surfacing a clear error before execution.
|
|
16
|
+
if (server.type === "remote") {
|
|
17
|
+
if (typeof server.url !== "string" || !server.url.trim()) continue;
|
|
18
|
+
out[server.name] = { type: "remote", url: server.url, enabled: true };
|
|
19
|
+
} else if (server.type === "local") {
|
|
20
|
+
if (typeof server.command !== "string" || !server.command.trim()) continue;
|
|
21
|
+
const args = Array.isArray(server.args) ? server.args : [];
|
|
22
|
+
out[server.name] = { type: "local", command: [server.command, ...args], enabled: true };
|
|
26
23
|
}
|
|
24
|
+
// Unknown types are silently skipped -- validate() at the adapter
|
|
25
|
+
// level is responsible for surfacing a clear error before execution.
|
|
26
|
+
}
|
|
27
27
|
|
|
28
|
-
|
|
28
|
+
return out;
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
module.exports = { toOpenCodeMcp };
|
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
|
|
1
|
+
"use strict";
|
|
2
2
|
|
|
3
3
|
// Generic interface every runtime (Direct, SRT, and later Daytona/OpenShell)
|
|
4
4
|
// must implement. Agent adapters never depend on a runtime directly, and
|
|
5
5
|
// runtimes never depend on an agent adapter -- they only see the normalized
|
|
6
6
|
// executionRequest built by lib/execution/lifecycle.js.
|
|
7
7
|
class RuntimeProvider {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
8
|
+
// executionRequest: { id, command, args, cwd, env, timeoutMs }
|
|
9
|
+
// handlers: { onLine(line: string), onExit(...) } -- onExit is not
|
|
10
|
+
// called directly by implementations; execute() resolves instead with
|
|
11
|
+
// { exitCode, signal, stderr, timedOut }.
|
|
12
|
+
async execute(_executionRequest, _handlers) {
|
|
13
|
+
throw new Error("RuntimeProvider.execute() not implemented");
|
|
14
|
+
}
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
async terminate(_executionId) {}
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
async cleanup(_executionId) {}
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
module.exports = { RuntimeProvider };
|
|
@@ -1,28 +1,28 @@
|
|
|
1
|
-
|
|
1
|
+
"use strict";
|
|
2
2
|
|
|
3
|
-
const { RuntimeProvider } = require(
|
|
4
|
-
const { runProcess, terminate } = require(
|
|
3
|
+
const { RuntimeProvider } = require("./base");
|
|
4
|
+
const { runProcess, terminate } = require("./process-exec");
|
|
5
5
|
|
|
6
6
|
// The portability baseline: runs the agent CLI directly as a child process
|
|
7
7
|
// wherever it's on PATH (laptop, GitHub runner, Kubernetes container, ...).
|
|
8
8
|
class DirectRuntime extends RuntimeProvider {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
9
|
+
async execute(executionRequest, handlers) {
|
|
10
|
+
return runProcess(
|
|
11
|
+
{
|
|
12
|
+
id: executionRequest.id,
|
|
13
|
+
cmd: executionRequest.command,
|
|
14
|
+
args: executionRequest.args,
|
|
15
|
+
cwd: executionRequest.cwd,
|
|
16
|
+
env: executionRequest.env,
|
|
17
|
+
timeoutMs: executionRequest.timeoutMs,
|
|
18
|
+
},
|
|
19
|
+
handlers,
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
async terminate(executionId) {
|
|
24
|
+
terminate(executionId);
|
|
25
|
+
}
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
module.exports = { DirectRuntime };
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
|
|
1
|
+
"use strict";
|
|
2
2
|
|
|
3
3
|
// Shared spawn/timeout/kill logic used by both the Direct and SRT runtimes.
|
|
4
4
|
// Direct and SRT differ only in *which* command+args get spawned (see
|
|
5
5
|
// direct.js / srt.js) -- everything about process lifecycle, JSONL line
|
|
6
6
|
// buffering, and timeout handling lives here exactly once.
|
|
7
|
-
const { spawn } = require(
|
|
7
|
+
const { spawn } = require("child_process");
|
|
8
8
|
|
|
9
9
|
const GRACE_PERIOD_MS = 2000;
|
|
10
10
|
|
|
@@ -12,94 +12,94 @@ const GRACE_PERIOD_MS = 2000;
|
|
|
12
12
|
const registry = new Map();
|
|
13
13
|
|
|
14
14
|
function killProcessGroup(child, signal) {
|
|
15
|
-
|
|
15
|
+
if (!child || child.exitCode !== null || child.signalCode !== null) return;
|
|
16
|
+
try {
|
|
17
|
+
// `detached: true` (see runProcess) gives the child its own process
|
|
18
|
+
// group with pgid === child.pid, so `-pid` reaches the whole tree
|
|
19
|
+
// (e.g. srt's bwrap wrapper + the agent CLI it spawns), leaving no
|
|
20
|
+
// orphans -- verified manually against a real `srt`-wrapped process.
|
|
21
|
+
process.kill(-child.pid, signal);
|
|
22
|
+
} catch (err) {
|
|
16
23
|
try {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
// orphans -- verified manually against a real `srt`-wrapped process.
|
|
21
|
-
process.kill(-child.pid, signal);
|
|
22
|
-
} catch (err) {
|
|
23
|
-
try {
|
|
24
|
-
child.kill(signal);
|
|
25
|
-
} catch (_err) {
|
|
26
|
-
// Process likely already exited between the check above and here.
|
|
27
|
-
}
|
|
24
|
+
child.kill(signal);
|
|
25
|
+
} catch (_err) {
|
|
26
|
+
// Process likely already exited between the check above and here.
|
|
28
27
|
}
|
|
28
|
+
}
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
function runProcess(executionRequest, handlers = {}) {
|
|
32
|
-
|
|
32
|
+
const { id, cmd, args, cwd, env, timeoutMs } = executionRequest;
|
|
33
33
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
34
|
+
return new Promise((resolve, reject) => {
|
|
35
|
+
let child;
|
|
36
|
+
try {
|
|
37
|
+
child = spawn(cmd, args, {
|
|
38
|
+
cwd: cwd || undefined,
|
|
39
|
+
env: env || process.env,
|
|
40
|
+
detached: true,
|
|
41
|
+
// Agent CLIs (opencode, srt-wrapped or not) wait on stdin if
|
|
42
|
+
// it's left open as a pipe; close it so they behave like a
|
|
43
|
+
// normal non-interactive invocation (see AGENTS.md).
|
|
44
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
45
|
+
});
|
|
46
|
+
} catch (err) {
|
|
47
|
+
reject(err);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
50
|
|
|
51
|
-
|
|
52
|
-
|
|
51
|
+
const state = { child, timeoutTimer: null, killTimer: null, timedOut: false };
|
|
52
|
+
registry.set(id, state);
|
|
53
53
|
|
|
54
|
-
|
|
55
|
-
|
|
54
|
+
let lineBuffer = "";
|
|
55
|
+
let stderr = "";
|
|
56
56
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
57
|
+
child.stdout.on("data", (chunk) => {
|
|
58
|
+
lineBuffer += chunk.toString();
|
|
59
|
+
let idx;
|
|
60
|
+
while ((idx = lineBuffer.indexOf("\n")) >= 0) {
|
|
61
|
+
const line = lineBuffer.slice(0, idx);
|
|
62
|
+
lineBuffer = lineBuffer.slice(idx + 1);
|
|
63
|
+
if (handlers.onLine) handlers.onLine(line);
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
child.stderr.on("data", (chunk) => {
|
|
67
|
+
stderr += chunk.toString();
|
|
68
|
+
});
|
|
69
69
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
70
|
+
if (timeoutMs && timeoutMs > 0) {
|
|
71
|
+
state.timeoutTimer = setTimeout(() => {
|
|
72
|
+
state.timedOut = true;
|
|
73
|
+
killProcessGroup(child, "SIGTERM");
|
|
74
|
+
state.killTimer = setTimeout(() => killProcessGroup(child, "SIGKILL"), GRACE_PERIOD_MS);
|
|
75
|
+
}, timeoutMs);
|
|
76
|
+
}
|
|
77
77
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
78
|
+
child.on("error", (err) => {
|
|
79
|
+
cleanupTimers(state);
|
|
80
|
+
registry.delete(id);
|
|
81
|
+
reject(err);
|
|
82
|
+
});
|
|
83
83
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
});
|
|
84
|
+
child.on("close", (code, signal) => {
|
|
85
|
+
if (lineBuffer.length && handlers.onLine) handlers.onLine(lineBuffer);
|
|
86
|
+
cleanupTimers(state);
|
|
87
|
+
registry.delete(id);
|
|
88
|
+
resolve({ exitCode: code, signal, stderr, timedOut: state.timedOut, pid: child.pid });
|
|
90
89
|
});
|
|
90
|
+
});
|
|
91
91
|
}
|
|
92
92
|
|
|
93
93
|
function cleanupTimers(state) {
|
|
94
|
-
|
|
95
|
-
|
|
94
|
+
if (state.timeoutTimer) clearTimeout(state.timeoutTimer);
|
|
95
|
+
if (state.killTimer) clearTimeout(state.killTimer);
|
|
96
96
|
}
|
|
97
97
|
|
|
98
98
|
function terminate(id) {
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
99
|
+
const state = registry.get(id);
|
|
100
|
+
if (!state) return;
|
|
101
|
+
killProcessGroup(state.child, "SIGTERM");
|
|
102
|
+
state.killTimer = setTimeout(() => killProcessGroup(state.child, "SIGKILL"), GRACE_PERIOD_MS);
|
|
103
103
|
}
|
|
104
104
|
|
|
105
105
|
module.exports = { runProcess, terminate, killProcessGroup, GRACE_PERIOD_MS };
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
|
|
1
|
+
"use strict";
|
|
2
2
|
|
|
3
|
-
const { RuntimeProvider } = require(
|
|
4
|
-
const { runProcess, terminate } = require(
|
|
3
|
+
const { RuntimeProvider } = require("./base");
|
|
4
|
+
const { runProcess, terminate } = require("./process-exec");
|
|
5
5
|
|
|
6
|
-
const DEFAULT_BINARY =
|
|
6
|
+
const DEFAULT_BINARY = "srt";
|
|
7
7
|
|
|
8
8
|
// Wraps execution in Anthropic's `srt` (sandbox-runtime) CLI, which is
|
|
9
9
|
// already installed on this host as a plain binary. Verified empirically
|
|
@@ -19,45 +19,45 @@ const DEFAULT_BINARY = 'srt';
|
|
|
19
19
|
// top of the exact same process-exec.js used by Direct: no new npm
|
|
20
20
|
// dependency, no shell-quoting step, no separate process lifecycle code.
|
|
21
21
|
class SrtRuntime extends RuntimeProvider {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
22
|
+
constructor(options = {}) {
|
|
23
|
+
super();
|
|
24
|
+
this.binary = options.binary || DEFAULT_BINARY;
|
|
25
|
+
// Left undefined by default so `srt` falls back to its own default
|
|
26
|
+
// (~/.srt-settings.json) -- SRT policy is deliberately kept out of
|
|
27
|
+
// the Agent node's core schema (spec: "SRT-specific configuration
|
|
28
|
+
// should be hidden... implemented by the runtime adapter").
|
|
29
|
+
this.settingsPath = options.settingsPath || undefined;
|
|
30
|
+
this.extraArgs = Array.isArray(options.extraArgs) ? options.extraArgs : [];
|
|
31
|
+
}
|
|
32
32
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
33
|
+
buildCommand(executionRequest) {
|
|
34
|
+
const flags = [];
|
|
35
|
+
if (this.settingsPath) flags.push("-s", this.settingsPath);
|
|
36
|
+
flags.push(...this.extraArgs);
|
|
37
|
+
return {
|
|
38
|
+
cmd: this.binary,
|
|
39
|
+
args: [...flags, executionRequest.command, ...executionRequest.args],
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
42
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
43
|
+
async execute(executionRequest, handlers) {
|
|
44
|
+
const { cmd, args } = this.buildCommand(executionRequest);
|
|
45
|
+
return runProcess(
|
|
46
|
+
{
|
|
47
|
+
id: executionRequest.id,
|
|
48
|
+
cmd,
|
|
49
|
+
args,
|
|
50
|
+
cwd: executionRequest.cwd,
|
|
51
|
+
env: executionRequest.env,
|
|
52
|
+
timeoutMs: executionRequest.timeoutMs,
|
|
53
|
+
},
|
|
54
|
+
handlers,
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
57
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
58
|
+
async terminate(executionId) {
|
|
59
|
+
terminate(executionId);
|
|
60
|
+
}
|
|
61
61
|
}
|
|
62
62
|
|
|
63
63
|
module.exports = { SrtRuntime };
|