@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.
@@ -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.2.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 response = await fetch(`${baseUrl}/internal/v1/tools/${def.name}`, {
776
- method: "POST",
777
- headers: {
778
- "content-type": "application/json",
779
- "x-rbo-client-id": options.clientId,
780
- "x-rbo-client-transport": "stdio"
781
- },
782
- body: JSON.stringify(args ?? {})
783
- });
784
- const text = await response.text();
785
- return {
786
- content: [{ type: "text", text }],
787
- isError: !response.ok
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.2.0";
18
- RBO_AGENT_VERSION = "0.2.0";
19
- RBO_STDIO_ADAPTER_VERSION = "0.2.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/debug", WINDOWS_EXECUTOR_BINARY_NAME),
2074
- join11(moduleDir, "../../native/windows-executor/target/release", WINDOWS_EXECUTOR_BINARY_NAME)
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
- const body = isDirect && process.platform !== "win32" && !scriptBody.startsWith("#!") ? `#!/usr/bin/env bash
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.pinnedTlsOptions()
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.pinnedTlsOptions()
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 result = await handleToolCall(ctx, def.name, args);
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 handleRemoteLogChunk(remoteOpts(), authenticated.agentId, payload);
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.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.3.0"
38
+ "@gemslibe/rbo-windows-executor-win32-x64": "0.4.0"
39
39
  },
40
40
  "devDependencies": {
41
41
  "@rbo/agent": "workspace:*",