@gemslibe/rbo 0.3.0 → 0.4.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/dist/rbo-mcp-stdio.js +71 -16
- package/dist/rbo.js +322 -44
- package/package.json +2 -2
package/dist/rbo-mcp-stdio.js
CHANGED
|
@@ -7,7 +7,7 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
|
7
7
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
8
8
|
|
|
9
9
|
// ../../packages/shared/dist/versions.js
|
|
10
|
-
var RBO_STDIO_ADAPTER_VERSION = "0.
|
|
10
|
+
var RBO_STDIO_ADAPTER_VERSION = "0.4.0";
|
|
11
11
|
|
|
12
12
|
// ../../packages/shared/dist/errors.js
|
|
13
13
|
import { z } from "zod";
|
|
@@ -686,6 +686,21 @@ var ARTIFACT_MATERIALIZE_INPUT = {
|
|
|
686
686
|
var AGENT_PROBE_INPUT = {
|
|
687
687
|
agent_id: z4.string().min(1)
|
|
688
688
|
};
|
|
689
|
+
var JOB_RUN_INPUT = {
|
|
690
|
+
command: z4.string().min(1).optional(),
|
|
691
|
+
project_root: z4.string().min(1).optional(),
|
|
692
|
+
job_id: z4.string().min(1).optional(),
|
|
693
|
+
cwd: z4.string().default("."),
|
|
694
|
+
timeout_seconds: z4.number().positive().max(3600).default(3600),
|
|
695
|
+
wait_seconds: z4.number().int().min(0).max(3600).optional(),
|
|
696
|
+
/** Max seconds this MCP response blocks; default keeps typical 60s clients safe. */
|
|
697
|
+
mcp_wait_slice_seconds: z4.number().int().min(1).max(55).default(50),
|
|
698
|
+
artifacts: JobRequestSchema.shape.artifacts,
|
|
699
|
+
risk_level: JobRequestSchema.shape.risk_level,
|
|
700
|
+
client_request_id: z4.string().min(1).optional(),
|
|
701
|
+
name: z4.string().optional(),
|
|
702
|
+
include_log_tail_lines: z4.number().int().min(0).max(1e3).default(80)
|
|
703
|
+
};
|
|
689
704
|
var MCP_TOOL_DEFS = [
|
|
690
705
|
{
|
|
691
706
|
name: "agents_list",
|
|
@@ -697,6 +712,11 @@ var MCP_TOOL_DEFS = [
|
|
|
697
712
|
description: "Submit a build/test job and capture an immutable workspace snapshot.",
|
|
698
713
|
inputShape: JOB_SUBMIT_INPUT
|
|
699
714
|
},
|
|
715
|
+
{
|
|
716
|
+
name: "job_run",
|
|
717
|
+
description: "Run a command remotely (snapshot + execute). Waits up to mcp_wait_slice_seconds (default 50). If still running, returns resume:true \u2014 call again with the same job_id. Preferred for interactive AI clients.",
|
|
718
|
+
inputShape: JOB_RUN_INPUT
|
|
719
|
+
},
|
|
700
720
|
{
|
|
701
721
|
name: "job_confirm",
|
|
702
722
|
description: "Confirm a destructive or hardware-risk job after snapshot capture.",
|
|
@@ -761,6 +781,32 @@ var CompatibilityMatrixSchema = z5.object({
|
|
|
761
781
|
});
|
|
762
782
|
|
|
763
783
|
// ../mcp-stdio/src/proxy.ts
|
|
784
|
+
var STDIO_JOB_RUN_HEARTBEAT_MS = 5e3;
|
|
785
|
+
function startProgressHeartbeat(input) {
|
|
786
|
+
const { progressToken, sendNotification } = input;
|
|
787
|
+
if (progressToken === void 0) {
|
|
788
|
+
return { stop: () => void 0 };
|
|
789
|
+
}
|
|
790
|
+
let progress = 0;
|
|
791
|
+
const prefix = input.messagePrefix ?? "job_run";
|
|
792
|
+
const timer = setInterval(() => {
|
|
793
|
+
progress += 1;
|
|
794
|
+
void sendNotification({
|
|
795
|
+
method: "notifications/progress",
|
|
796
|
+
params: {
|
|
797
|
+
progressToken,
|
|
798
|
+
progress,
|
|
799
|
+
message: `${prefix} waiting (heartbeat ${progress})`
|
|
800
|
+
}
|
|
801
|
+
}).catch(() => void 0);
|
|
802
|
+
}, input.intervalMs ?? STDIO_JOB_RUN_HEARTBEAT_MS);
|
|
803
|
+
timer.unref?.();
|
|
804
|
+
return {
|
|
805
|
+
stop: () => {
|
|
806
|
+
clearInterval(timer);
|
|
807
|
+
}
|
|
808
|
+
};
|
|
809
|
+
}
|
|
764
810
|
function createStdioProxyServer(options) {
|
|
765
811
|
const server = new McpServer({
|
|
766
812
|
name: "rbo-mcp-stdio",
|
|
@@ -771,21 +817,30 @@ function createStdioProxyServer(options) {
|
|
|
771
817
|
server.registerTool(
|
|
772
818
|
def.name,
|
|
773
819
|
{ description: def.description, inputSchema: def.inputShape },
|
|
774
|
-
async (args) => {
|
|
775
|
-
const
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
820
|
+
async (args, extra) => {
|
|
821
|
+
const heartbeat = def.name === "job_run" ? startProgressHeartbeat({
|
|
822
|
+
progressToken: extra._meta?.progressToken,
|
|
823
|
+
sendNotification: (notification) => extra.sendNotification(notification)
|
|
824
|
+
}) : { stop: () => void 0 };
|
|
825
|
+
try {
|
|
826
|
+
const response = await fetch(`${baseUrl}/internal/v1/tools/${def.name}`, {
|
|
827
|
+
method: "POST",
|
|
828
|
+
headers: {
|
|
829
|
+
"content-type": "application/json",
|
|
830
|
+
"x-rbo-client-id": options.clientId,
|
|
831
|
+
"x-rbo-client-transport": "stdio"
|
|
832
|
+
},
|
|
833
|
+
body: JSON.stringify(args ?? {}),
|
|
834
|
+
signal: extra.signal
|
|
835
|
+
});
|
|
836
|
+
const text = await response.text();
|
|
837
|
+
return {
|
|
838
|
+
content: [{ type: "text", text }],
|
|
839
|
+
isError: !response.ok
|
|
840
|
+
};
|
|
841
|
+
} finally {
|
|
842
|
+
heartbeat.stop();
|
|
843
|
+
}
|
|
789
844
|
}
|
|
790
845
|
);
|
|
791
846
|
}
|
package/dist/rbo.js
CHANGED
|
@@ -14,9 +14,9 @@ var RBO_CONTROLLER_VERSION, RBO_AGENT_VERSION, RBO_STDIO_ADAPTER_VERSION, RBO_WI
|
|
|
14
14
|
var init_versions = __esm({
|
|
15
15
|
"../../packages/shared/dist/versions.js"() {
|
|
16
16
|
"use strict";
|
|
17
|
-
RBO_CONTROLLER_VERSION = "0.
|
|
18
|
-
RBO_AGENT_VERSION = "0.
|
|
19
|
-
RBO_STDIO_ADAPTER_VERSION = "0.
|
|
17
|
+
RBO_CONTROLLER_VERSION = "0.4.0";
|
|
18
|
+
RBO_AGENT_VERSION = "0.4.0";
|
|
19
|
+
RBO_STDIO_ADAPTER_VERSION = "0.4.0";
|
|
20
20
|
RBO_WIRE_PROTOCOL_MIN_VERSION = 1;
|
|
21
21
|
RBO_WIRE_PROTOCOL_MAX_VERSION = 1;
|
|
22
22
|
RBO_CONTROLLER_SCHEMA_VERSION = 3;
|
|
@@ -1636,7 +1636,7 @@ import { z as z4 } from "zod";
|
|
|
1636
1636
|
function getMcpToolDef(name) {
|
|
1637
1637
|
return MCP_TOOL_DEFS.find((def) => def.name === name);
|
|
1638
1638
|
}
|
|
1639
|
-
var JOB_SUBMIT_INPUT, JOB_CONFIRM_INPUT, AGENTS_LIST_INPUT, JOB_GET_INPUT, JOB_WAIT_INPUT, JOB_LOGS_INPUT, JOB_CANCEL_INPUT, JOB_ARTIFACTS_INPUT, ARTIFACT_MATERIALIZE_INPUT, AGENT_PROBE_INPUT, MCP_TOOL_DEFS;
|
|
1639
|
+
var JOB_SUBMIT_INPUT, JOB_CONFIRM_INPUT, AGENTS_LIST_INPUT, JOB_GET_INPUT, JOB_WAIT_INPUT, JOB_LOGS_INPUT, JOB_CANCEL_INPUT, JOB_ARTIFACTS_INPUT, ARTIFACT_MATERIALIZE_INPUT, AGENT_PROBE_INPUT, JOB_RUN_INPUT, MCP_TOOL_DEFS;
|
|
1640
1640
|
var init_mcp_tools = __esm({
|
|
1641
1641
|
"../../packages/protocol/dist/mcp-tools.js"() {
|
|
1642
1642
|
"use strict";
|
|
@@ -1680,6 +1680,21 @@ var init_mcp_tools = __esm({
|
|
|
1680
1680
|
AGENT_PROBE_INPUT = {
|
|
1681
1681
|
agent_id: z4.string().min(1)
|
|
1682
1682
|
};
|
|
1683
|
+
JOB_RUN_INPUT = {
|
|
1684
|
+
command: z4.string().min(1).optional(),
|
|
1685
|
+
project_root: z4.string().min(1).optional(),
|
|
1686
|
+
job_id: z4.string().min(1).optional(),
|
|
1687
|
+
cwd: z4.string().default("."),
|
|
1688
|
+
timeout_seconds: z4.number().positive().max(3600).default(3600),
|
|
1689
|
+
wait_seconds: z4.number().int().min(0).max(3600).optional(),
|
|
1690
|
+
/** Max seconds this MCP response blocks; default keeps typical 60s clients safe. */
|
|
1691
|
+
mcp_wait_slice_seconds: z4.number().int().min(1).max(55).default(50),
|
|
1692
|
+
artifacts: JobRequestSchema.shape.artifacts,
|
|
1693
|
+
risk_level: JobRequestSchema.shape.risk_level,
|
|
1694
|
+
client_request_id: z4.string().min(1).optional(),
|
|
1695
|
+
name: z4.string().optional(),
|
|
1696
|
+
include_log_tail_lines: z4.number().int().min(0).max(1e3).default(80)
|
|
1697
|
+
};
|
|
1683
1698
|
MCP_TOOL_DEFS = [
|
|
1684
1699
|
{
|
|
1685
1700
|
name: "agents_list",
|
|
@@ -1691,6 +1706,11 @@ var init_mcp_tools = __esm({
|
|
|
1691
1706
|
description: "Submit a build/test job and capture an immutable workspace snapshot.",
|
|
1692
1707
|
inputShape: JOB_SUBMIT_INPUT
|
|
1693
1708
|
},
|
|
1709
|
+
{
|
|
1710
|
+
name: "job_run",
|
|
1711
|
+
description: "Run a command remotely (snapshot + execute). Waits up to mcp_wait_slice_seconds (default 50). If still running, returns resume:true \u2014 call again with the same job_id. Preferred for interactive AI clients.",
|
|
1712
|
+
inputShape: JOB_RUN_INPUT
|
|
1713
|
+
},
|
|
1694
1714
|
{
|
|
1695
1715
|
name: "job_confirm",
|
|
1696
1716
|
description: "Confirm a destructive or hardware-risk job after snapshot capture.",
|
|
@@ -2067,11 +2087,11 @@ function describeWindowsExecutorResolution(options = {}) {
|
|
|
2067
2087
|
}
|
|
2068
2088
|
candidates.push(join11(moduleDir, WINDOWS_EXECUTOR_BINARY_NAME), join11(moduleDir, "..", WINDOWS_EXECUTOR_BINARY_NAME), join11(moduleDir, "..", "bin", WINDOWS_EXECUTOR_BINARY_NAME), join11(moduleDir, "../..", "bin", WINDOWS_EXECUTOR_BINARY_NAME));
|
|
2069
2089
|
candidates.push(
|
|
2070
|
-
join11(moduleDir, "../../../native/windows-executor/target/debug", WINDOWS_EXECUTOR_BINARY_NAME),
|
|
2071
2090
|
join11(moduleDir, "../../../native/windows-executor/target/release", WINDOWS_EXECUTOR_BINARY_NAME),
|
|
2091
|
+
join11(moduleDir, "../../../native/windows-executor/target/debug", WINDOWS_EXECUTOR_BINARY_NAME),
|
|
2072
2092
|
// When bundled into apps/cli/dist/rbo.js, climb to repo root.
|
|
2073
|
-
join11(moduleDir, "../../native/windows-executor/target/
|
|
2074
|
-
join11(moduleDir, "../../native/windows-executor/target/
|
|
2093
|
+
join11(moduleDir, "../../native/windows-executor/target/release", WINDOWS_EXECUTOR_BINARY_NAME),
|
|
2094
|
+
join11(moduleDir, "../../native/windows-executor/target/debug", WINDOWS_EXECUTOR_BINARY_NAME)
|
|
2075
2095
|
);
|
|
2076
2096
|
if (options.extraCandidates) {
|
|
2077
2097
|
candidates.push(...options.extraCandidates);
|
|
@@ -2178,8 +2198,11 @@ async function writeJobScript(controlDir, execution, scriptName) {
|
|
|
2178
2198
|
const scriptPath = join12(controlDir, fileName);
|
|
2179
2199
|
const isCleanup = scriptName === "cleanup.ps1" || scriptName === "cleanup.sh" || scriptName === "cleanup.cmd";
|
|
2180
2200
|
const scriptBody = isCleanup ? execution.cleanup_script ?? "" : execution.script;
|
|
2181
|
-
|
|
2201
|
+
let body = isDirect && process.platform !== "win32" && !scriptBody.startsWith("#!") ? `#!/usr/bin/env bash
|
|
2182
2202
|
${scriptBody}` : scriptBody;
|
|
2203
|
+
if (isPowerShell) {
|
|
2204
|
+
body = `${POWERSHELL_JOB_PRELUDE}${body}`;
|
|
2205
|
+
}
|
|
2183
2206
|
await writeFile6(scriptPath, body, "utf8");
|
|
2184
2207
|
if (process.platform !== "win32") {
|
|
2185
2208
|
await chmod(scriptPath, 493);
|
|
@@ -2525,7 +2548,7 @@ async function runCleanupScript(input) {
|
|
|
2525
2548
|
}
|
|
2526
2549
|
return { exitCode: result.exitCode, timedOut: false };
|
|
2527
2550
|
}
|
|
2528
|
-
var execFileAsync4, ManagedChildProcess, PHASE3_UNSUPPORTED_SHELLS;
|
|
2551
|
+
var execFileAsync4, ManagedChildProcess, PHASE3_UNSUPPORTED_SHELLS, POWERSHELL_JOB_PRELUDE;
|
|
2529
2552
|
var init_script = __esm({
|
|
2530
2553
|
"../../packages/executor/dist/script.js"() {
|
|
2531
2554
|
"use strict";
|
|
@@ -2555,6 +2578,13 @@ var init_script = __esm({
|
|
|
2555
2578
|
}
|
|
2556
2579
|
};
|
|
2557
2580
|
PHASE3_UNSUPPORTED_SHELLS = ["sh", "zsh", "cmd", "pwsh"];
|
|
2581
|
+
POWERSHELL_JOB_PRELUDE = [
|
|
2582
|
+
"# RBO PowerShell runtime prelude (injected; do not remove)",
|
|
2583
|
+
"$global:LASTEXITCODE = 0",
|
|
2584
|
+
"# --- end RBO prelude ---",
|
|
2585
|
+
"",
|
|
2586
|
+
""
|
|
2587
|
+
].join("\n");
|
|
2558
2588
|
}
|
|
2559
2589
|
});
|
|
2560
2590
|
|
|
@@ -7383,6 +7413,18 @@ async function handleRemoteLogChunk(opts, agentId, payload) {
|
|
|
7383
7413
|
updateAttempt(opts.db, attempt.id, { log_acked_sequence: payload.sequence });
|
|
7384
7414
|
sendAck();
|
|
7385
7415
|
}
|
|
7416
|
+
function enqueueRemoteLogChunk(opts, agentId, payload) {
|
|
7417
|
+
const key = payload.attempt_id;
|
|
7418
|
+
const prev = logChunkChains.get(key) ?? Promise.resolve();
|
|
7419
|
+
const next = prev.catch(() => void 0).then(() => handleRemoteLogChunk(opts, agentId, payload));
|
|
7420
|
+
logChunkChains.set(key, next);
|
|
7421
|
+
void next.finally(() => {
|
|
7422
|
+
if (logChunkChains.get(key) === next) {
|
|
7423
|
+
logChunkChains.delete(key);
|
|
7424
|
+
}
|
|
7425
|
+
});
|
|
7426
|
+
return next;
|
|
7427
|
+
}
|
|
7386
7428
|
function handleRemoteJobStarted(opts, agentId, payload) {
|
|
7387
7429
|
const attempt = loadAttemptByLease(opts.db, payload.attempt_id, payload.lease_id, payload.lease_epoch);
|
|
7388
7430
|
if (!rejectStale("job_started", attempt, agentId, ["running"])) {
|
|
@@ -7496,7 +7538,7 @@ function handleRemoteCleanupComplete(opts, agentId, payload) {
|
|
|
7496
7538
|
result_json: JSON.stringify({ outcome, exit_code: exitCode })
|
|
7497
7539
|
});
|
|
7498
7540
|
}
|
|
7499
|
-
var execFileAsync8, logger11, DEFAULT_LEASE_TTL_SECONDS, GitBundleSizeExceededError;
|
|
7541
|
+
var execFileAsync8, logger11, DEFAULT_LEASE_TTL_SECONDS, GitBundleSizeExceededError, logChunkChains;
|
|
7500
7542
|
var init_remote_execution = __esm({
|
|
7501
7543
|
"../controller/dist/execution/remote-execution.js"() {
|
|
7502
7544
|
"use strict";
|
|
@@ -7523,6 +7565,7 @@ var init_remote_execution = __esm({
|
|
|
7523
7565
|
this.name = "GitBundleSizeExceededError";
|
|
7524
7566
|
}
|
|
7525
7567
|
};
|
|
7568
|
+
logChunkChains = /* @__PURE__ */ new Map();
|
|
7526
7569
|
}
|
|
7527
7570
|
});
|
|
7528
7571
|
|
|
@@ -8329,7 +8372,7 @@ async function handleJobConfirm(ctx, args) {
|
|
|
8329
8372
|
});
|
|
8330
8373
|
return { job_id: job.id, state: "queued" };
|
|
8331
8374
|
}
|
|
8332
|
-
async function waitForJob(ctx, jobId, waitSeconds, includeLogTailLines) {
|
|
8375
|
+
async function waitForJob(ctx, jobId, waitSeconds, includeLogTailLines, options) {
|
|
8333
8376
|
const deadline = Date.now() + waitSeconds * 1e3;
|
|
8334
8377
|
let job = getJob(ctx.db, jobId);
|
|
8335
8378
|
if (!job) {
|
|
@@ -8338,6 +8381,9 @@ async function waitForJob(ctx, jobId, waitSeconds, includeLogTailLines) {
|
|
|
8338
8381
|
};
|
|
8339
8382
|
}
|
|
8340
8383
|
while (job && !isTerminalJobState(job.state) && Date.now() < deadline) {
|
|
8384
|
+
if (options?.onTick) {
|
|
8385
|
+
await options.onTick(job);
|
|
8386
|
+
}
|
|
8341
8387
|
await new Promise((resolvePromise) => setTimeout(resolvePromise, 200));
|
|
8342
8388
|
job = getJob(ctx.db, jobId);
|
|
8343
8389
|
}
|
|
@@ -10618,6 +10664,49 @@ async function streamDownloadWithLimits(source, destPath, expectedSize, expected
|
|
|
10618
10664
|
}
|
|
10619
10665
|
}
|
|
10620
10666
|
|
|
10667
|
+
// ../agent/dist/executor/tls-pin.js
|
|
10668
|
+
init_dist();
|
|
10669
|
+
import { Agent as HttpsAgent } from "node:https";
|
|
10670
|
+
var controllerDataPlaneAgent = new HttpsAgent({
|
|
10671
|
+
keepAlive: false,
|
|
10672
|
+
maxCachedSessions: 100
|
|
10673
|
+
});
|
|
10674
|
+
function pinnedTlsRequestOptions(expectedFingerprint) {
|
|
10675
|
+
return {
|
|
10676
|
+
rejectUnauthorized: false,
|
|
10677
|
+
agent: controllerDataPlaneAgent,
|
|
10678
|
+
checkServerIdentity: (_host, cert) => {
|
|
10679
|
+
if (!cert?.raw) {
|
|
10680
|
+
return void 0;
|
|
10681
|
+
}
|
|
10682
|
+
const actual = certificateFingerprint(cert.raw);
|
|
10683
|
+
if (actual !== expectedFingerprint) {
|
|
10684
|
+
return new Error(`Controller certificate fingerprint mismatch: expected ${expectedFingerprint}, got ${actual}`);
|
|
10685
|
+
}
|
|
10686
|
+
return void 0;
|
|
10687
|
+
}
|
|
10688
|
+
};
|
|
10689
|
+
}
|
|
10690
|
+
function assertPinnedPeerCert(res, expectedFingerprint) {
|
|
10691
|
+
const socket = res.socket;
|
|
10692
|
+
if (!socket) {
|
|
10693
|
+
return new Error("Controller did not present a TLS certificate");
|
|
10694
|
+
}
|
|
10695
|
+
if (socket.isSessionReused?.()) {
|
|
10696
|
+
return void 0;
|
|
10697
|
+
}
|
|
10698
|
+
const cert = socket.getPeerCertificate?.(true);
|
|
10699
|
+
const raw = cert?.raw ?? socket.getX509Certificate?.()?.raw;
|
|
10700
|
+
if (!raw) {
|
|
10701
|
+
return new Error("Controller did not present a TLS certificate");
|
|
10702
|
+
}
|
|
10703
|
+
const actual = certificateFingerprint(raw);
|
|
10704
|
+
if (actual !== expectedFingerprint) {
|
|
10705
|
+
return new Error(`Controller certificate fingerprint mismatch: expected ${expectedFingerprint}, got ${actual}`);
|
|
10706
|
+
}
|
|
10707
|
+
return void 0;
|
|
10708
|
+
}
|
|
10709
|
+
|
|
10621
10710
|
// ../agent/dist/executor/index.js
|
|
10622
10711
|
var logger4 = createLogger("agent.executor");
|
|
10623
10712
|
var ARTIFACT_TOKEN_TIMEOUT_MS = 6e4;
|
|
@@ -10639,18 +10728,6 @@ function resolveProjectIdentity(offer, prepare) {
|
|
|
10639
10728
|
}
|
|
10640
10729
|
return `local:${offer.snapshot_metadata.content_id}`;
|
|
10641
10730
|
}
|
|
10642
|
-
function assertPinnedPeerCert(res, expectedFingerprint) {
|
|
10643
|
-
const socket = res.socket;
|
|
10644
|
-
const cert = socket.getPeerCertificate?.(true);
|
|
10645
|
-
if (!cert?.raw) {
|
|
10646
|
-
return new Error("Controller did not present a TLS certificate");
|
|
10647
|
-
}
|
|
10648
|
-
const actual = certificateFingerprint(cert.raw);
|
|
10649
|
-
if (actual !== expectedFingerprint) {
|
|
10650
|
-
return new Error(`Controller certificate fingerprint mismatch: expected ${expectedFingerprint}, got ${actual}`);
|
|
10651
|
-
}
|
|
10652
|
-
return void 0;
|
|
10653
|
-
}
|
|
10654
10731
|
var AgentJobExecutor = class {
|
|
10655
10732
|
socket;
|
|
10656
10733
|
config;
|
|
@@ -11813,22 +11890,6 @@ var AgentJobExecutor = class {
|
|
|
11813
11890
|
this.sendFrame("artifact_manifest", attemptId, leaseId, leaseEpoch, manifest);
|
|
11814
11891
|
});
|
|
11815
11892
|
}
|
|
11816
|
-
pinnedTlsOptions() {
|
|
11817
|
-
const expected = this.config.controllerFingerprint;
|
|
11818
|
-
return {
|
|
11819
|
-
rejectUnauthorized: false,
|
|
11820
|
-
checkServerIdentity: (_host, cert) => {
|
|
11821
|
-
if (!cert.raw) {
|
|
11822
|
-
return new Error("Controller did not present a TLS certificate");
|
|
11823
|
-
}
|
|
11824
|
-
const actual = certificateFingerprint(cert.raw);
|
|
11825
|
-
if (actual !== expected) {
|
|
11826
|
-
return new Error(`Controller certificate fingerprint mismatch: expected ${expected}, got ${actual}`);
|
|
11827
|
-
}
|
|
11828
|
-
return void 0;
|
|
11829
|
-
}
|
|
11830
|
-
};
|
|
11831
|
-
}
|
|
11832
11893
|
downloadSnapshotFile(downloadUrl, dataToken, destPath, expectedSize, expectedSha256) {
|
|
11833
11894
|
return new Promise((resolvePromise, rejectPromise) => {
|
|
11834
11895
|
const req = httpsRequest(downloadUrl, {
|
|
@@ -11836,7 +11897,7 @@ var AgentJobExecutor = class {
|
|
|
11836
11897
|
headers: {
|
|
11837
11898
|
Authorization: `Bearer ${dataToken}`
|
|
11838
11899
|
},
|
|
11839
|
-
...this.
|
|
11900
|
+
...pinnedTlsRequestOptions(this.config.controllerFingerprint)
|
|
11840
11901
|
}, (res) => {
|
|
11841
11902
|
const pinError = assertPinnedPeerCert(res, this.config.controllerFingerprint);
|
|
11842
11903
|
if (pinError) {
|
|
@@ -11865,7 +11926,7 @@ var AgentJobExecutor = class {
|
|
|
11865
11926
|
"X-RBO-SHA256": sha2562,
|
|
11866
11927
|
"X-RBO-Artifact-Name": logicalName
|
|
11867
11928
|
},
|
|
11868
|
-
...this.
|
|
11929
|
+
...pinnedTlsRequestOptions(this.config.controllerFingerprint)
|
|
11869
11930
|
}, (res) => {
|
|
11870
11931
|
const pinError = assertPinnedPeerCert(res, this.config.controllerFingerprint);
|
|
11871
11932
|
if (pinError) {
|
|
@@ -12884,6 +12945,189 @@ function listAgents(db, includeOffline) {
|
|
|
12884
12945
|
// ../controller/dist/mcp/handlers.js
|
|
12885
12946
|
init_artifacts2();
|
|
12886
12947
|
init_runner();
|
|
12948
|
+
|
|
12949
|
+
// ../controller/dist/jobs/job-run.js
|
|
12950
|
+
init_dist2();
|
|
12951
|
+
init_dist();
|
|
12952
|
+
init_lifecycle();
|
|
12953
|
+
init_submit();
|
|
12954
|
+
var DEFAULT_MCP_WAIT_SLICE_SECONDS = 50;
|
|
12955
|
+
function wrapCommandAsExecution(command, timeoutSeconds, platform3 = process.platform) {
|
|
12956
|
+
if (platform3 === "win32") {
|
|
12957
|
+
return {
|
|
12958
|
+
shell: "powershell",
|
|
12959
|
+
script: [
|
|
12960
|
+
"$ErrorActionPreference = 'Stop'",
|
|
12961
|
+
command,
|
|
12962
|
+
"if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }"
|
|
12963
|
+
].join("\n"),
|
|
12964
|
+
timeout_seconds: timeoutSeconds
|
|
12965
|
+
};
|
|
12966
|
+
}
|
|
12967
|
+
return {
|
|
12968
|
+
shell: "bash",
|
|
12969
|
+
script: `set -euo pipefail
|
|
12970
|
+
${command}
|
|
12971
|
+
`,
|
|
12972
|
+
timeout_seconds: timeoutSeconds
|
|
12973
|
+
};
|
|
12974
|
+
}
|
|
12975
|
+
function deriveJobName(command) {
|
|
12976
|
+
const compact = command.replace(/\s+/g, " ").trim();
|
|
12977
|
+
if (compact.length <= 72) {
|
|
12978
|
+
return compact;
|
|
12979
|
+
}
|
|
12980
|
+
return `${compact.slice(0, 69)}...`;
|
|
12981
|
+
}
|
|
12982
|
+
function buildJobRunRequest(input, platform3 = process.platform) {
|
|
12983
|
+
if (!input.command || !input.project_root) {
|
|
12984
|
+
throw RboError.validation("job_run requires command and project_root unless job_id is set");
|
|
12985
|
+
}
|
|
12986
|
+
const timeoutSeconds = input.timeout_seconds ?? 3600;
|
|
12987
|
+
return JobRequestSchema.parse({
|
|
12988
|
+
client_request_id: input.client_request_id ?? generateId("req"),
|
|
12989
|
+
name: input.name ?? deriveJobName(input.command),
|
|
12990
|
+
source: {
|
|
12991
|
+
project_root: input.project_root,
|
|
12992
|
+
cwd: input.cwd ?? "."
|
|
12993
|
+
},
|
|
12994
|
+
execution: wrapCommandAsExecution(input.command, timeoutSeconds, platform3),
|
|
12995
|
+
risk_level: input.risk_level ?? "normal",
|
|
12996
|
+
artifacts: input.artifacts ?? []
|
|
12997
|
+
});
|
|
12998
|
+
}
|
|
12999
|
+
function summarizeJob(job) {
|
|
13000
|
+
if (!job) {
|
|
13001
|
+
return {
|
|
13002
|
+
state: null,
|
|
13003
|
+
outcome: null,
|
|
13004
|
+
exit_code: null,
|
|
13005
|
+
failure_category: null,
|
|
13006
|
+
failure_message: null
|
|
13007
|
+
};
|
|
13008
|
+
}
|
|
13009
|
+
return {
|
|
13010
|
+
state: job.state,
|
|
13011
|
+
outcome: job.outcome,
|
|
13012
|
+
exit_code: job.exit_code,
|
|
13013
|
+
failure_category: job.failure_category,
|
|
13014
|
+
failure_message: job.failure_message
|
|
13015
|
+
};
|
|
13016
|
+
}
|
|
13017
|
+
function resolveSliceSeconds(rawInput) {
|
|
13018
|
+
const slice = rawInput.mcp_wait_slice_seconds ?? DEFAULT_MCP_WAIT_SLICE_SECONDS;
|
|
13019
|
+
return Math.min(55, Math.max(1, slice));
|
|
13020
|
+
}
|
|
13021
|
+
function resolveWaitBudget(rawInput) {
|
|
13022
|
+
const timeoutSeconds = rawInput.timeout_seconds ?? 3600;
|
|
13023
|
+
const requested = rawInput.wait_seconds ?? timeoutSeconds;
|
|
13024
|
+
return Math.min(requested, resolveSliceSeconds(rawInput));
|
|
13025
|
+
}
|
|
13026
|
+
async function finishJobRunResponse(ctx, jobId, waitResult) {
|
|
13027
|
+
if ("error" in waitResult && waitResult.error) {
|
|
13028
|
+
return waitResult;
|
|
13029
|
+
}
|
|
13030
|
+
const job = waitResult.job;
|
|
13031
|
+
const terminal = job ? isTerminalJobState(job.state) : false;
|
|
13032
|
+
const artifacts = terminal && job ? handleJobArtifacts(ctx.db, jobId).artifacts ?? [] : [];
|
|
13033
|
+
return {
|
|
13034
|
+
job_id: jobId,
|
|
13035
|
+
...summarizeJob(job),
|
|
13036
|
+
resume: !terminal,
|
|
13037
|
+
log_tail: waitResult.log_tail ?? null,
|
|
13038
|
+
artifacts
|
|
13039
|
+
};
|
|
13040
|
+
}
|
|
13041
|
+
async function handleJobRun(ctx, rawInput, options = {}) {
|
|
13042
|
+
const includeLogTailLines = rawInput.include_log_tail_lines ?? 80;
|
|
13043
|
+
const waitBudget = resolveWaitBudget(rawInput);
|
|
13044
|
+
let progressCounter = 0;
|
|
13045
|
+
const startedAt = Date.now();
|
|
13046
|
+
let lastProgressAt = 0;
|
|
13047
|
+
const onTick = options.onProgress ? async (job) => {
|
|
13048
|
+
const now = Date.now();
|
|
13049
|
+
if (now - lastProgressAt < 5e3 && progressCounter > 0) {
|
|
13050
|
+
return;
|
|
13051
|
+
}
|
|
13052
|
+
lastProgressAt = now;
|
|
13053
|
+
progressCounter += 1;
|
|
13054
|
+
const elapsedSec = Math.round((now - startedAt) / 1e3);
|
|
13055
|
+
await options.onProgress?.({
|
|
13056
|
+
progress: progressCounter,
|
|
13057
|
+
message: `job_run state=${job.state} elapsed=${elapsedSec}s`
|
|
13058
|
+
});
|
|
13059
|
+
} : void 0;
|
|
13060
|
+
let jobId = rawInput.job_id?.trim() || "";
|
|
13061
|
+
if (jobId) {
|
|
13062
|
+
const existing = getJob(ctx.db, jobId);
|
|
13063
|
+
if (!existing) {
|
|
13064
|
+
return {
|
|
13065
|
+
error: {
|
|
13066
|
+
category: "validation",
|
|
13067
|
+
message: `Unknown job_id '${jobId}'`,
|
|
13068
|
+
retryable: false
|
|
13069
|
+
}
|
|
13070
|
+
};
|
|
13071
|
+
}
|
|
13072
|
+
if (existing.state === "awaiting_confirmation") {
|
|
13073
|
+
return {
|
|
13074
|
+
job_id: jobId,
|
|
13075
|
+
...summarizeJob(existing),
|
|
13076
|
+
resume: false,
|
|
13077
|
+
confirmation_required: true,
|
|
13078
|
+
log_tail: null,
|
|
13079
|
+
artifacts: []
|
|
13080
|
+
};
|
|
13081
|
+
}
|
|
13082
|
+
if (isTerminalJobState(existing.state)) {
|
|
13083
|
+
const artifactsResult = handleJobArtifacts(ctx.db, jobId);
|
|
13084
|
+
return {
|
|
13085
|
+
job_id: jobId,
|
|
13086
|
+
...summarizeJob(existing),
|
|
13087
|
+
resume: false,
|
|
13088
|
+
log_tail: null,
|
|
13089
|
+
artifacts: Array.isArray(artifactsResult.artifacts) ? artifactsResult.artifacts : []
|
|
13090
|
+
};
|
|
13091
|
+
}
|
|
13092
|
+
} else {
|
|
13093
|
+
if (!rawInput.command || !rawInput.project_root) {
|
|
13094
|
+
return {
|
|
13095
|
+
error: {
|
|
13096
|
+
category: "validation",
|
|
13097
|
+
message: "job_run requires command and project_root unless job_id is set",
|
|
13098
|
+
retryable: false
|
|
13099
|
+
}
|
|
13100
|
+
};
|
|
13101
|
+
}
|
|
13102
|
+
const request = buildJobRunRequest(rawInput);
|
|
13103
|
+
const submit = await handleJobSubmit(ctx, request);
|
|
13104
|
+
if ("error" in submit && submit.error) {
|
|
13105
|
+
return submit;
|
|
13106
|
+
}
|
|
13107
|
+
jobId = String(submit.job_id);
|
|
13108
|
+
if (submit.state === "awaiting_confirmation") {
|
|
13109
|
+
return {
|
|
13110
|
+
job_id: jobId,
|
|
13111
|
+
state: "awaiting_confirmation",
|
|
13112
|
+
confirmation_token: submit.confirmation_token,
|
|
13113
|
+
snapshot_id: submit.snapshot_id,
|
|
13114
|
+
content_id: submit.content_id,
|
|
13115
|
+
secret_warnings: submit.secret_warnings,
|
|
13116
|
+
outcome: null,
|
|
13117
|
+
exit_code: null,
|
|
13118
|
+
failure_category: null,
|
|
13119
|
+
failure_message: null,
|
|
13120
|
+
resume: false,
|
|
13121
|
+
log_tail: null,
|
|
13122
|
+
artifacts: []
|
|
13123
|
+
};
|
|
13124
|
+
}
|
|
13125
|
+
}
|
|
13126
|
+
const waitResult = await waitForJob(ctx, jobId, waitBudget, includeLogTailLines, { onTick });
|
|
13127
|
+
return finishJobRunResponse(ctx, jobId, waitResult);
|
|
13128
|
+
}
|
|
13129
|
+
|
|
13130
|
+
// ../controller/dist/mcp/handlers.js
|
|
12887
13131
|
init_lifecycle();
|
|
12888
13132
|
init_submit();
|
|
12889
13133
|
function runnerContext(ctx) {
|
|
@@ -12936,6 +13180,21 @@ async function handleToolCall(ctx, name, rawArgs) {
|
|
|
12936
13180
|
return { agents: listAgents(ctx.db, args.include_offline === true) };
|
|
12937
13181
|
case "job_submit":
|
|
12938
13182
|
return handleJobSubmit(submitContext(ctx), args);
|
|
13183
|
+
case "job_run":
|
|
13184
|
+
return handleJobRun(submitContext(ctx), {
|
|
13185
|
+
command: args.command,
|
|
13186
|
+
project_root: args.project_root,
|
|
13187
|
+
job_id: args.job_id,
|
|
13188
|
+
cwd: args.cwd,
|
|
13189
|
+
timeout_seconds: args.timeout_seconds,
|
|
13190
|
+
wait_seconds: args.wait_seconds,
|
|
13191
|
+
mcp_wait_slice_seconds: args.mcp_wait_slice_seconds,
|
|
13192
|
+
artifacts: args.artifacts,
|
|
13193
|
+
risk_level: args.risk_level,
|
|
13194
|
+
client_request_id: args.client_request_id,
|
|
13195
|
+
name: args.name,
|
|
13196
|
+
include_log_tail_lines: args.include_log_tail_lines
|
|
13197
|
+
}, ctx.jobRunOptions);
|
|
12939
13198
|
case "job_confirm":
|
|
12940
13199
|
return handleJobConfirm(submitContext(ctx), {
|
|
12941
13200
|
job_id: args.job_id,
|
|
@@ -13043,8 +13302,27 @@ function buildMcpServer(ctx) {
|
|
|
13043
13302
|
version: RBO_CONTROLLER_VERSION
|
|
13044
13303
|
});
|
|
13045
13304
|
for (const def of MCP_TOOL_DEFS) {
|
|
13046
|
-
server.registerTool(def.name, { description: def.description, inputSchema: def.inputShape }, async (args) => {
|
|
13047
|
-
const
|
|
13305
|
+
server.registerTool(def.name, { description: def.description, inputSchema: def.inputShape }, async (args, extra) => {
|
|
13306
|
+
const toolCtx = def.name === "job_run" ? {
|
|
13307
|
+
...ctx,
|
|
13308
|
+
jobRunOptions: {
|
|
13309
|
+
onProgress: async (update) => {
|
|
13310
|
+
const progressToken = extra._meta?.progressToken;
|
|
13311
|
+
if (progressToken === void 0) {
|
|
13312
|
+
return;
|
|
13313
|
+
}
|
|
13314
|
+
await extra.sendNotification({
|
|
13315
|
+
method: "notifications/progress",
|
|
13316
|
+
params: {
|
|
13317
|
+
progressToken,
|
|
13318
|
+
progress: update.progress,
|
|
13319
|
+
message: update.message
|
|
13320
|
+
}
|
|
13321
|
+
});
|
|
13322
|
+
}
|
|
13323
|
+
}
|
|
13324
|
+
} : ctx;
|
|
13325
|
+
const result = await handleToolCall(toolCtx, def.name, args);
|
|
13048
13326
|
return {
|
|
13049
13327
|
content: [{ type: "text", text: JSON.stringify(result) }]
|
|
13050
13328
|
};
|
|
@@ -13775,7 +14053,7 @@ async function startAgentPlaneServer(options) {
|
|
|
13775
14053
|
const payload = parsePayload(LogChunkPayloadSchema, message.payload, message.type);
|
|
13776
14054
|
if (!payload)
|
|
13777
14055
|
return;
|
|
13778
|
-
void
|
|
14056
|
+
void enqueueRemoteLogChunk(remoteOpts(), authenticated.agentId, payload);
|
|
13779
14057
|
return;
|
|
13780
14058
|
}
|
|
13781
14059
|
case "job_exit": {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gemslibe/rbo",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/rbo.js",
|
|
6
6
|
"bin": {
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
"zod": "^3.24.2"
|
|
36
36
|
},
|
|
37
37
|
"optionalDependencies": {
|
|
38
|
-
"@gemslibe/rbo-windows-executor-win32-x64": "0.
|
|
38
|
+
"@gemslibe/rbo-windows-executor-win32-x64": "0.4.0"
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|
|
41
41
|
"@rbo/agent": "workspace:*",
|