@gemslibe/rbo 0.2.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.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;
@@ -204,11 +204,11 @@ async function resolveContainedCwd(projectPath, cwd) {
204
204
  if (!isPathContained(projectPath, joined)) {
205
205
  throw new Error(`cwd escapes project root: ${cwd}`);
206
206
  }
207
- const { access: access4, mkdir: mkdir16 } = await import("node:fs/promises");
207
+ const { access: access4, mkdir: mkdir15 } = await import("node:fs/promises");
208
208
  try {
209
209
  await access4(joined);
210
210
  } catch {
211
- await mkdir16(joined, { recursive: true });
211
+ await mkdir15(joined, { recursive: true });
212
212
  }
213
213
  await assertRealPathContained(projectPath, joined);
214
214
  return resolveRealPath(joined);
@@ -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.",
@@ -2028,7 +2048,7 @@ var init_runtime_env = __esm({
2028
2048
  });
2029
2049
 
2030
2050
  // ../../packages/executor/dist/windows-executor-path.js
2031
- import { existsSync as existsSync2 } from "node:fs";
2051
+ import { existsSync as existsSync3 } from "node:fs";
2032
2052
  import { createRequire } from "node:module";
2033
2053
  import { dirname as dirname3, join as join11 } from "node:path";
2034
2054
  import { fileURLToPath } from "node:url";
@@ -2048,7 +2068,7 @@ function describeWindowsExecutorResolution(options = {}) {
2048
2068
  const env = options.env ?? process.env;
2049
2069
  const platform3 = options.platform ?? process.platform;
2050
2070
  const arch2 = options.arch ?? process.arch;
2051
- const exists = options.existsSyncFn ?? existsSync2;
2071
+ const exists = options.existsSyncFn ?? existsSync3;
2052
2072
  const moduleDir = options.moduleDir ?? dirname3(fileURLToPath(import.meta.url));
2053
2073
  const fromEnv = env.RBO_WINDOWS_EXECUTOR;
2054
2074
  if (fromEnv && exists(fromEnv)) {
@@ -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
 
@@ -4238,7 +4268,7 @@ var init_materialize = __esm({
4238
4268
  });
4239
4269
 
4240
4270
  // ../../packages/snapshot/dist/index.js
4241
- import { z as z6 } from "zod";
4271
+ import { z as z7 } from "zod";
4242
4272
  var Sha256HexSchema, ContentIdSchema, SafeRelativePathSchema, SnapshotFileEntrySchema, SnapshotWorkspaceSchema, SnapshotAdditionalRootSchema, SnapshotRepoSchema, payloadBase, FullSnapshotManifestSchema, GitOverlaySnapshotManifestSchema, SnapshotManifestSchema, SnapshotInstanceSchema;
4243
4273
  var init_dist3 = __esm({
4244
4274
  "../../packages/snapshot/dist/index.js"() {
@@ -4252,88 +4282,88 @@ var init_dist3 = __esm({
4252
4282
  init_capture();
4253
4283
  init_overlay();
4254
4284
  init_materialize();
4255
- Sha256HexSchema = z6.string().regex(/^[0-9a-f]{64}$/i);
4256
- ContentIdSchema = z6.string().regex(/^sha256:[0-9a-f]{64}$/i);
4257
- SafeRelativePathSchema = z6.string().min(1).refine((value) => isSafeRelativePath(value), {
4285
+ Sha256HexSchema = z7.string().regex(/^[0-9a-f]{64}$/i);
4286
+ ContentIdSchema = z7.string().regex(/^sha256:[0-9a-f]{64}$/i);
4287
+ SafeRelativePathSchema = z7.string().min(1).refine((value) => isSafeRelativePath(value), {
4258
4288
  message: "must be a relative path without '..', absolute, or UNC segments"
4259
4289
  });
4260
- SnapshotFileEntrySchema = z6.discriminatedUnion("type", [
4261
- z6.object({
4262
- path: z6.string().min(1),
4263
- type: z6.literal("file"),
4264
- mode: z6.enum(["100644", "100755"]),
4265
- size: z6.number().int().nonnegative(),
4290
+ SnapshotFileEntrySchema = z7.discriminatedUnion("type", [
4291
+ z7.object({
4292
+ path: z7.string().min(1),
4293
+ type: z7.literal("file"),
4294
+ mode: z7.enum(["100644", "100755"]),
4295
+ size: z7.number().int().nonnegative(),
4266
4296
  sha256: Sha256HexSchema
4267
4297
  }),
4268
- z6.object({
4269
- path: z6.string().min(1),
4270
- type: z6.literal("symlink"),
4271
- mode: z6.literal("120000"),
4272
- target: z6.string().min(1)
4298
+ z7.object({
4299
+ path: z7.string().min(1),
4300
+ type: z7.literal("symlink"),
4301
+ mode: z7.literal("120000"),
4302
+ target: z7.string().min(1)
4273
4303
  })
4274
4304
  ]);
4275
- SnapshotWorkspaceSchema = z6.object({
4305
+ SnapshotWorkspaceSchema = z7.object({
4276
4306
  main_mount: SafeRelativePathSchema,
4277
- cwd: z6.string().min(1).refine((value) => isSafeRelativePath(value, { allowDot: true }), {
4307
+ cwd: z7.string().min(1).refine((value) => isSafeRelativePath(value, { allowDot: true }), {
4278
4308
  message: "must be a relative path without '..', absolute, or UNC segments"
4279
4309
  })
4280
4310
  });
4281
- SnapshotAdditionalRootSchema = z6.object({
4282
- id: z6.string().min(1),
4311
+ SnapshotAdditionalRootSchema = z7.object({
4312
+ id: z7.string().min(1),
4283
4313
  mount: SafeRelativePathSchema,
4284
- file_count: z6.number().int().nonnegative(),
4285
- total_size: z6.number().int().nonnegative(),
4314
+ file_count: z7.number().int().nonnegative(),
4315
+ total_size: z7.number().int().nonnegative(),
4286
4316
  tree_sha256: Sha256HexSchema,
4287
- mode: z6.enum(["read_only", "read_write"]).default("read_only")
4288
- });
4289
- SnapshotRepoSchema = z6.object({
4290
- canonical_id: z6.string().min(1),
4291
- url: z6.string().min(1),
4292
- branch: z6.string().nullable(),
4293
- base_commit: z6.string().min(1),
4294
- head_is_pushed: z6.boolean(),
4317
+ mode: z7.enum(["read_only", "read_write"]).default("read_only")
4318
+ });
4319
+ SnapshotRepoSchema = z7.object({
4320
+ canonical_id: z7.string().min(1),
4321
+ url: z7.string().min(1),
4322
+ branch: z7.string().nullable(),
4323
+ base_commit: z7.string().min(1),
4324
+ head_is_pushed: z7.boolean(),
4295
4325
  /** Optional; Controller derives from branch when absent (§10.6). */
4296
- fetch_refs: z6.array(z6.string().min(1)).optional()
4326
+ fetch_refs: z7.array(z7.string().min(1)).optional()
4297
4327
  });
4298
4328
  payloadBase = {
4299
- format: z6.literal("tar"),
4300
- compression: z6.literal("zstd"),
4329
+ format: z7.literal("tar"),
4330
+ compression: z7.literal("zstd"),
4301
4331
  sha256: Sha256HexSchema,
4302
- size: z6.number().int().nonnegative()
4332
+ size: z7.number().int().nonnegative()
4303
4333
  };
4304
- FullSnapshotManifestSchema = z6.object({
4305
- schema_version: z6.literal(1),
4334
+ FullSnapshotManifestSchema = z7.object({
4335
+ schema_version: z7.literal(1),
4306
4336
  content_id: ContentIdSchema,
4307
4337
  repo: SnapshotRepoSchema.partial({ base_commit: true }).optional(),
4308
4338
  workspace: SnapshotWorkspaceSchema,
4309
- source: z6.object({
4310
- files: z6.array(SnapshotFileEntrySchema),
4311
- empty_directories: z6.array(z6.string()).default([])
4339
+ source: z7.object({
4340
+ files: z7.array(SnapshotFileEntrySchema),
4341
+ empty_directories: z7.array(z7.string()).default([])
4312
4342
  }),
4313
- additional_roots: z6.array(SnapshotAdditionalRootSchema).default([]),
4314
- payload: z6.object({ mode: z6.literal("full"), ...payloadBase })
4343
+ additional_roots: z7.array(SnapshotAdditionalRootSchema).default([]),
4344
+ payload: z7.object({ mode: z7.literal("full"), ...payloadBase })
4315
4345
  }).strict();
4316
- GitOverlaySnapshotManifestSchema = z6.object({
4317
- schema_version: z6.literal(1),
4346
+ GitOverlaySnapshotManifestSchema = z7.object({
4347
+ schema_version: z7.literal(1),
4318
4348
  content_id: ContentIdSchema,
4319
4349
  repo: SnapshotRepoSchema,
4320
4350
  workspace: SnapshotWorkspaceSchema,
4321
- overlay: z6.object({
4322
- files: z6.array(SnapshotFileEntrySchema),
4323
- deletions: z6.array(z6.string()),
4324
- empty_directories: z6.array(z6.string()).default([])
4351
+ overlay: z7.object({
4352
+ files: z7.array(SnapshotFileEntrySchema),
4353
+ deletions: z7.array(z7.string()),
4354
+ empty_directories: z7.array(z7.string()).default([])
4325
4355
  }),
4326
- additional_roots: z6.array(SnapshotAdditionalRootSchema).default([]),
4327
- payload: z6.object({ mode: z6.literal("git_overlay"), ...payloadBase })
4356
+ additional_roots: z7.array(SnapshotAdditionalRootSchema).default([]),
4357
+ payload: z7.object({ mode: z7.literal("git_overlay"), ...payloadBase })
4328
4358
  }).strict();
4329
- SnapshotManifestSchema = z6.union([
4359
+ SnapshotManifestSchema = z7.union([
4330
4360
  FullSnapshotManifestSchema,
4331
4361
  GitOverlaySnapshotManifestSchema
4332
4362
  ]);
4333
- SnapshotInstanceSchema = z6.object({
4334
- snapshot_id: z6.string().min(1),
4363
+ SnapshotInstanceSchema = z7.object({
4364
+ snapshot_id: z7.string().min(1),
4335
4365
  content_id: ContentIdSchema,
4336
- captured_at: z6.string().min(1)
4366
+ captured_at: z7.string().min(1)
4337
4367
  });
4338
4368
  }
4339
4369
  });
@@ -4723,21 +4753,48 @@ var init_dist4 = __esm({
4723
4753
  });
4724
4754
 
4725
4755
  // ../controller/dist/config.js
4726
- import { mkdirSync as mkdirSync5 } from "node:fs";
4727
- import { join as join25 } from "node:path";
4756
+ import { existsSync as existsSync8, mkdirSync as mkdirSync5, readFileSync as readFileSync7, writeFileSync as writeFileSync5 } from "node:fs";
4757
+ import { isAbsolute as isAbsolute2, join as join25 } from "node:path";
4758
+ import { z as z8 } from "zod";
4728
4759
  function parseCsv2(raw) {
4729
4760
  if (!raw?.trim()) {
4730
4761
  return [];
4731
4762
  }
4732
4763
  return raw.split(",").map((part) => part.trim()).filter(Boolean);
4733
4764
  }
4734
- function parseGitAllowlist2(overrides) {
4735
- if (overrides) {
4736
- return overrides;
4765
+ function envSet2(key, env = process.env) {
4766
+ return env[key] !== void 0;
4767
+ }
4768
+ function parseEnvNumber2(key, raw) {
4769
+ const n = Number(raw);
4770
+ if (!Number.isFinite(n)) {
4771
+ throw new Error(`Invalid ${key}=${JSON.stringify(raw)}: expected a finite number`);
4737
4772
  }
4738
- const schemes = parseCsv2(process.env.RBO_GIT_ALLOWLIST_SCHEMES);
4739
- const hosts = parseCsv2(process.env.RBO_GIT_ALLOWLIST_HOSTS);
4740
- const prefixesRaw = process.env.RBO_GIT_ALLOWLIST_PREFIXES?.trim();
4773
+ return n;
4774
+ }
4775
+ function parseEnvInt2(key, raw) {
4776
+ const n = parseEnvNumber2(key, raw);
4777
+ if (!Number.isInteger(n)) {
4778
+ throw new Error(`Invalid ${key}=${JSON.stringify(raw)}: expected an integer`);
4779
+ }
4780
+ return n;
4781
+ }
4782
+ function assertAbsoluteAllowlistPaths(paths, label) {
4783
+ return paths.map((path, index) => {
4784
+ const parsed = AbsoluteAllowlistPathSchema.safeParse(path);
4785
+ if (!parsed.success) {
4786
+ throw new Error(`Invalid ${label}[${index}]=${JSON.stringify(path)}: must be a non-empty absolute path`);
4787
+ }
4788
+ return parsed.data;
4789
+ });
4790
+ }
4791
+ function parseGitAllowlistFromEnv2(env = process.env) {
4792
+ if (!envSet2("RBO_GIT_ALLOWLIST_SCHEMES", env) && !envSet2("RBO_GIT_ALLOWLIST_HOSTS", env) && !envSet2("RBO_GIT_ALLOWLIST_PREFIXES", env)) {
4793
+ return void 0;
4794
+ }
4795
+ const schemes = parseCsv2(env.RBO_GIT_ALLOWLIST_SCHEMES);
4796
+ const hosts = parseCsv2(env.RBO_GIT_ALLOWLIST_HOSTS);
4797
+ const prefixesRaw = env.RBO_GIT_ALLOWLIST_PREFIXES?.trim();
4741
4798
  let repository_prefixes;
4742
4799
  if (prefixesRaw) {
4743
4800
  try {
@@ -4756,34 +4813,124 @@ function parseGitAllowlist2(overrides) {
4756
4813
  ...repository_prefixes ? { repository_prefixes } : {}
4757
4814
  };
4758
4815
  }
4816
+ function mergeGitAllowlist2(file, fromEnv, override) {
4817
+ if (override) {
4818
+ return override;
4819
+ }
4820
+ if (fromEnv) {
4821
+ return fromEnv;
4822
+ }
4823
+ if (file) {
4824
+ return {
4825
+ schemes: file.schemes && file.schemes.length > 0 ? file.schemes : [...DEFAULT_GIT_ALLOWLIST_SCHEMES2],
4826
+ hosts: file.hosts && file.hosts.length > 0 ? file.hosts : [...DEFAULT_GIT_ALLOWLIST_HOSTS2],
4827
+ ...file.repository_prefixes ? { repository_prefixes: file.repository_prefixes } : {}
4828
+ };
4829
+ }
4830
+ return {
4831
+ schemes: [...DEFAULT_GIT_ALLOWLIST_SCHEMES2],
4832
+ hosts: [...DEFAULT_GIT_ALLOWLIST_HOSTS2]
4833
+ };
4834
+ }
4835
+ function defaultControllerConfigFile() {
4836
+ return {
4837
+ mcp_host: "127.0.0.1",
4838
+ mcp_port: 7410,
4839
+ agent_plane_port: 7411,
4840
+ controller_public_host: "127.0.0.1",
4841
+ data_plane_base_url: null,
4842
+ allowed_project_roots: [],
4843
+ allowed_artifact_destinations: [],
4844
+ git_allowlist: {
4845
+ schemes: [...DEFAULT_GIT_ALLOWLIST_SCHEMES2],
4846
+ hosts: [...DEFAULT_GIT_ALLOWLIST_HOSTS2]
4847
+ },
4848
+ local_max_concurrent_jobs: 1,
4849
+ disconnect_grace_seconds: 60,
4850
+ orphan_timeout_seconds: 300,
4851
+ reconcile_deadline_seconds: 120,
4852
+ allow_local_fallback: true,
4853
+ max_git_bundle_bytes: DEFAULT_MAX_GIT_BUNDLE_BYTES,
4854
+ local_fallback_max_host_cpu_percent: 80
4855
+ };
4856
+ }
4857
+ function resolveControllerConfigPath(dataDir) {
4858
+ return join25(dataDir, CONTROLLER_CONFIG_FILENAME);
4859
+ }
4860
+ function readControllerConfigFile(configPath) {
4861
+ if (!existsSync8(configPath)) {
4862
+ return void 0;
4863
+ }
4864
+ let raw;
4865
+ try {
4866
+ raw = JSON.parse(readFileSync7(configPath, "utf8"));
4867
+ } catch (error) {
4868
+ throw new Error(`Invalid controller config JSON at ${configPath}: ${error instanceof Error ? error.message : String(error)}`);
4869
+ }
4870
+ const parsed = ControllerConfigFileSchema.safeParse(raw);
4871
+ if (!parsed.success) {
4872
+ throw new Error(`Invalid controller config at ${configPath}: ${parsed.error.message}`);
4873
+ }
4874
+ return parsed.data;
4875
+ }
4876
+ function writeDefaultControllerConfigFile(dataDir, options = {}) {
4877
+ mkdirSync5(dataDir, { recursive: true });
4878
+ const path = resolveControllerConfigPath(dataDir);
4879
+ if (existsSync8(path) && !options.force) {
4880
+ return { path, written: false };
4881
+ }
4882
+ const body = `${JSON.stringify(defaultControllerConfigFile(), null, 2)}
4883
+ `;
4884
+ writeFileSync5(path, body, "utf8");
4885
+ return { path, written: true };
4886
+ }
4759
4887
  function loadControllerConfig(overrides = {}) {
4760
- const dataDir = overrides.dataDir ?? resolveControllerDataDir();
4888
+ const { configPath: configPathOption, ...fieldOverrides } = overrides;
4889
+ const dataDir = fieldOverrides.dataDir ?? resolveControllerDataDir();
4890
+ const configPath = configPathOption === void 0 ? resolveControllerConfigPath(dataDir) : configPathOption;
4891
+ const file = configPath === null ? void 0 : readControllerConfigFile(configPath);
4892
+ const mcpHost = fieldOverrides.mcpHost ?? (envSet2("RBO_MCP_HOST") ? process.env.RBO_MCP_HOST : void 0) ?? file?.mcp_host ?? "127.0.0.1";
4893
+ const mcpPort = fieldOverrides.mcpPort ?? (envSet2("RBO_MCP_PORT") ? parseEnvInt2("RBO_MCP_PORT", process.env.RBO_MCP_PORT) : void 0) ?? file?.mcp_port ?? 7410;
4894
+ const agentPlanePort = fieldOverrides.agentPlanePort ?? (envSet2("RBO_AGENT_PORT") ? parseEnvInt2("RBO_AGENT_PORT", process.env.RBO_AGENT_PORT) : void 0) ?? file?.agent_plane_port ?? 7411;
4895
+ const controllerPublicHost = fieldOverrides.controllerPublicHost ?? (envSet2("RBO_CONTROLLER_PUBLIC_HOST") ? process.env.RBO_CONTROLLER_PUBLIC_HOST : void 0) ?? file?.controller_public_host ?? "127.0.0.1";
4896
+ let dataPlaneBaseUrl = fieldOverrides.dataPlaneBaseUrl;
4897
+ if (dataPlaneBaseUrl === void 0) {
4898
+ if (envSet2("RBO_DATA_PLANE_BASE_URL")) {
4899
+ dataPlaneBaseUrl = process.env.RBO_DATA_PLANE_BASE_URL;
4900
+ } else if (file?.data_plane_base_url !== void 0) {
4901
+ dataPlaneBaseUrl = file.data_plane_base_url ?? void 0;
4902
+ }
4903
+ }
4904
+ const allowedProjectRoots = assertAbsoluteAllowlistPaths(fieldOverrides.allowedProjectRoots ?? (envSet2("RBO_ALLOWED_PROJECT_ROOTS") ? parseCsv2(process.env.RBO_ALLOWED_PROJECT_ROOTS) : void 0) ?? file?.allowed_project_roots ?? [], "allowed_project_roots");
4905
+ const allowedArtifactDestinations = assertAbsoluteAllowlistPaths(fieldOverrides.allowedArtifactDestinations ?? (envSet2("RBO_ALLOWED_ARTIFACT_DESTINATIONS") ? parseCsv2(process.env.RBO_ALLOWED_ARTIFACT_DESTINATIONS) : void 0) ?? file?.allowed_artifact_destinations ?? [], "allowed_artifact_destinations");
4906
+ const allowLocalFallback = fieldOverrides.allowLocalFallback ?? (envSet2("RBO_ALLOW_LOCAL_FALLBACK") ? process.env.RBO_ALLOW_LOCAL_FALLBACK !== "false" : void 0) ?? file?.allow_local_fallback ?? true;
4907
+ const maxHostCpuBusyFraction = fieldOverrides.maxHostCpuBusyFraction ?? (envSet2("RBO_LOCAL_FALLBACK_MAX_HOST_CPU_PERCENT") ? parseEnvNumber2("RBO_LOCAL_FALLBACK_MAX_HOST_CPU_PERCENT", process.env.RBO_LOCAL_FALLBACK_MAX_HOST_CPU_PERCENT) / 100 : void 0) ?? (file?.local_fallback_max_host_cpu_percent !== void 0 ? file.local_fallback_max_host_cpu_percent / 100 : void 0) ?? 0.8;
4761
4908
  return {
4762
- mcpHost: overrides.mcpHost ?? process.env.RBO_MCP_HOST ?? "127.0.0.1",
4763
- mcpPort: overrides.mcpPort ?? Number(process.env.RBO_MCP_PORT ?? 7410),
4764
- agentPlanePort: overrides.agentPlanePort ?? Number(process.env.RBO_AGENT_PORT ?? 7411),
4765
- controllerPublicHost: overrides.controllerPublicHost ?? process.env.RBO_CONTROLLER_PUBLIC_HOST ?? "127.0.0.1",
4766
- dataPlaneBaseUrl: overrides.dataPlaneBaseUrl ?? process.env.RBO_DATA_PLANE_BASE_URL,
4909
+ mcpHost,
4910
+ mcpPort,
4911
+ agentPlanePort,
4912
+ controllerPublicHost,
4913
+ dataPlaneBaseUrl,
4767
4914
  dataDir,
4768
- databasePath: overrides.databasePath ?? join25(dataDir, "controller.db"),
4769
- allowedProjectRoots: overrides.allowedProjectRoots ?? parseCsv2(process.env.RBO_ALLOWED_PROJECT_ROOTS),
4770
- allowedArtifactDestinations: overrides.allowedArtifactDestinations ?? parseCsv2(process.env.RBO_ALLOWED_ARTIFACT_DESTINATIONS),
4771
- gitAllowlist: parseGitAllowlist2(overrides.gitAllowlist),
4915
+ databasePath: fieldOverrides.databasePath ?? join25(dataDir, "controller.db"),
4916
+ allowedProjectRoots,
4917
+ allowedArtifactDestinations,
4918
+ gitAllowlist: mergeGitAllowlist2(file?.git_allowlist, parseGitAllowlistFromEnv2(), fieldOverrides.gitAllowlist),
4772
4919
  localExecutor: {
4773
- maxConcurrentJobs: overrides.localExecutor?.maxConcurrentJobs ?? Number(process.env.RBO_LOCAL_MAX_CONCURRENT_JOBS ?? 1)
4920
+ maxConcurrentJobs: fieldOverrides.localExecutor?.maxConcurrentJobs ?? (envSet2("RBO_LOCAL_MAX_CONCURRENT_JOBS") ? parseEnvInt2("RBO_LOCAL_MAX_CONCURRENT_JOBS", process.env.RBO_LOCAL_MAX_CONCURRENT_JOBS) : void 0) ?? file?.local_max_concurrent_jobs ?? 1
4774
4921
  },
4775
- disconnectGraceSeconds: overrides.disconnectGraceSeconds ?? Number(process.env.RBO_DISCONNECT_GRACE_SECONDS ?? 60),
4776
- orphanTimeoutSeconds: overrides.orphanTimeoutSeconds ?? Number(process.env.RBO_ORPHAN_TIMEOUT_SECONDS ?? 300),
4777
- reconcileDeadlineSeconds: overrides.reconcileDeadlineSeconds ?? Number(process.env.RBO_RECONCILE_DEADLINE_SECONDS ?? 120),
4778
- allowLocalFallback: overrides.allowLocalFallback ?? process.env.RBO_ALLOW_LOCAL_FALLBACK !== "false",
4779
- maxGitBundleBytes: overrides.maxGitBundleBytes ?? Number(process.env.RBO_MAX_GIT_BUNDLE_BYTES ?? DEFAULT_MAX_GIT_BUNDLE_BYTES),
4780
- maxHostCpuBusyFraction: overrides.maxHostCpuBusyFraction ?? Number(process.env.RBO_LOCAL_FALLBACK_MAX_HOST_CPU_PERCENT ?? 80) / 100
4922
+ disconnectGraceSeconds: fieldOverrides.disconnectGraceSeconds ?? (envSet2("RBO_DISCONNECT_GRACE_SECONDS") ? parseEnvInt2("RBO_DISCONNECT_GRACE_SECONDS", process.env.RBO_DISCONNECT_GRACE_SECONDS) : void 0) ?? file?.disconnect_grace_seconds ?? 60,
4923
+ orphanTimeoutSeconds: fieldOverrides.orphanTimeoutSeconds ?? (envSet2("RBO_ORPHAN_TIMEOUT_SECONDS") ? parseEnvInt2("RBO_ORPHAN_TIMEOUT_SECONDS", process.env.RBO_ORPHAN_TIMEOUT_SECONDS) : void 0) ?? file?.orphan_timeout_seconds ?? 300,
4924
+ reconcileDeadlineSeconds: fieldOverrides.reconcileDeadlineSeconds ?? (envSet2("RBO_RECONCILE_DEADLINE_SECONDS") ? parseEnvInt2("RBO_RECONCILE_DEADLINE_SECONDS", process.env.RBO_RECONCILE_DEADLINE_SECONDS) : void 0) ?? file?.reconcile_deadline_seconds ?? 120,
4925
+ allowLocalFallback,
4926
+ maxGitBundleBytes: fieldOverrides.maxGitBundleBytes ?? (envSet2("RBO_MAX_GIT_BUNDLE_BYTES") ? parseEnvInt2("RBO_MAX_GIT_BUNDLE_BYTES", process.env.RBO_MAX_GIT_BUNDLE_BYTES) : void 0) ?? file?.max_git_bundle_bytes ?? DEFAULT_MAX_GIT_BUNDLE_BYTES,
4927
+ maxHostCpuBusyFraction
4781
4928
  };
4782
4929
  }
4783
4930
  function ensureDataDir(config) {
4784
4931
  mkdirSync5(config.dataDir, { recursive: true });
4785
4932
  }
4786
- var DEFAULT_MAX_GIT_BUNDLE_BYTES, DEFAULT_GIT_ALLOWLIST_SCHEMES2, DEFAULT_GIT_ALLOWLIST_HOSTS2;
4933
+ var DEFAULT_MAX_GIT_BUNDLE_BYTES, DEFAULT_GIT_ALLOWLIST_SCHEMES2, DEFAULT_GIT_ALLOWLIST_HOSTS2, CONTROLLER_CONFIG_FILENAME, GitAllowlistFileSchema2, AbsoluteAllowlistPathSchema, ControllerConfigFileSchema;
4787
4934
  var init_config = __esm({
4788
4935
  "../controller/dist/config.js"() {
4789
4936
  "use strict";
@@ -4791,6 +4938,32 @@ var init_config = __esm({
4791
4938
  DEFAULT_MAX_GIT_BUNDLE_BYTES = 512 * 1024 * 1024;
4792
4939
  DEFAULT_GIT_ALLOWLIST_SCHEMES2 = ["https", "ssh"];
4793
4940
  DEFAULT_GIT_ALLOWLIST_HOSTS2 = ["github.com"];
4941
+ CONTROLLER_CONFIG_FILENAME = "controller.json";
4942
+ GitAllowlistFileSchema2 = z8.object({
4943
+ schemes: z8.array(z8.string().min(1)).optional(),
4944
+ hosts: z8.array(z8.string().min(1)).optional(),
4945
+ repository_prefixes: z8.array(z8.string().min(1)).optional()
4946
+ });
4947
+ AbsoluteAllowlistPathSchema = z8.string().refine((value) => value.length > 0 && value === value.trim() && isAbsolute2(value), {
4948
+ message: 'must be a non-empty absolute path (relative paths, ".", and whitespace are rejected)'
4949
+ });
4950
+ ControllerConfigFileSchema = z8.object({
4951
+ mcp_host: z8.string().min(1).optional(),
4952
+ mcp_port: z8.number().int().positive().optional(),
4953
+ agent_plane_port: z8.number().int().positive().optional(),
4954
+ controller_public_host: z8.string().min(1).optional(),
4955
+ data_plane_base_url: z8.string().min(1).nullable().optional(),
4956
+ allowed_project_roots: z8.array(AbsoluteAllowlistPathSchema).optional(),
4957
+ allowed_artifact_destinations: z8.array(AbsoluteAllowlistPathSchema).optional(),
4958
+ git_allowlist: GitAllowlistFileSchema2.optional(),
4959
+ local_max_concurrent_jobs: z8.number().int().positive().optional(),
4960
+ disconnect_grace_seconds: z8.number().int().nonnegative().optional(),
4961
+ orphan_timeout_seconds: z8.number().int().nonnegative().optional(),
4962
+ reconcile_deadline_seconds: z8.number().int().nonnegative().optional(),
4963
+ allow_local_fallback: z8.boolean().optional(),
4964
+ max_git_bundle_bytes: z8.number().int().positive().optional(),
4965
+ local_fallback_max_host_cpu_percent: z8.number().min(0).max(100).optional()
4966
+ }).strict();
4794
4967
  }
4795
4968
  });
4796
4969
 
@@ -5028,7 +5201,7 @@ var init_database = __esm({
5028
5201
  });
5029
5202
 
5030
5203
  // ../controller/dist/execution/artifacts.js
5031
- import { appendFile as appendFile3, mkdir as mkdir12, writeFile as writeFile12 } from "node:fs/promises";
5204
+ import { appendFile as appendFile3, mkdir as mkdir11, writeFile as writeFile11 } from "node:fs/promises";
5032
5205
  import { join as join26 } from "node:path";
5033
5206
  async function persistCollectedArtifacts(input) {
5034
5207
  const results = [];
@@ -5073,7 +5246,7 @@ async function materializeArtifactToDestination(input) {
5073
5246
  if (!allowed) {
5074
5247
  throw new Error("Destination path is outside allowed artifact destinations");
5075
5248
  }
5076
- const { access: access4, readFile: readFile20, rename: rename6 } = await import("node:fs/promises");
5249
+ const { access: access4, readFile: readFile19, rename: rename6 } = await import("node:fs/promises");
5077
5250
  try {
5078
5251
  await access4(input.destinationPath);
5079
5252
  if (!input.overwrite) {
@@ -5084,20 +5257,20 @@ async function materializeArtifactToDestination(input) {
5084
5257
  throw error;
5085
5258
  }
5086
5259
  }
5087
- const content = await readFile20(row.path);
5260
+ const content = await readFile19(row.path);
5088
5261
  if (sha256(content) !== row.sha256) {
5089
5262
  throw new Error("Stored artifact hash mismatch");
5090
5263
  }
5091
5264
  const tempPath = `${input.destinationPath}.rbo.tmp`;
5092
- await writeFile12(tempPath, content);
5093
- const writtenHash = sha256(await readFile20(tempPath));
5265
+ await writeFile11(tempPath, content);
5266
+ const writtenHash = sha256(await readFile19(tempPath));
5094
5267
  if (writtenHash !== row.sha256) {
5095
5268
  throw new Error("Temporary artifact hash mismatch");
5096
5269
  }
5097
5270
  await rename6(tempPath, input.destinationPath);
5098
5271
  if (input.dataDir) {
5099
5272
  const auditDir = join26(input.dataDir, "audit");
5100
- await mkdir12(auditDir, { recursive: true });
5273
+ await mkdir11(auditDir, { recursive: true });
5101
5274
  const auditLine = JSON.stringify({
5102
5275
  type: "artifact_materialize",
5103
5276
  created_at: nowIso(),
@@ -5284,7 +5457,7 @@ var init_lifecycle = __esm({
5284
5457
  });
5285
5458
 
5286
5459
  // ../controller/dist/execution/runner.js
5287
- import { mkdir as mkdir13, readFile as readFile15, rm as rm10, writeFile as writeFile13 } from "node:fs/promises";
5460
+ import { mkdir as mkdir12, readFile as readFile14, rm as rm10, writeFile as writeFile12 } from "node:fs/promises";
5288
5461
  import { dirname as dirname7, join as join27 } from "node:path";
5289
5462
  function getLocalRunningJobsCount() {
5290
5463
  return admissionActive;
@@ -5410,9 +5583,9 @@ async function runLocalJob(ctx, jobId) {
5410
5583
  const persistentLogs = attemptLogDir(ctx.dataDir, attempt.id);
5411
5584
  const controlDir = attemptControlDir(ctx.dataDir, attempt.id);
5412
5585
  const artifactsDir = attemptArtifactsDir(ctx.dataDir, attempt.id);
5413
- await mkdir13(artifactsDir, { recursive: true });
5414
- await mkdir13(persistentLogs, { recursive: true });
5415
- await mkdir13(controlDir, { recursive: true });
5586
+ await mkdir12(artifactsDir, { recursive: true });
5587
+ await mkdir12(persistentLogs, { recursive: true });
5588
+ await mkdir12(controlDir, { recursive: true });
5416
5589
  let timedOut = false;
5417
5590
  let cancelled = false;
5418
5591
  let durationComplete = false;
@@ -5433,7 +5606,7 @@ async function runLocalJob(ctx, jobId) {
5433
5606
  if (!snapshotRow?.payload_path) {
5434
5607
  throw new RboError("internal", "Job is missing snapshot payload");
5435
5608
  }
5436
- const manifest = JSON.parse(await readFile15(snapshotRow.manifest_path, "utf8"));
5609
+ const manifest = JSON.parse(await readFile14(snapshotRow.manifest_path, "utf8"));
5437
5610
  const materialized = await materializeFullSnapshot({
5438
5611
  manifest,
5439
5612
  archivePath: snapshotRow.payload_path,
@@ -5451,7 +5624,7 @@ async function runLocalJob(ctx, jobId) {
5451
5624
  });
5452
5625
  const warningsPath = join27(dirname7(snapshotRow.manifest_path), "secret-warnings.json");
5453
5626
  try {
5454
- const warnings = JSON.parse(await readFile15(warningsPath, "utf8"));
5627
+ const warnings = JSON.parse(await readFile14(warningsPath, "utf8"));
5455
5628
  for (const warning of warnings) {
5456
5629
  await emitJobEvent(ctx, logs, {
5457
5630
  type: "secret_warning",
@@ -5698,9 +5871,9 @@ async function captureAndPersistSnapshot(ctx, jobId, request) {
5698
5871
  contentStorageDir: storageDir
5699
5872
  });
5700
5873
  const manifestPath = join27(storageDir, "manifest.json");
5701
- await writeFile13(manifestPath, JSON.stringify(captured.manifest, null, 2));
5702
- await writeFile13(join27(storageDir, "secret-warnings.json"), JSON.stringify(captured.secretWarnings));
5703
- await writeFile13(join27(storageDir, "git-source-requirements.json"), JSON.stringify(captured.gitSourceRequirements));
5874
+ await writeFile12(manifestPath, JSON.stringify(captured.manifest, null, 2));
5875
+ await writeFile12(join27(storageDir, "secret-warnings.json"), JSON.stringify(captured.secretWarnings));
5876
+ await writeFile12(join27(storageDir, "git-source-requirements.json"), JSON.stringify(captured.gitSourceRequirements));
5704
5877
  persistSnapshot(ctx.db, {
5705
5878
  snapshotId: captured.instance.snapshot_id,
5706
5879
  contentId: captured.manifest.content_id,
@@ -5746,7 +5919,7 @@ function mergeGitSourceToolRequirements(request, gitSourceRequirements) {
5746
5919
  }
5747
5920
  async function readGitSourceRequirements(dataDir, jobId) {
5748
5921
  try {
5749
- const raw = await readFile15(join27(dataDir, "snapshots", jobId, "git-source-requirements.json"), "utf8");
5922
+ const raw = await readFile14(join27(dataDir, "snapshots", jobId, "git-source-requirements.json"), "utf8");
5750
5923
  const parsed = JSON.parse(raw);
5751
5924
  return {
5752
5925
  submodules: parsed.submodules === true,
@@ -5838,7 +6011,7 @@ var init_data_tokens = __esm({
5838
6011
  // ../controller/dist/http/data-plane.js
5839
6012
  import { createHash as createHash7 } from "node:crypto";
5840
6013
  import { createReadStream as createReadStream2, createWriteStream as createWriteStream2 } from "node:fs";
5841
- import { access as access3, mkdir as mkdir14, readFile as readFile16, rename as rename5, rm as rm11, stat as stat8 } from "node:fs/promises";
6014
+ import { access as access3, mkdir as mkdir13, readFile as readFile15, rename as rename5, rm as rm11, stat as stat8 } from "node:fs/promises";
5842
6015
  import { dirname as dirname8, join as join28 } from "node:path";
5843
6016
  function artifactUploadStreamLimit(declaredSizeBytes, globalCapBytes = DEFAULT_ARTIFACT_CAP_BYTES) {
5844
6017
  return Math.min(globalCapBytes, declaredSizeBytes);
@@ -5868,7 +6041,7 @@ async function filterMissingArtifacts(dataDir, attemptId, artifacts) {
5868
6041
  for (const art of artifacts) {
5869
6042
  const destPath = join28(artifactsDir, art.logical_name);
5870
6043
  try {
5871
- const buf = await readFile16(destPath);
6044
+ const buf = await readFile15(destPath);
5872
6045
  const hash = createHash7("sha256").update(buf).digest("hex");
5873
6046
  if (hash === art.sha256 && buf.length === art.size_bytes) {
5874
6047
  continue;
@@ -5988,7 +6161,7 @@ async function handleDataPlaneRequest(req, res, options) {
5988
6161
  const transferStats = await stat8(transferSnapshot);
5989
6162
  payloadPath = transferSnapshot;
5990
6163
  sizeBytes = transferStats.size;
5991
- const transferData = await readFile16(transferSnapshot);
6164
+ const transferData = await readFile15(transferSnapshot);
5992
6165
  sha2562 = createHash7("sha256").update(transferData).digest("hex");
5993
6166
  } catch {
5994
6167
  }
@@ -6087,8 +6260,8 @@ async function handleDataPlaneRequest(req, res, options) {
6087
6260
  return true;
6088
6261
  }
6089
6262
  const partPath = join28(artifactsDir, `.upload-${createHash7("sha256").update(logicalName).digest("hex").slice(0, 16)}.part`);
6090
- await mkdir14(dirname8(destPath), { recursive: true });
6091
- await mkdir14(artifactsDir, { recursive: true });
6263
+ await mkdir13(dirname8(destPath), { recursive: true });
6264
+ await mkdir13(artifactsDir, { recursive: true });
6092
6265
  const hasher = createHash7("sha256");
6093
6266
  const writeStream = createWriteStream2(partPath);
6094
6267
  let sizeCounter = 0;
@@ -6297,7 +6470,7 @@ async function handleDataPlaneRequest(req, res, options) {
6297
6470
  }
6298
6471
  const bundlePath = join28(attemptTransferDir(options.dataDir, attemptId), "bundle.gitbundle");
6299
6472
  try {
6300
- const data = await readFile16(bundlePath);
6473
+ const data = await readFile15(bundlePath);
6301
6474
  const sha2562 = createHash7("sha256").update(data).digest("hex");
6302
6475
  res.writeHead(200, {
6303
6476
  "content-type": "application/octet-stream",
@@ -6696,8 +6869,8 @@ var init_coordinator = __esm({
6696
6869
  // ../controller/dist/execution/remote-execution.js
6697
6870
  import { execFile as execFile8 } from "node:child_process";
6698
6871
  import { createHash as createHash8 } from "node:crypto";
6699
- import { readFileSync as readFileSync6 } from "node:fs";
6700
- import { mkdir as mkdir15, readFile as readFile17, rm as rm12, stat as stat9, writeFile as writeFile14 } from "node:fs/promises";
6872
+ import { readFileSync as readFileSync8 } from "node:fs";
6873
+ import { mkdir as mkdir14, readFile as readFile16, rm as rm12, stat as stat9, writeFile as writeFile13 } from "node:fs/promises";
6701
6874
  import { dirname as dirname9, join as join29 } from "node:path";
6702
6875
  import { promisify as promisify8 } from "node:util";
6703
6876
  function snapshotManifestMode(manifest) {
@@ -6804,7 +6977,7 @@ function failAttemptPrepare(opts, attemptId, jobId, category, message) {
6804
6977
  });
6805
6978
  }
6806
6979
  async function createGitBundle(projectRoot, baseCommit, bundlePath, maxBytes = DEFAULT_MAX_GIT_BUNDLE_BYTES) {
6807
- await mkdir15(dirname9(bundlePath), { recursive: true });
6980
+ await mkdir14(dirname9(bundlePath), { recursive: true });
6808
6981
  const { stdout: head } = await execFileAsync8("git", ["rev-parse", "HEAD"], {
6809
6982
  cwd: projectRoot,
6810
6983
  windowsHide: true
@@ -6833,7 +7006,7 @@ async function createGitBundle(projectRoot, baseCommit, bundlePath, maxBytes = D
6833
7006
  await rm12(bundlePath, { force: true });
6834
7007
  throw new GitBundleSizeExceededError(sizeBytes, maxBytes);
6835
7008
  }
6836
- const data = await readFile17(bundlePath);
7009
+ const data = await readFile16(bundlePath);
6837
7010
  return {
6838
7011
  sizeBytes: data.length,
6839
7012
  sha256: createHash8("sha256").update(data).digest("hex")
@@ -6849,9 +7022,9 @@ async function ensureFullFallbackArchive(opts, attemptId, jobId) {
6849
7022
  const manifestPath = join29(transferDir, "snapshot.manifest.json");
6850
7023
  try {
6851
7024
  const existing = await stat9(archivePath);
6852
- const data = await readFile17(archivePath);
7025
+ const data = await readFile16(archivePath);
6853
7026
  const sha2562 = createHash8("sha256").update(data).digest("hex");
6854
- const manifestRaw = await readFile17(manifestPath, "utf8");
7027
+ const manifestRaw = await readFile16(manifestPath, "utf8");
6855
7028
  const manifest = JSON.parse(manifestRaw);
6856
7029
  if (manifest.payload?.sha256 === sha2562 && manifest.payload?.size === existing.size) {
6857
7030
  return {
@@ -6863,7 +7036,7 @@ async function ensureFullFallbackArchive(opts, attemptId, jobId) {
6863
7036
  }
6864
7037
  } catch {
6865
7038
  }
6866
- await mkdir15(transferDir, { recursive: true });
7039
+ await mkdir14(transferDir, { recursive: true });
6867
7040
  const captured = await captureFullSnapshot({
6868
7041
  projectRoot: request.source.project_root,
6869
7042
  allowedProjectRoots: opts.allowedProjectRoots ?? [request.source.project_root],
@@ -6876,8 +7049,8 @@ async function ensureFullFallbackArchive(opts, attemptId, jobId) {
6876
7049
  additionalRoots: request.source.additional_roots,
6877
7050
  contentStorageDir: transferDir
6878
7051
  });
6879
- await writeFile14(archivePath, await readFile17(captured.archivePath));
6880
- await writeFile14(manifestPath, JSON.stringify(captured.manifest, null, 2));
7052
+ await writeFile13(archivePath, await readFile16(captured.archivePath));
7053
+ await writeFile13(manifestPath, JSON.stringify(captured.manifest, null, 2));
6881
7054
  return {
6882
7055
  archivePath,
6883
7056
  sizeBytes: captured.manifest.payload.size,
@@ -7023,7 +7196,7 @@ function handleRemoteLeaseAccept(opts, agentId, payload) {
7023
7196
  let manifest;
7024
7197
  if (snapshotRow.manifest_path) {
7025
7198
  try {
7026
- manifest = JSON.parse(readFileSync6(snapshotRow.manifest_path, "utf8"));
7199
+ manifest = JSON.parse(readFileSync8(snapshotRow.manifest_path, "utf8"));
7027
7200
  } catch {
7028
7201
  }
7029
7202
  }
@@ -7050,7 +7223,7 @@ async function handleRemoteSourceNeed(opts, agentId, payload) {
7050
7223
  }
7051
7224
  let manifest;
7052
7225
  try {
7053
- manifest = JSON.parse(readFileSync6(snapshotRow.manifest_path, "utf8"));
7226
+ manifest = JSON.parse(readFileSync8(snapshotRow.manifest_path, "utf8"));
7054
7227
  } catch {
7055
7228
  failAttemptPrepare(opts, attempt.id, attempt.job_id, "materialization", "Cannot create bundle: manifest unreadable");
7056
7229
  return;
@@ -7097,7 +7270,7 @@ async function handleRemoteSourceNeed(opts, agentId, payload) {
7097
7270
  }
7098
7271
  let manifest;
7099
7272
  try {
7100
- manifest = JSON.parse(readFileSync6(snapshotRow.manifest_path, "utf8"));
7273
+ manifest = JSON.parse(readFileSync8(snapshotRow.manifest_path, "utf8"));
7101
7274
  } catch {
7102
7275
  manifest = void 0;
7103
7276
  }
@@ -7240,6 +7413,18 @@ async function handleRemoteLogChunk(opts, agentId, payload) {
7240
7413
  updateAttempt(opts.db, attempt.id, { log_acked_sequence: payload.sequence });
7241
7414
  sendAck();
7242
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
+ }
7243
7428
  function handleRemoteJobStarted(opts, agentId, payload) {
7244
7429
  const attempt = loadAttemptByLease(opts.db, payload.attempt_id, payload.lease_id, payload.lease_epoch);
7245
7430
  if (!rejectStale("job_started", attempt, agentId, ["running"])) {
@@ -7353,7 +7538,7 @@ function handleRemoteCleanupComplete(opts, agentId, payload) {
7353
7538
  result_json: JSON.stringify({ outcome, exit_code: exitCode })
7354
7539
  });
7355
7540
  }
7356
- var execFileAsync8, logger11, DEFAULT_LEASE_TTL_SECONDS, GitBundleSizeExceededError;
7541
+ var execFileAsync8, logger11, DEFAULT_LEASE_TTL_SECONDS, GitBundleSizeExceededError, logChunkChains;
7357
7542
  var init_remote_execution = __esm({
7358
7543
  "../controller/dist/execution/remote-execution.js"() {
7359
7544
  "use strict";
@@ -7380,6 +7565,7 @@ var init_remote_execution = __esm({
7380
7565
  this.name = "GitBundleSizeExceededError";
7381
7566
  }
7382
7567
  };
7568
+ logChunkChains = /* @__PURE__ */ new Map();
7383
7569
  }
7384
7570
  });
7385
7571
 
@@ -7714,7 +7900,7 @@ function selectAgentForJob(agents, request, options = {}) {
7714
7900
  const busyAgentRunningJobs = [];
7715
7901
  for (const candidate of agents) {
7716
7902
  const caps = candidate.capabilities;
7717
- const effectiveCapacity = Math.min(caps.execution.max_jobs, 1);
7903
+ const effectiveCapacity = caps.execution.max_jobs;
7718
7904
  if (effectiveCapacity <= 0 || candidate.activeJobsCount >= effectiveCapacity) {
7719
7905
  if (effectiveCapacity > 0) {
7720
7906
  busyAgentRunningJobs.push(candidate.activeJobsCount);
@@ -8186,7 +8372,7 @@ async function handleJobConfirm(ctx, args) {
8186
8372
  });
8187
8373
  return { job_id: job.id, state: "queued" };
8188
8374
  }
8189
- async function waitForJob(ctx, jobId, waitSeconds, includeLogTailLines) {
8375
+ async function waitForJob(ctx, jobId, waitSeconds, includeLogTailLines, options) {
8190
8376
  const deadline = Date.now() + waitSeconds * 1e3;
8191
8377
  let job = getJob(ctx.db, jobId);
8192
8378
  if (!job) {
@@ -8195,6 +8381,9 @@ async function waitForJob(ctx, jobId, waitSeconds, includeLogTailLines) {
8195
8381
  };
8196
8382
  }
8197
8383
  while (job && !isTerminalJobState(job.state) && Date.now() < deadline) {
8384
+ if (options?.onTick) {
8385
+ await options.onTick(job);
8386
+ }
8198
8387
  await new Promise((resolvePromise) => setTimeout(resolvePromise, 200));
8199
8388
  job = getJob(ctx.db, jobId);
8200
8389
  }
@@ -8303,22 +8492,18 @@ var init_submit = __esm({
8303
8492
 
8304
8493
  // src/main.ts
8305
8494
  init_dist();
8306
- import { readFile as readFile19 } from "node:fs/promises";
8495
+ import { readFile as readFile18 } from "node:fs/promises";
8307
8496
 
8308
8497
  // src/commands/agent.ts
8309
- import { existsSync as existsSync6 } from "node:fs";
8310
- import { mkdir as mkdir11, readFile as readFile14, writeFile as writeFile11 } from "node:fs/promises";
8498
+ import { existsSync as existsSync7 } from "node:fs";
8311
8499
  import { join as join24 } from "node:path";
8312
8500
 
8313
- // ../agent/dist/run.js
8501
+ // ../agent/dist/config.js
8502
+ init_dist2();
8314
8503
  init_dist();
8315
-
8316
- // ../agent/dist/capabilities/probe.js
8317
- import { execFile as execFile2 } from "node:child_process";
8318
- import { readFile as readFile5, readdir as readdir5, statfs } from "node:fs/promises";
8319
- import { arch, cpus as cpus2, freemem, hostname, loadavg, platform as platform2, release, totalmem } from "node:os";
8320
- import { join as join8 } from "node:path";
8321
- import { promisify as promisify2 } from "node:util";
8504
+ import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "node:fs";
8505
+ import { dirname as dirname2, join as join7 } from "node:path";
8506
+ import { z as z6 } from "zod";
8322
8507
 
8323
8508
  // ../agent/dist/build-cache/index.js
8324
8509
  import { access, readdir as readdir3 } from "node:fs/promises";
@@ -9021,12 +9206,6 @@ function resolveBuildCachesDir(stateDir) {
9021
9206
  return join5(stateDir, "build-caches");
9022
9207
  }
9023
9208
 
9024
- // ../agent/dist/config.js
9025
- init_dist2();
9026
- init_dist();
9027
- import { mkdirSync as mkdirSync2 } from "node:fs";
9028
- import { dirname as dirname2, join as join7 } from "node:path";
9029
-
9030
9209
  // ../agent/dist/repos/mirror.js
9031
9210
  init_dist();
9032
9211
  import { execFile } from "node:child_process";
@@ -9383,19 +9562,77 @@ var DEFAULT_LOG_SPOOL_MAX_BYTES = 536870912;
9383
9562
  var DEFAULT_LOG_SEND_QUEUE_MAX = 64;
9384
9563
  var DEFAULT_GIT_ALLOWLIST_SCHEMES = ["https", "ssh"];
9385
9564
  var DEFAULT_GIT_ALLOWLIST_HOSTS = ["github.com"];
9565
+ var AGENT_CONFIG_FILENAME = "agent.json";
9566
+ var AGENT_CONFIG_SCHEMA_VERSION = 1;
9567
+ var RiskLevelSchema2 = z6.enum(["safe", "normal", "destructive", "hardware"]);
9568
+ var GitAllowlistFileSchema = z6.object({
9569
+ schemes: z6.array(z6.string().min(1)).optional(),
9570
+ hosts: z6.array(z6.string().min(1)).optional(),
9571
+ repository_prefixes: z6.array(z6.string().min(1)).optional()
9572
+ });
9573
+ var RepoCacheFileSchema = z6.object({
9574
+ max_size_gb: z6.number().positive().optional(),
9575
+ min_free_disk_gb: z6.number().nonnegative().optional(),
9576
+ retention_days: z6.number().nonnegative().optional()
9577
+ });
9578
+ var BuildCacheFileSchema = z6.object({
9579
+ enabled_kinds: z6.array(BuildCacheKindSchema).optional(),
9580
+ max_size_gb: z6.number().positive().optional(),
9581
+ min_free_disk_gb: z6.number().nonnegative().optional(),
9582
+ retention_days: z6.number().nonnegative().optional(),
9583
+ allow_read_risk_levels: z6.array(RiskLevelSchema2).optional(),
9584
+ allow_write_risk_levels: z6.array(RiskLevelSchema2).optional()
9585
+ });
9586
+ var AgentConfigFileSchema = z6.object({
9587
+ schema_version: z6.number().int().positive().optional(),
9588
+ initialized_at: z6.string().min(1).optional(),
9589
+ controller_url: z6.string().optional(),
9590
+ controller_fingerprint: z6.string().optional(),
9591
+ display_name: z6.string().min(1).optional(),
9592
+ max_jobs: z6.number().int().positive().optional(),
9593
+ repo_cache_dir: z6.string().min(1).nullable().optional(),
9594
+ secret_map: z6.record(z6.string()).nullable().optional(),
9595
+ git_allowlist: GitAllowlistFileSchema.optional(),
9596
+ repo_cache: RepoCacheFileSchema.optional(),
9597
+ build_cache: BuildCacheFileSchema.optional(),
9598
+ log_spool_max_bytes: z6.number().int().positive().optional(),
9599
+ log_send_queue_max: z6.number().int().positive().optional(),
9600
+ disconnect_grace_seconds: z6.number().int().nonnegative().optional(),
9601
+ orphan_timeout_seconds: z6.number().int().nonnegative().optional(),
9602
+ reconcile_deadline_seconds: z6.number().int().nonnegative().optional(),
9603
+ disk_min_free_bytes: z6.number().int().nonnegative().nullable().optional(),
9604
+ configured_priority: z6.number().nullable().optional()
9605
+ }).strict();
9386
9606
  function parseCsv(raw) {
9387
9607
  if (!raw?.trim()) {
9388
9608
  return [];
9389
9609
  }
9390
9610
  return raw.split(",").map((part) => part.trim()).filter(Boolean);
9391
9611
  }
9392
- function parseGitAllowlist(overrides) {
9393
- if (overrides) {
9394
- return overrides;
9612
+ function envSet(key, env = process.env) {
9613
+ return env[key] !== void 0;
9614
+ }
9615
+ function parseEnvNumber(key, raw) {
9616
+ const n = Number(raw);
9617
+ if (!Number.isFinite(n)) {
9618
+ throw new Error(`Invalid ${key}=${JSON.stringify(raw)}: expected a finite number`);
9619
+ }
9620
+ return n;
9621
+ }
9622
+ function parseEnvInt(key, raw) {
9623
+ const n = parseEnvNumber(key, raw);
9624
+ if (!Number.isInteger(n)) {
9625
+ throw new Error(`Invalid ${key}=${JSON.stringify(raw)}: expected an integer`);
9395
9626
  }
9396
- const schemes = parseCsv(process.env.RBO_GIT_ALLOWLIST_SCHEMES);
9397
- const hosts = parseCsv(process.env.RBO_GIT_ALLOWLIST_HOSTS);
9398
- const prefixesRaw = process.env.RBO_GIT_ALLOWLIST_PREFIXES?.trim();
9627
+ return n;
9628
+ }
9629
+ function parseGitAllowlistFromEnv(env = process.env) {
9630
+ if (!envSet("RBO_GIT_ALLOWLIST_SCHEMES", env) && !envSet("RBO_GIT_ALLOWLIST_HOSTS", env) && !envSet("RBO_GIT_ALLOWLIST_PREFIXES", env)) {
9631
+ return void 0;
9632
+ }
9633
+ const schemes = parseCsv(env.RBO_GIT_ALLOWLIST_SCHEMES);
9634
+ const hosts = parseCsv(env.RBO_GIT_ALLOWLIST_HOSTS);
9635
+ const prefixesRaw = env.RBO_GIT_ALLOWLIST_PREFIXES?.trim();
9399
9636
  let repository_prefixes;
9400
9637
  if (prefixesRaw) {
9401
9638
  try {
@@ -9414,17 +9651,23 @@ function parseGitAllowlist(overrides) {
9414
9651
  ...repository_prefixes ? { repository_prefixes } : {}
9415
9652
  };
9416
9653
  }
9417
- function parseRepoCache(overrides) {
9418
- if (overrides) {
9419
- return overrides;
9654
+ function mergeGitAllowlist(file, fromEnv, override) {
9655
+ if (override) {
9656
+ return override;
9657
+ }
9658
+ if (fromEnv) {
9659
+ return fromEnv;
9660
+ }
9661
+ if (file) {
9662
+ return {
9663
+ schemes: file.schemes && file.schemes.length > 0 ? file.schemes : [...DEFAULT_GIT_ALLOWLIST_SCHEMES],
9664
+ hosts: file.hosts && file.hosts.length > 0 ? file.hosts : [...DEFAULT_GIT_ALLOWLIST_HOSTS],
9665
+ ...file.repository_prefixes ? { repository_prefixes: file.repository_prefixes } : {}
9666
+ };
9420
9667
  }
9421
- const maxSize = Number(process.env.RBO_REPO_CACHE_MAX_SIZE_GB ?? DEFAULT_REPO_CACHE_CONFIG.max_size_gb);
9422
- const minFree = Number(process.env.RBO_REPO_CACHE_MIN_FREE_DISK_GB ?? DEFAULT_REPO_CACHE_CONFIG.min_free_disk_gb);
9423
- const retention = Number(process.env.RBO_REPO_CACHE_RETENTION_DAYS ?? DEFAULT_REPO_CACHE_CONFIG.retention_days);
9424
9668
  return {
9425
- max_size_gb: maxSize,
9426
- min_free_disk_gb: minFree,
9427
- retention_days: retention
9669
+ schemes: [...DEFAULT_GIT_ALLOWLIST_SCHEMES],
9670
+ hosts: [...DEFAULT_GIT_ALLOWLIST_HOSTS]
9428
9671
  };
9429
9672
  }
9430
9673
  function parseBuildCacheKinds(raw) {
@@ -9455,23 +9698,30 @@ function parseRiskLevels(raw) {
9455
9698
  }
9456
9699
  return levels;
9457
9700
  }
9458
- function parseBuildCache(overrides) {
9459
- if (overrides) {
9460
- return overrides;
9701
+ function mergeRepoCache(file, override) {
9702
+ if (override) {
9703
+ return override;
9461
9704
  }
9705
+ const maxSize = envSet("RBO_REPO_CACHE_MAX_SIZE_GB") ? parseEnvNumber("RBO_REPO_CACHE_MAX_SIZE_GB", process.env.RBO_REPO_CACHE_MAX_SIZE_GB) : file?.max_size_gb ?? DEFAULT_REPO_CACHE_CONFIG.max_size_gb;
9706
+ const minFree = envSet("RBO_REPO_CACHE_MIN_FREE_DISK_GB") ? parseEnvNumber("RBO_REPO_CACHE_MIN_FREE_DISK_GB", process.env.RBO_REPO_CACHE_MIN_FREE_DISK_GB) : file?.min_free_disk_gb ?? DEFAULT_REPO_CACHE_CONFIG.min_free_disk_gb;
9707
+ const retention = envSet("RBO_REPO_CACHE_RETENTION_DAYS") ? parseEnvNumber("RBO_REPO_CACHE_RETENTION_DAYS", process.env.RBO_REPO_CACHE_RETENTION_DAYS) : file?.retention_days ?? DEFAULT_REPO_CACHE_CONFIG.retention_days;
9462
9708
  return {
9463
- enabledKinds: parseBuildCacheKinds(process.env.RBO_BUILD_CACHE_ENABLED_KINDS) ?? [
9464
- ...DEFAULT_BUILD_CACHE_CONFIG.enabledKinds
9465
- ],
9466
- maxSizeGb: Number(process.env.RBO_BUILD_CACHE_MAX_SIZE_GB ?? DEFAULT_BUILD_CACHE_CONFIG.maxSizeGb),
9467
- minFreeDiskGb: Number(process.env.RBO_BUILD_CACHE_MIN_FREE_DISK_GB ?? DEFAULT_BUILD_CACHE_CONFIG.minFreeDiskGb),
9468
- retentionDays: Number(process.env.RBO_BUILD_CACHE_RETENTION_DAYS ?? DEFAULT_BUILD_CACHE_CONFIG.retentionDays),
9469
- allowReadRiskLevels: parseRiskLevels(process.env.RBO_BUILD_CACHE_ALLOW_READ_RISKS) ?? [
9470
- ...DEFAULT_BUILD_CACHE_CONFIG.allowReadRiskLevels
9471
- ],
9472
- allowWriteRiskLevels: parseRiskLevels(process.env.RBO_BUILD_CACHE_ALLOW_WRITE_RISKS) ?? [
9473
- ...DEFAULT_BUILD_CACHE_CONFIG.allowWriteRiskLevels
9474
- ]
9709
+ max_size_gb: maxSize,
9710
+ min_free_disk_gb: minFree,
9711
+ retention_days: retention
9712
+ };
9713
+ }
9714
+ function mergeBuildCache(file, override) {
9715
+ if (override) {
9716
+ return override;
9717
+ }
9718
+ return {
9719
+ enabledKinds: (envSet("RBO_BUILD_CACHE_ENABLED_KINDS") ? parseBuildCacheKinds(process.env.RBO_BUILD_CACHE_ENABLED_KINDS) : void 0) ?? file?.enabled_kinds ?? [...DEFAULT_BUILD_CACHE_CONFIG.enabledKinds],
9720
+ maxSizeGb: envSet("RBO_BUILD_CACHE_MAX_SIZE_GB") ? parseEnvNumber("RBO_BUILD_CACHE_MAX_SIZE_GB", process.env.RBO_BUILD_CACHE_MAX_SIZE_GB) : file?.max_size_gb ?? DEFAULT_BUILD_CACHE_CONFIG.maxSizeGb,
9721
+ minFreeDiskGb: envSet("RBO_BUILD_CACHE_MIN_FREE_DISK_GB") ? parseEnvNumber("RBO_BUILD_CACHE_MIN_FREE_DISK_GB", process.env.RBO_BUILD_CACHE_MIN_FREE_DISK_GB) : file?.min_free_disk_gb ?? DEFAULT_BUILD_CACHE_CONFIG.minFreeDiskGb,
9722
+ retentionDays: envSet("RBO_BUILD_CACHE_RETENTION_DAYS") ? parseEnvNumber("RBO_BUILD_CACHE_RETENTION_DAYS", process.env.RBO_BUILD_CACHE_RETENTION_DAYS) : file?.retention_days ?? DEFAULT_BUILD_CACHE_CONFIG.retentionDays,
9723
+ allowReadRiskLevels: (envSet("RBO_BUILD_CACHE_ALLOW_READ_RISKS") ? parseRiskLevels(process.env.RBO_BUILD_CACHE_ALLOW_READ_RISKS) : void 0) ?? file?.allow_read_risk_levels ?? [...DEFAULT_BUILD_CACHE_CONFIG.allowReadRiskLevels],
9724
+ allowWriteRiskLevels: (envSet("RBO_BUILD_CACHE_ALLOW_WRITE_RISKS") ? parseRiskLevels(process.env.RBO_BUILD_CACHE_ALLOW_WRITE_RISKS) : void 0) ?? file?.allow_write_risk_levels ?? [...DEFAULT_BUILD_CACHE_CONFIG.allowWriteRiskLevels]
9475
9725
  };
9476
9726
  }
9477
9727
  function parseSecretMap(raw) {
@@ -9495,50 +9745,157 @@ function parseSecretMap(raw) {
9495
9745
  throw new Error(`Invalid RBO_SECRET_MAP: ${error instanceof Error ? error.message : String(error)}`);
9496
9746
  }
9497
9747
  }
9748
+ function defaultAgentConfigFile(options = {}) {
9749
+ return {
9750
+ schema_version: AGENT_CONFIG_SCHEMA_VERSION,
9751
+ initialized_at: options.initializedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
9752
+ controller_url: "",
9753
+ controller_fingerprint: "",
9754
+ display_name: "rbo-agent",
9755
+ max_jobs: 1,
9756
+ repo_cache_dir: null,
9757
+ secret_map: null,
9758
+ git_allowlist: {
9759
+ schemes: [...DEFAULT_GIT_ALLOWLIST_SCHEMES],
9760
+ hosts: [...DEFAULT_GIT_ALLOWLIST_HOSTS]
9761
+ },
9762
+ repo_cache: {
9763
+ max_size_gb: DEFAULT_REPO_CACHE_CONFIG.max_size_gb,
9764
+ min_free_disk_gb: DEFAULT_REPO_CACHE_CONFIG.min_free_disk_gb,
9765
+ retention_days: DEFAULT_REPO_CACHE_CONFIG.retention_days
9766
+ },
9767
+ build_cache: {
9768
+ enabled_kinds: [...DEFAULT_BUILD_CACHE_CONFIG.enabledKinds],
9769
+ max_size_gb: DEFAULT_BUILD_CACHE_CONFIG.maxSizeGb,
9770
+ min_free_disk_gb: DEFAULT_BUILD_CACHE_CONFIG.minFreeDiskGb,
9771
+ retention_days: DEFAULT_BUILD_CACHE_CONFIG.retentionDays,
9772
+ allow_read_risk_levels: [...DEFAULT_BUILD_CACHE_CONFIG.allowReadRiskLevels],
9773
+ allow_write_risk_levels: [...DEFAULT_BUILD_CACHE_CONFIG.allowWriteRiskLevels]
9774
+ },
9775
+ log_spool_max_bytes: DEFAULT_LOG_SPOOL_MAX_BYTES,
9776
+ log_send_queue_max: DEFAULT_LOG_SEND_QUEUE_MAX,
9777
+ disconnect_grace_seconds: 60,
9778
+ orphan_timeout_seconds: 300,
9779
+ reconcile_deadline_seconds: 120,
9780
+ disk_min_free_bytes: null,
9781
+ configured_priority: null
9782
+ };
9783
+ }
9784
+ function resolveAgentConfigPath(stateDir) {
9785
+ return join7(stateDir, AGENT_CONFIG_FILENAME);
9786
+ }
9787
+ function readAgentConfigFile(configPath) {
9788
+ if (!existsSync2(configPath)) {
9789
+ return void 0;
9790
+ }
9791
+ let raw;
9792
+ try {
9793
+ raw = JSON.parse(readFileSync3(configPath, "utf8"));
9794
+ } catch (error) {
9795
+ throw new Error(`Invalid agent config JSON at ${configPath}: ${error instanceof Error ? error.message : String(error)}`);
9796
+ }
9797
+ const parsed = AgentConfigFileSchema.safeParse(raw);
9798
+ if (!parsed.success) {
9799
+ throw new Error(`Invalid agent config at ${configPath}: ${parsed.error.message}`);
9800
+ }
9801
+ return parsed.data;
9802
+ }
9803
+ function writeDefaultAgentConfigFile(stateDir, options = {}) {
9804
+ mkdirSync2(stateDir, { recursive: true });
9805
+ const path = resolveAgentConfigPath(stateDir);
9806
+ if (existsSync2(path) && !options.force) {
9807
+ let initialized_at2 = (/* @__PURE__ */ new Date(0)).toISOString();
9808
+ let schema_version = AGENT_CONFIG_SCHEMA_VERSION;
9809
+ try {
9810
+ const existing = readAgentConfigFile(path);
9811
+ initialized_at2 = existing?.initialized_at ?? initialized_at2;
9812
+ schema_version = existing?.schema_version ?? schema_version;
9813
+ } catch {
9814
+ }
9815
+ return {
9816
+ path,
9817
+ written: false,
9818
+ initialized_at: initialized_at2,
9819
+ schema_version
9820
+ };
9821
+ }
9822
+ const initialized_at = options.initializedAt ?? (/* @__PURE__ */ new Date()).toISOString();
9823
+ const body = `${JSON.stringify(defaultAgentConfigFile({ initializedAt: initialized_at }), null, 2)}
9824
+ `;
9825
+ writeFileSync2(path, body, "utf8");
9826
+ return {
9827
+ path,
9828
+ written: true,
9829
+ initialized_at,
9830
+ schema_version: AGENT_CONFIG_SCHEMA_VERSION
9831
+ };
9832
+ }
9498
9833
  function loadAgentConfig(overrides = {}) {
9499
- const controllerUrl = overrides.controllerUrl ?? process.env.RBO_CONTROLLER_URL;
9500
- const controllerFingerprint = overrides.controllerFingerprint ?? process.env.RBO_CONTROLLER_FINGERPRINT;
9834
+ const { configPath: configPathOption, ...fieldOverrides } = overrides;
9835
+ const stateDir = fieldOverrides.stateDir ?? resolveAgentStateDir();
9836
+ const configPath = configPathOption === void 0 ? resolveAgentConfigPath(stateDir) : configPathOption;
9837
+ const file = configPath === null ? void 0 : readAgentConfigFile(configPath);
9838
+ const controllerUrl = fieldOverrides.controllerUrl ?? (envSet("RBO_CONTROLLER_URL") ? process.env.RBO_CONTROLLER_URL : void 0) ?? (file?.controller_url?.trim() ? file.controller_url.trim() : void 0);
9839
+ const controllerFingerprint = fieldOverrides.controllerFingerprint ?? (envSet("RBO_CONTROLLER_FINGERPRINT") ? process.env.RBO_CONTROLLER_FINGERPRINT : void 0) ?? (file?.controller_fingerprint?.trim() ? file.controller_fingerprint.trim() : void 0);
9501
9840
  if (!controllerUrl) {
9502
- throw new Error("RBO_CONTROLLER_URL is required (wss://<host>:7411/agent)");
9841
+ throw new Error("controller_url is required \u2014 set it in agent.json (or RBO_CONTROLLER_URL=wss://<host>:7411/agent)");
9503
9842
  }
9504
9843
  if (!controllerFingerprint) {
9505
- throw new Error("RBO_CONTROLLER_FINGERPRINT is required \u2014 run `rbo controller fingerprint` on the Controller and copy the value here");
9844
+ throw new Error("controller_fingerprint is required \u2014 set it in agent.json (or RBO_CONTROLLER_FINGERPRINT from `rbo controller fingerprint`)");
9845
+ }
9846
+ const repoCache = mergeRepoCache(file?.repo_cache, fieldOverrides.repoCache);
9847
+ let secretMap = fieldOverrides.secretMap;
9848
+ if (secretMap === void 0) {
9849
+ if (envSet("RBO_SECRET_MAP")) {
9850
+ secretMap = parseSecretMap(process.env.RBO_SECRET_MAP);
9851
+ } else if (file?.secret_map !== void 0) {
9852
+ secretMap = file.secret_map ?? void 0;
9853
+ }
9854
+ }
9855
+ let repoCacheDir = fieldOverrides.repoCacheDir;
9856
+ if (repoCacheDir === void 0) {
9857
+ if (envSet("RBO_REPO_CACHE_DIR") && process.env.RBO_REPO_CACHE_DIR?.trim()) {
9858
+ repoCacheDir = process.env.RBO_REPO_CACHE_DIR.trim();
9859
+ } else if (file?.repo_cache_dir) {
9860
+ repoCacheDir = file.repo_cache_dir;
9861
+ }
9862
+ }
9863
+ let configuredPriority = fieldOverrides.configuredPriority;
9864
+ if (configuredPriority === void 0) {
9865
+ if (envSet("RBO_CONFIGURED_PRIORITY")) {
9866
+ configuredPriority = parseEnvNumber("RBO_CONFIGURED_PRIORITY", process.env.RBO_CONFIGURED_PRIORITY);
9867
+ } else if (file?.configured_priority !== void 0 && file.configured_priority !== null) {
9868
+ configuredPriority = file.configured_priority;
9869
+ }
9870
+ }
9871
+ let diskMinFreeBytes = fieldOverrides.diskMinFreeBytes;
9872
+ if (diskMinFreeBytes === void 0) {
9873
+ if (envSet("RBO_DISK_MIN_FREE_BYTES")) {
9874
+ diskMinFreeBytes = parseEnvInt("RBO_DISK_MIN_FREE_BYTES", process.env.RBO_DISK_MIN_FREE_BYTES);
9875
+ } else if (file?.disk_min_free_bytes !== void 0 && file.disk_min_free_bytes !== null) {
9876
+ diskMinFreeBytes = file.disk_min_free_bytes;
9877
+ } else {
9878
+ diskMinFreeBytes = Math.max(DEFAULT_DISK_MIN_FREE_BYTES, Math.floor(repoCache.min_free_disk_gb * 1024 ** 3));
9879
+ }
9506
9880
  }
9507
9881
  return {
9508
9882
  controllerUrl,
9509
9883
  controllerFingerprint,
9510
- displayName: overrides.displayName ?? process.env.RBO_AGENT_NAME ?? "rbo-agent",
9511
- maxJobs: overrides.maxJobs ?? Number(process.env.RBO_MAX_JOBS ?? 1),
9512
- stateDir: overrides.stateDir ?? resolveAgentStateDir(),
9513
- repoCacheDir: overrides.repoCacheDir ?? (process.env.RBO_REPO_CACHE_DIR?.trim() ? process.env.RBO_REPO_CACHE_DIR.trim() : void 0),
9514
- secretMap: overrides.secretMap ?? parseSecretMap(process.env.RBO_SECRET_MAP),
9515
- gitAllowlist: parseGitAllowlist(overrides.gitAllowlist),
9516
- repoCache: parseRepoCache(overrides.repoCache),
9517
- buildCache: parseBuildCache(overrides.buildCache),
9518
- logSpoolMaxBytes: overrides.logSpoolMaxBytes ?? Number(process.env.RBO_LOG_SPOOL_MAX_BYTES ?? DEFAULT_LOG_SPOOL_MAX_BYTES),
9519
- logSendQueueMax: overrides.logSendQueueMax ?? Number(process.env.RBO_LOG_SEND_QUEUE_MAX ?? DEFAULT_LOG_SEND_QUEUE_MAX),
9520
- disconnectGraceSeconds: overrides.disconnectGraceSeconds ?? Number(process.env.RBO_DISCONNECT_GRACE_SECONDS ?? 60),
9521
- orphanTimeoutSeconds: overrides.orphanTimeoutSeconds ?? Number(process.env.RBO_ORPHAN_TIMEOUT_SECONDS ?? 300),
9522
- reconcileDeadlineSeconds: overrides.reconcileDeadlineSeconds ?? Number(process.env.RBO_RECONCILE_DEADLINE_SECONDS ?? 120),
9523
- diskMinFreeBytes: (() => {
9524
- if (overrides.diskMinFreeBytes !== void 0) {
9525
- return overrides.diskMinFreeBytes;
9526
- }
9527
- if (process.env.RBO_DISK_MIN_FREE_BYTES) {
9528
- return Number(process.env.RBO_DISK_MIN_FREE_BYTES);
9529
- }
9530
- const repoCache = parseRepoCache(overrides.repoCache);
9531
- return Math.max(DEFAULT_DISK_MIN_FREE_BYTES, Math.floor(repoCache.min_free_disk_gb * 1024 ** 3));
9532
- })(),
9533
- configuredPriority: (() => {
9534
- if (overrides.configuredPriority !== void 0) {
9535
- return overrides.configuredPriority;
9536
- }
9537
- if (process.env.RBO_CONFIGURED_PRIORITY) {
9538
- return Number(process.env.RBO_CONFIGURED_PRIORITY);
9539
- }
9540
- return void 0;
9541
- })()
9884
+ displayName: fieldOverrides.displayName ?? (envSet("RBO_AGENT_NAME") ? process.env.RBO_AGENT_NAME : void 0) ?? file?.display_name ?? "rbo-agent",
9885
+ maxJobs: fieldOverrides.maxJobs ?? (envSet("RBO_MAX_JOBS") ? parseEnvInt("RBO_MAX_JOBS", process.env.RBO_MAX_JOBS) : void 0) ?? file?.max_jobs ?? 1,
9886
+ stateDir,
9887
+ repoCacheDir,
9888
+ secretMap,
9889
+ gitAllowlist: mergeGitAllowlist(file?.git_allowlist, parseGitAllowlistFromEnv(), fieldOverrides.gitAllowlist),
9890
+ repoCache,
9891
+ buildCache: mergeBuildCache(file?.build_cache, fieldOverrides.buildCache),
9892
+ logSpoolMaxBytes: fieldOverrides.logSpoolMaxBytes ?? (envSet("RBO_LOG_SPOOL_MAX_BYTES") ? parseEnvInt("RBO_LOG_SPOOL_MAX_BYTES", process.env.RBO_LOG_SPOOL_MAX_BYTES) : void 0) ?? file?.log_spool_max_bytes ?? DEFAULT_LOG_SPOOL_MAX_BYTES,
9893
+ logSendQueueMax: fieldOverrides.logSendQueueMax ?? (envSet("RBO_LOG_SEND_QUEUE_MAX") ? parseEnvInt("RBO_LOG_SEND_QUEUE_MAX", process.env.RBO_LOG_SEND_QUEUE_MAX) : void 0) ?? file?.log_send_queue_max ?? DEFAULT_LOG_SEND_QUEUE_MAX,
9894
+ disconnectGraceSeconds: fieldOverrides.disconnectGraceSeconds ?? (envSet("RBO_DISCONNECT_GRACE_SECONDS") ? parseEnvInt("RBO_DISCONNECT_GRACE_SECONDS", process.env.RBO_DISCONNECT_GRACE_SECONDS) : void 0) ?? file?.disconnect_grace_seconds ?? 60,
9895
+ orphanTimeoutSeconds: fieldOverrides.orphanTimeoutSeconds ?? (envSet("RBO_ORPHAN_TIMEOUT_SECONDS") ? parseEnvInt("RBO_ORPHAN_TIMEOUT_SECONDS", process.env.RBO_ORPHAN_TIMEOUT_SECONDS) : void 0) ?? file?.orphan_timeout_seconds ?? 300,
9896
+ reconcileDeadlineSeconds: fieldOverrides.reconcileDeadlineSeconds ?? (envSet("RBO_RECONCILE_DEADLINE_SECONDS") ? parseEnvInt("RBO_RECONCILE_DEADLINE_SECONDS", process.env.RBO_RECONCILE_DEADLINE_SECONDS) : void 0) ?? file?.reconcile_deadline_seconds ?? 120,
9897
+ diskMinFreeBytes,
9898
+ configuredPriority
9542
9899
  };
9543
9900
  }
9544
9901
  function resolveRepoCacheRoot(config) {
@@ -9554,7 +9911,15 @@ function ensureStateDir(config) {
9554
9911
  mkdirSync2(config.stateDir, { recursive: true });
9555
9912
  }
9556
9913
 
9914
+ // ../agent/dist/run.js
9915
+ init_dist();
9916
+
9557
9917
  // ../agent/dist/capabilities/probe.js
9918
+ import { execFile as execFile2 } from "node:child_process";
9919
+ import { readFile as readFile5, readdir as readdir5, statfs } from "node:fs/promises";
9920
+ import { arch, cpus as cpus2, freemem, hostname, loadavg, platform as platform2, release, totalmem } from "node:os";
9921
+ import { join as join8 } from "node:path";
9922
+ import { promisify as promisify2 } from "node:util";
9558
9923
  var execFileAsync2 = promisify2(execFile2);
9559
9924
  function osFamily() {
9560
9925
  const p = platform2();
@@ -9704,7 +10069,7 @@ async function probeCapabilities(input) {
9704
10069
  // ../agent/dist/connection/client.js
9705
10070
  init_dist2();
9706
10071
  init_dist();
9707
- import { existsSync as existsSync4, mkdirSync as mkdirSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "node:fs";
10072
+ import { existsSync as existsSync5, mkdirSync as mkdirSync4, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "node:fs";
9708
10073
  import { hostname as hostname2 } from "node:os";
9709
10074
  import { join as join22 } from "node:path";
9710
10075
  import WebSocket from "ws";
@@ -9808,7 +10173,7 @@ init_dist4();
9808
10173
  init_dist();
9809
10174
  init_dist3();
9810
10175
  import { createHash as createHash6 } from "node:crypto";
9811
- import { createReadStream, existsSync as existsSync3 } from "node:fs";
10176
+ import { createReadStream, existsSync as existsSync4 } from "node:fs";
9812
10177
  import { copyFile, mkdir as mkdir9, readFile as readFile13, rename as rename4, rm as rm9, stat as stat7 } from "node:fs/promises";
9813
10178
  import { request as httpsRequest } from "node:https";
9814
10179
  import { join as join21 } from "node:path";
@@ -9911,7 +10276,7 @@ var SpoolSender = class {
9911
10276
 
9912
10277
  // ../agent/dist/recovery/attempt-metadata.js
9913
10278
  init_dist();
9914
- import { mkdirSync as mkdirSync3, readFileSync as readFileSync3, renameSync, writeFileSync as writeFileSync2 } from "node:fs";
10279
+ import { mkdirSync as mkdirSync3, readFileSync as readFileSync4, renameSync, writeFileSync as writeFileSync3 } from "node:fs";
9915
10280
  import { readdir as readdir10, rm as rm6 } from "node:fs/promises";
9916
10281
  import { dirname as dirname5, join as join18 } from "node:path";
9917
10282
  function attemptMetadataDir(stateDir, attemptId) {
@@ -9929,14 +10294,14 @@ function writeAttemptMetadata(stateDir, meta) {
9929
10294
  ...meta,
9930
10295
  updated_at: meta.updated_at || (/* @__PURE__ */ new Date()).toISOString()
9931
10296
  };
9932
- writeFileSync2(tmp, `${JSON.stringify(payload, null, 2)}
10297
+ writeFileSync3(tmp, `${JSON.stringify(payload, null, 2)}
9933
10298
  `, { encoding: "utf8", mode: 384 });
9934
10299
  renameSync(tmp, target);
9935
10300
  }
9936
10301
  function readAttemptMetadata(stateDir, attemptId) {
9937
10302
  const path = attemptMetadataPath(stateDir, attemptId);
9938
10303
  try {
9939
- const raw = readFileSync3(path, "utf8");
10304
+ const raw = readFileSync4(path, "utf8");
9940
10305
  return JSON.parse(raw);
9941
10306
  } catch {
9942
10307
  return null;
@@ -10084,8 +10449,8 @@ async function applyDiskPressureCleanup(options) {
10084
10449
  for (const name of entries) {
10085
10450
  const metaPath = join19(reposDir, name, "metadata.json");
10086
10451
  try {
10087
- const { readFile: readFile20 } = await import("node:fs/promises");
10088
- const raw = await readFile20(metaPath, "utf8");
10452
+ const { readFile: readFile19 } = await import("node:fs/promises");
10453
+ const raw = await readFile19(metaPath, "utf8");
10089
10454
  const meta = JSON.parse(raw);
10090
10455
  if ((meta.active_worktree_count ?? 0) > 0) {
10091
10456
  continue;
@@ -10299,6 +10664,49 @@ async function streamDownloadWithLimits(source, destPath, expectedSize, expected
10299
10664
  }
10300
10665
  }
10301
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
+
10302
10710
  // ../agent/dist/executor/index.js
10303
10711
  var logger4 = createLogger("agent.executor");
10304
10712
  var ARTIFACT_TOKEN_TIMEOUT_MS = 6e4;
@@ -10320,18 +10728,6 @@ function resolveProjectIdentity(offer, prepare) {
10320
10728
  }
10321
10729
  return `local:${offer.snapshot_metadata.content_id}`;
10322
10730
  }
10323
- function assertPinnedPeerCert(res, expectedFingerprint) {
10324
- const socket = res.socket;
10325
- const cert = socket.getPeerCertificate?.(true);
10326
- if (!cert?.raw) {
10327
- return new Error("Controller did not present a TLS certificate");
10328
- }
10329
- const actual = certificateFingerprint(cert.raw);
10330
- if (actual !== expectedFingerprint) {
10331
- return new Error(`Controller certificate fingerprint mismatch: expected ${expectedFingerprint}, got ${actual}`);
10332
- }
10333
- return void 0;
10334
- }
10335
10731
  var AgentJobExecutor = class {
10336
10732
  socket;
10337
10733
  config;
@@ -11067,7 +11463,7 @@ var AgentJobExecutor = class {
11067
11463
  const currentProfiles = this.config.toolchainProfiles ?? [];
11068
11464
  for (const profile of offer.selected_toolchain_profiles ?? []) {
11069
11465
  const activationPath = profile.activation.path;
11070
- if (activationPath && !existsSync3(activationPath)) {
11466
+ if (activationPath && !existsSync4(activationPath)) {
11071
11467
  this.failTerminal(run.attempt_id, run.lease_id, run.lease_epoch, "toolchain_changed", `Selected toolchain path missing: ${activationPath}`);
11072
11468
  await rm9(attemptDir, { recursive: true, force: true }).catch(() => void 0);
11073
11469
  this.clearAttempt();
@@ -11494,22 +11890,6 @@ var AgentJobExecutor = class {
11494
11890
  this.sendFrame("artifact_manifest", attemptId, leaseId, leaseEpoch, manifest);
11495
11891
  });
11496
11892
  }
11497
- pinnedTlsOptions() {
11498
- const expected = this.config.controllerFingerprint;
11499
- return {
11500
- rejectUnauthorized: false,
11501
- checkServerIdentity: (_host, cert) => {
11502
- if (!cert.raw) {
11503
- return new Error("Controller did not present a TLS certificate");
11504
- }
11505
- const actual = certificateFingerprint(cert.raw);
11506
- if (actual !== expected) {
11507
- return new Error(`Controller certificate fingerprint mismatch: expected ${expected}, got ${actual}`);
11508
- }
11509
- return void 0;
11510
- }
11511
- };
11512
- }
11513
11893
  downloadSnapshotFile(downloadUrl, dataToken, destPath, expectedSize, expectedSha256) {
11514
11894
  return new Promise((resolvePromise, rejectPromise) => {
11515
11895
  const req = httpsRequest(downloadUrl, {
@@ -11517,7 +11897,7 @@ var AgentJobExecutor = class {
11517
11897
  headers: {
11518
11898
  Authorization: `Bearer ${dataToken}`
11519
11899
  },
11520
- ...this.pinnedTlsOptions()
11900
+ ...pinnedTlsRequestOptions(this.config.controllerFingerprint)
11521
11901
  }, (res) => {
11522
11902
  const pinError = assertPinnedPeerCert(res, this.config.controllerFingerprint);
11523
11903
  if (pinError) {
@@ -11546,7 +11926,7 @@ var AgentJobExecutor = class {
11546
11926
  "X-RBO-SHA256": sha2562,
11547
11927
  "X-RBO-Artifact-Name": logicalName
11548
11928
  },
11549
- ...this.pinnedTlsOptions()
11929
+ ...pinnedTlsRequestOptions(this.config.controllerFingerprint)
11550
11930
  }, (res) => {
11551
11931
  const pinError = assertPinnedPeerCert(res, this.config.controllerFingerprint);
11552
11932
  if (pinError) {
@@ -11839,8 +12219,8 @@ var AgentConnection = class {
11839
12219
  return join22(this.options.stateDir, "agent-state.json");
11840
12220
  }
11841
12221
  loadState() {
11842
- if (existsSync4(this.statePath())) {
11843
- return JSON.parse(readFileSync4(this.statePath(), "utf8"));
12222
+ if (existsSync5(this.statePath())) {
12223
+ return JSON.parse(readFileSync5(this.statePath(), "utf8"));
11844
12224
  }
11845
12225
  const pair = generateDeviceKeyPair();
11846
12226
  const state = {
@@ -11852,7 +12232,7 @@ var AgentConnection = class {
11852
12232
  }
11853
12233
  saveState(state) {
11854
12234
  mkdirSync4(this.options.stateDir, { recursive: true });
11855
- writeFileSync3(this.statePath(), JSON.stringify(state), { mode: 384 });
12235
+ writeFileSync4(this.statePath(), JSON.stringify(state), { mode: 384 });
11856
12236
  }
11857
12237
  hasStoredCredential() {
11858
12238
  const state = this.loadState();
@@ -12329,7 +12709,7 @@ init_dist();
12329
12709
 
12330
12710
  // src/commands/daemon.ts
12331
12711
  import { spawn as nodeSpawn } from "node:child_process";
12332
- import { existsSync as existsSync5, openSync, readFileSync as readFileSync5 } from "node:fs";
12712
+ import { existsSync as existsSync6, openSync, readFileSync as readFileSync6 } from "node:fs";
12333
12713
  import { mkdir as mkdir10, writeFile as writeFile10 } from "node:fs/promises";
12334
12714
  import { dirname as dirname6, join as join23 } from "node:path";
12335
12715
  function stripDaemonFlag(args) {
@@ -12360,10 +12740,10 @@ function isProcessAlive(pid) {
12360
12740
  }
12361
12741
  }
12362
12742
  function assertNoLivePid(pidFile, label) {
12363
- if (!existsSync5(pidFile)) {
12743
+ if (!existsSync6(pidFile)) {
12364
12744
  return;
12365
12745
  }
12366
- const raw = readFileSync5(pidFile, "utf8").trim();
12746
+ const raw = readFileSync6(pidFile, "utf8").trim();
12367
12747
  const pid = Number.parseInt(raw, 10);
12368
12748
  if (isProcessAlive(pid)) {
12369
12749
  throw new Error(
@@ -12407,36 +12787,21 @@ async function spawnDetachedDaemon(options) {
12407
12787
  }
12408
12788
 
12409
12789
  // src/commands/agent.ts
12410
- var AGENT_MARKER = "agent.json";
12411
- var AGENT_SCHEMA_VERSION = 1;
12412
12790
  function isAgentInitialized(stateDir) {
12413
- return existsSync6(join24(stateDir, AGENT_MARKER));
12791
+ return existsSync7(join24(stateDir, AGENT_CONFIG_FILENAME));
12414
12792
  }
12415
- async function readAgentMarker(stateDir) {
12416
- const markerPath = join24(stateDir, AGENT_MARKER);
12417
- if (!existsSync6(markerPath)) {
12418
- return void 0;
12419
- }
12420
- const marker = JSON.parse(await readFile14(markerPath, "utf8"));
12793
+ async function runAgentInit(options = {}) {
12794
+ const stateDir = options.stateDir ?? resolveAgentStateDir();
12795
+ const result = writeDefaultAgentConfigFile(stateDir, { force: options.force });
12421
12796
  return {
12422
12797
  stateDir,
12423
- initialized_at: marker.initialized_at,
12424
- schema_version: marker.schema_version
12798
+ initialized_at: result.initialized_at,
12799
+ schema_version: result.schema_version,
12800
+ configPath: result.path,
12801
+ configWritten: result.written,
12802
+ ...result.written ? {} : { hint: "agent.json already exists; pass --force to rewrite defaults" }
12425
12803
  };
12426
12804
  }
12427
- async function runAgentInit(options = {}) {
12428
- const stateDir = options.stateDir ?? resolveAgentStateDir();
12429
- await mkdir11(stateDir, { recursive: true });
12430
- const existing = await readAgentMarker(stateDir);
12431
- if (existing) {
12432
- return existing;
12433
- }
12434
- const initialized_at = (/* @__PURE__ */ new Date()).toISOString();
12435
- const marker = { initialized_at, schema_version: AGENT_SCHEMA_VERSION };
12436
- await writeFile11(join24(stateDir, AGENT_MARKER), `${JSON.stringify(marker, null, 2)}
12437
- `, "utf8");
12438
- return { stateDir, ...marker };
12439
- }
12440
12805
  function assertAgentInitialized(stateDir) {
12441
12806
  if (!isAgentInitialized(stateDir)) {
12442
12807
  throw new Error("Agent is not initialized. Run `rbo agent init` first.");
@@ -12476,8 +12841,24 @@ async function postAdmin(baseUrl, action, body) {
12476
12841
  }
12477
12842
  return json;
12478
12843
  }
12479
- function listAgentsRemote(baseUrl) {
12480
- return postAdmin(baseUrl, "agents/list", {});
12844
+ async function listAgentsRemote(baseUrl) {
12845
+ const [agentsResult, pairingResult] = await Promise.all([
12846
+ postAdmin(baseUrl, "agents/list", {}),
12847
+ postAdmin(baseUrl, "pairing/list", {
12848
+ state: "pending"
12849
+ })
12850
+ ]);
12851
+ return {
12852
+ agents: agentsResult.agents,
12853
+ pending_pairings: pairingResult.requests.map((row) => ({
12854
+ id: row.id,
12855
+ display_name: row.display_name,
12856
+ hostname: row.hostname,
12857
+ state: row.state,
12858
+ one_time_code: row.one_time_code,
12859
+ expires_at: row.expires_at
12860
+ }))
12861
+ };
12481
12862
  }
12482
12863
  function approveAgentRemote(baseUrl, pairingRequestId) {
12483
12864
  return postAdmin(baseUrl, "pairing/approve", { pairing_request_id: pairingRequestId });
@@ -12490,8 +12871,9 @@ function probeAgentRemote(baseUrl, agentId) {
12490
12871
  }
12491
12872
 
12492
12873
  // src/commands/controller.ts
12493
- import { existsSync as existsSync7 } from "node:fs";
12494
- import { readFile as readFile18 } from "node:fs/promises";
12874
+ init_config();
12875
+ import { existsSync as existsSync9 } from "node:fs";
12876
+ import { readFile as readFile17 } from "node:fs/promises";
12495
12877
  import { join as join32 } from "node:path";
12496
12878
 
12497
12879
  // ../controller/dist/run.js
@@ -12509,7 +12891,7 @@ init_dist4();
12509
12891
  init_dist2();
12510
12892
  init_dist();
12511
12893
  import { join as join31 } from "node:path";
12512
- import { z as z7 } from "zod";
12894
+ import { z as z9 } from "zod";
12513
12895
 
12514
12896
  // ../controller/dist/agents/probe.js
12515
12897
  function requestAgentProbe(connectedAgents, agentId) {
@@ -12563,6 +12945,189 @@ function listAgents(db, includeOffline) {
12563
12945
  // ../controller/dist/mcp/handlers.js
12564
12946
  init_artifacts2();
12565
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
12566
13131
  init_lifecycle();
12567
13132
  init_submit();
12568
13133
  function runnerContext(ctx) {
@@ -12597,7 +13162,7 @@ function validateToolInput(name, args) {
12597
13162
  if (!def) {
12598
13163
  throw RboError.validation(`Unknown tool '${name}'`);
12599
13164
  }
12600
- const parsed = z7.object(def.inputShape).safeParse(args ?? {});
13165
+ const parsed = z9.object(def.inputShape).safeParse(args ?? {});
12601
13166
  if (!parsed.success) {
12602
13167
  throw RboError.validation(`Invalid input for tool '${name}'`, {
12603
13168
  issues: parsed.error.issues.map((issue) => ({
@@ -12615,6 +13180,21 @@ async function handleToolCall(ctx, name, rawArgs) {
12615
13180
  return { agents: listAgents(ctx.db, args.include_offline === true) };
12616
13181
  case "job_submit":
12617
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);
12618
13198
  case "job_confirm":
12619
13199
  return handleJobConfirm(submitContext(ctx), {
12620
13200
  job_id: args.job_id,
@@ -12722,8 +13302,27 @@ function buildMcpServer(ctx) {
12722
13302
  version: RBO_CONTROLLER_VERSION
12723
13303
  });
12724
13304
  for (const def of MCP_TOOL_DEFS) {
12725
- server.registerTool(def.name, { description: def.description, inputSchema: def.inputShape }, async (args) => {
12726
- 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);
12727
13326
  return {
12728
13327
  content: [{ type: "text", text: JSON.stringify(result) }]
12729
13328
  };
@@ -12745,7 +13344,7 @@ function revokeAgent(db, agentId) {
12745
13344
  }
12746
13345
  }
12747
13346
  function updateAgentCapabilities(db, agentId, report) {
12748
- db.prepare("UPDATE agents SET capabilities_json = ?, hostname = ?, last_seen_at = ? WHERE id = ?").run(JSON.stringify(report), report.hostname, nowIso(), agentId);
13347
+ db.prepare("UPDATE agents SET capabilities_json = ?, hostname = ?, max_jobs = ?, last_seen_at = ? WHERE id = ?").run(JSON.stringify(report), report.hostname, report.execution.max_jobs, nowIso(), agentId);
12749
13348
  }
12750
13349
  function patchAgentCpuLoad(db, agentId, cpuLoad) {
12751
13350
  const row = db.prepare("SELECT capabilities_json FROM agents WHERE id = ?").get(agentId);
@@ -12829,11 +13428,20 @@ function getPairingRequest(db, id) {
12829
13428
  const row = db.prepare("SELECT * FROM pairing_requests WHERE id = ?").get(id);
12830
13429
  return row ?? null;
12831
13430
  }
13431
+ function expireStalePairingRequests(db) {
13432
+ const now = nowIso();
13433
+ const result = db.prepare(`UPDATE pairing_requests
13434
+ SET state = 'expired', resolved_at = COALESCE(resolved_at, ?)
13435
+ WHERE state IN ('pending', 'approved') AND expires_at <= ?`).run(now, now);
13436
+ return result.changes;
13437
+ }
12832
13438
  function listPairingRequests(db, state) {
13439
+ expireStalePairingRequests(db);
12833
13440
  const rows = state ? db.prepare("SELECT * FROM pairing_requests WHERE state = ? ORDER BY created_at").all(state) : db.prepare("SELECT * FROM pairing_requests ORDER BY created_at").all();
12834
13441
  return rows;
12835
13442
  }
12836
13443
  function createPairingRequest(db, input) {
13444
+ expireStalePairingRequests(db);
12837
13445
  const thumbprint = publicKeyThumbprint(input.devicePublicKeyPem);
12838
13446
  const existing = db.prepare("SELECT * FROM pairing_requests WHERE device_thumbprint = ? AND state IN ('pending', 'approved') AND expires_at > ?").get(thumbprint, nowIso());
12839
13447
  if (existing) {
@@ -13445,7 +14053,7 @@ async function startAgentPlaneServer(options) {
13445
14053
  const payload = parsePayload(LogChunkPayloadSchema, message.payload, message.type);
13446
14054
  if (!payload)
13447
14055
  return;
13448
- void handleRemoteLogChunk(remoteOpts(), authenticated.agentId, payload);
14056
+ void enqueueRemoteLogChunk(remoteOpts(), authenticated.agentId, payload);
13449
14057
  return;
13450
14058
  }
13451
14059
  case "job_exit": {
@@ -13609,7 +14217,13 @@ async function runController(overrides = {}) {
13609
14217
  init_dist();
13610
14218
  async function runControllerInit(options) {
13611
14219
  const identity = await ensureControllerIdentity(options.dataDir);
13612
- return { controllerId: identity.controllerId, fingerprint: identity.fingerprint };
14220
+ const config = writeDefaultControllerConfigFile(options.dataDir, { force: options.force });
14221
+ return {
14222
+ controllerId: identity.controllerId,
14223
+ fingerprint: identity.fingerprint,
14224
+ configPath: config.path,
14225
+ configWritten: config.written
14226
+ };
13613
14227
  }
13614
14228
  async function runControllerFingerprint(options) {
13615
14229
  const identity = await ensureControllerIdentity(options.dataDir);
@@ -13618,10 +14232,10 @@ async function runControllerFingerprint(options) {
13618
14232
  async function readExistingControllerId(dataDir) {
13619
14233
  const certPath = join32(dataDir, "security", "controller-cert.pem");
13620
14234
  const metaPath = join32(dataDir, "security", "controller.json");
13621
- if (!existsSync7(certPath) || !existsSync7(metaPath)) {
14235
+ if (!existsSync9(certPath) || !existsSync9(metaPath)) {
13622
14236
  return void 0;
13623
14237
  }
13624
- const meta = JSON.parse(await readFile18(metaPath, "utf8"));
14238
+ const meta = JSON.parse(await readFile17(metaPath, "utf8"));
13625
14239
  return meta.controller_id;
13626
14240
  }
13627
14241
  async function isControllerInitialized(dataDir) {
@@ -13665,11 +14279,25 @@ async function runControllerRestore(options) {
13665
14279
  // src/commands/doctor.ts
13666
14280
  init_dist4();
13667
14281
  import { execFile as execFile9 } from "node:child_process";
13668
- import { mkdirSync as mkdirSync6, writeFileSync as writeFileSync4 } from "node:fs";
14282
+ import { mkdirSync as mkdirSync6, writeFileSync as writeFileSync6 } from "node:fs";
13669
14283
  import { join as join33 } from "node:path";
13670
- import { promisify as promisify9 } from "node:util";
14284
+ import { promisify as promisify9, styleText } from "node:util";
13671
14285
  var execFileAsync9 = promisify9(execFile9);
13672
14286
  var REQUIRED_NODE_ENGINES = { major: 22, minor: 14 };
14287
+ function doctorStatusTag(check) {
14288
+ if (!check.ok) return "FAIL";
14289
+ if (check.warn) return "WARN";
14290
+ return "OK ";
14291
+ }
14292
+ function formatDoctorCheckLine(check, options = {}) {
14293
+ const tag = doctorStatusTag(check);
14294
+ const colorName = !check.ok ? "red" : check.warn ? "yellow" : "green";
14295
+ const styledTag = options.color === false ? tag : styleText(colorName, tag, {
14296
+ stream: options.stream ?? process.stdout,
14297
+ validateStream: options.color !== true
14298
+ });
14299
+ return `${styledTag} ${check.name}: ${check.detail}`;
14300
+ }
13673
14301
  async function checkGit() {
13674
14302
  try {
13675
14303
  const { stdout } = await execFileAsync9("git", ["--version"]);
@@ -13682,7 +14310,7 @@ function checkDataDirWritable(dataDir) {
13682
14310
  try {
13683
14311
  mkdirSync6(dataDir, { recursive: true });
13684
14312
  const probe = join33(dataDir, ".rbo-doctor-probe");
13685
- writeFileSync4(probe, "ok");
14313
+ writeFileSync6(probe, "ok");
13686
14314
  return { name: "data_dir_writable", ok: true, detail: dataDir };
13687
14315
  } catch (error) {
13688
14316
  return { name: "data_dir_writable", ok: false, detail: String(error) };
@@ -13777,6 +14405,18 @@ async function runDoctor(options) {
13777
14405
  }
13778
14406
 
13779
14407
  // src/commands/flags.ts
14408
+ function parseForceFlag(args) {
14409
+ const rest = [];
14410
+ let force = false;
14411
+ for (const arg of args) {
14412
+ if (arg === "--force") {
14413
+ force = true;
14414
+ continue;
14415
+ }
14416
+ rest.push(arg);
14417
+ }
14418
+ return { force, rest };
14419
+ }
13780
14420
  function parseDataDirFlag(args) {
13781
14421
  const rest = [];
13782
14422
  let dataDir;
@@ -13849,6 +14489,7 @@ function cancelJobRemote(baseUrl, jobId, reason) {
13849
14489
  }
13850
14490
 
13851
14491
  // src/commands/service.ts
14492
+ init_dist();
13852
14493
  import { execFile as execFile10 } from "node:child_process";
13853
14494
  import { promisify as promisify10 } from "node:util";
13854
14495
  var execFileAsync10 = promisify10(execFile10);
@@ -13881,14 +14522,21 @@ var defaultCommandRunner = {
13881
14522
  var SERVICE_NAME = "RBOAgent";
13882
14523
  var LAUNCHD_LABEL = "com.rbo.agent";
13883
14524
  var SYSTEMD_UNIT = "rbo-agent";
13884
- function renderServiceInstallPlan(platform3, executablePath = "C:/Program Files/RBO/rbo-agent.exe") {
14525
+ function renderAgentServiceCommand(options = {}) {
14526
+ const nodePath = options.nodePath ?? process.execPath;
14527
+ const rboScriptPath = options.rboScriptPath ?? process.argv[1] ?? "rbo.js";
14528
+ const stateDir = options.stateDir ?? resolveAgentStateDir();
14529
+ return `"${nodePath}" "${rboScriptPath}" agent start --state-dir "${stateDir}"`;
14530
+ }
14531
+ function renderServiceInstallPlan(platform3, options = {}) {
14532
+ const binPath = renderAgentServiceCommand(options);
13885
14533
  switch (platform3) {
13886
14534
  case "win32":
13887
14535
  return {
13888
14536
  kind: "windows_service",
13889
14537
  serviceName: SERVICE_NAME,
13890
14538
  commands: [
13891
- `sc.exe create ${SERVICE_NAME} binPath= "${executablePath}" start= auto`,
14539
+ `sc.exe create ${SERVICE_NAME} binPath= ${JSON.stringify(binPath)} start= auto`,
13892
14540
  `sc.exe description ${SERVICE_NAME} "Remote Build Orchestrator Agent"`,
13893
14541
  `sc.exe start ${SERVICE_NAME}`
13894
14542
  ]
@@ -13898,6 +14546,7 @@ function renderServiceInstallPlan(platform3, executablePath = "C:/Program Files/
13898
14546
  kind: "launchd",
13899
14547
  serviceName: LAUNCHD_LABEL,
13900
14548
  commands: [
14549
+ `# Write /Library/LaunchDaemons/${LAUNCHD_LABEL}.plist with ProgramArguments: ${binPath}`,
13901
14550
  `launchctl load -w /Library/LaunchDaemons/${LAUNCHD_LABEL}.plist`,
13902
14551
  `launchctl start ${LAUNCHD_LABEL}`
13903
14552
  ]
@@ -13907,6 +14556,7 @@ function renderServiceInstallPlan(platform3, executablePath = "C:/Program Files/
13907
14556
  kind: "systemd",
13908
14557
  serviceName: SYSTEMD_UNIT,
13909
14558
  commands: [
14559
+ `# Write /etc/systemd/system/${SYSTEMD_UNIT}.service with ExecStart=${binPath}`,
13910
14560
  "systemctl daemon-reload",
13911
14561
  `systemctl enable ${SYSTEMD_UNIT}`,
13912
14562
  `systemctl start ${SYSTEMD_UNIT}`
@@ -14009,10 +14659,10 @@ function renderServiceStopPlan(platform3) {
14009
14659
  };
14010
14660
  }
14011
14661
  }
14012
- function renderServiceActionPlan(platform3, action, executablePath) {
14662
+ function renderServiceActionPlan(platform3, action, options) {
14013
14663
  switch (action) {
14014
14664
  case "install":
14015
- return renderServiceInstallPlan(platform3, executablePath);
14665
+ return renderServiceInstallPlan(platform3, options);
14016
14666
  case "uninstall":
14017
14667
  return renderServiceUninstallPlan(platform3);
14018
14668
  case "status":
@@ -14027,7 +14677,11 @@ function hasExecuteFlag(args) {
14027
14677
  return args.includes("--execute");
14028
14678
  }
14029
14679
  function formatDryRunPlan(action, plan) {
14030
- const lines = [`# ${action} (dry run \u2014 pass --execute to run these commands)`];
14680
+ const lines = [
14681
+ `# ${action} (dry run \u2014 pass --execute to run these commands)`,
14682
+ "# Prefer `rbo agent start --daemon` for day-to-day use; OS service install is best-effort",
14683
+ "# and expects node + bundled rbo.js (not a Program Files rbo-agent.exe)."
14684
+ ];
14031
14685
  for (const command of plan.commands) {
14032
14686
  lines.push(command);
14033
14687
  }
@@ -14036,6 +14690,10 @@ function formatDryRunPlan(action, plan) {
14036
14690
  async function executeServicePlan(plan, runner = defaultCommandRunner) {
14037
14691
  const results = [];
14038
14692
  for (const command of plan.commands) {
14693
+ if (command.trimStart().startsWith("#")) {
14694
+ results.push({ command, stdout: "", stderr: "skipped comment", code: 0 });
14695
+ continue;
14696
+ }
14039
14697
  const { stdout, stderr, code } = await runner.run(command);
14040
14698
  results.push({ command, stdout, stderr, code });
14041
14699
  if (code !== 0) {
@@ -14067,7 +14725,8 @@ async function main() {
14067
14725
  const dataDir = flagDataDir ?? resolveControllerDataDir();
14068
14726
  const sub = controllerArgs[0];
14069
14727
  if (sub === "init") {
14070
- const result = await runControllerInit({ dataDir });
14728
+ const { force } = parseForceFlag(controllerArgs.slice(1));
14729
+ const result = await runControllerInit({ dataDir, force });
14071
14730
  console.log(JSON.stringify(result, null, 2));
14072
14731
  return;
14073
14732
  }
@@ -14106,7 +14765,7 @@ async function main() {
14106
14765
  }
14107
14766
  case "agents": {
14108
14767
  const result = await listAgentsRemote(controllerUrl);
14109
- console.log(JSON.stringify(result.agents, null, 2));
14768
+ console.log(JSON.stringify(result, null, 2));
14110
14769
  return;
14111
14770
  }
14112
14771
  case "agent": {
@@ -14135,7 +14794,8 @@ async function main() {
14135
14794
  return;
14136
14795
  }
14137
14796
  if (sub === "init") {
14138
- const result = await runAgentInit({ stateDir });
14797
+ const { force } = parseForceFlag(agentArgs.slice(1));
14798
+ const result = await runAgentInit({ stateDir, force });
14139
14799
  console.log(JSON.stringify(result, null, 2));
14140
14800
  return;
14141
14801
  }
@@ -14178,8 +14838,7 @@ async function main() {
14178
14838
  const dataDir = flagDataDir ?? resolveControllerDataDir();
14179
14839
  const report = await runDoctor({ dataDir, controllerUrl });
14180
14840
  for (const check of report.checks) {
14181
- const tag = !check.ok ? "FAIL" : check.warn ? "WARN" : "OK ";
14182
- console.log(`${tag} ${check.name}: ${check.detail}`);
14841
+ console.log(formatDoctorCheckLine(check));
14183
14842
  }
14184
14843
  process.exitCode = report.ok ? 0 : 1;
14185
14844
  return;
@@ -14189,7 +14848,7 @@ async function main() {
14189
14848
  if (!requestPath) {
14190
14849
  throw new Error("Usage: rbo submit <job-request.json>");
14191
14850
  }
14192
- const request = JSON.parse(await readFile19(requestPath, "utf8"));
14851
+ const request = JSON.parse(await readFile18(requestPath, "utf8"));
14193
14852
  const result = await submitJobRemote(controllerUrl, request);
14194
14853
  console.log(JSON.stringify(result, null, 2));
14195
14854
  return;