@madarco/agentbox 0.7.0 → 0.8.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.
Files changed (60) hide show
  1. package/dist/_cloud-attach-T727ZPRV.js +13 -0
  2. package/dist/{chunk-NW5NYTQM.js → chunk-67N47KUS.js} +359 -85
  3. package/dist/chunk-67N47KUS.js.map +1 -0
  4. package/dist/{chunk-NAVL4R34.js → chunk-6OZDFNBF.js} +1084 -516
  5. package/dist/chunk-6OZDFNBF.js.map +1 -0
  6. package/dist/chunk-BGK32PZE.js +455 -0
  7. package/dist/chunk-BGK32PZE.js.map +1 -0
  8. package/dist/{chunk-7KOEFGN2.js → chunk-FODMEHD3.js} +52 -14
  9. package/dist/chunk-FODMEHD3.js.map +1 -0
  10. package/dist/{chunk-UK72UQ5U.js → chunk-G3H2L3O2.js} +55 -4
  11. package/dist/chunk-G3H2L3O2.js.map +1 -0
  12. package/dist/{chunk-V5KZGB5V.js → chunk-LEV3KICD.js} +18 -2
  13. package/dist/chunk-LEV3KICD.js.map +1 -0
  14. package/dist/{cloud-poller-ZIWSADJB-JXFRJUEM.js → cloud-poller-SUNA6ZQC-2RG5WPRN.js} +2 -2
  15. package/dist/{dist-R67WMLCF.js → dist-L4LCG5SJ.js} +120 -10
  16. package/dist/dist-L4LCG5SJ.js.map +1 -0
  17. package/dist/{dist-ETCFRVPA.js → dist-LOZBWMBF.js} +44 -20
  18. package/dist/{dist-QZGJIBT5.js → dist-ZODPD2I6.js} +142 -74
  19. package/dist/dist-ZODPD2I6.js.map +1 -0
  20. package/dist/index.js +3563 -845
  21. package/dist/index.js.map +1 -1
  22. package/dist/prepared-state-CL4CWXQA-ME4HSKDE.js +18 -0
  23. package/dist/prepared-state-CL4CWXQA-ME4HSKDE.js.map +1 -0
  24. package/package.json +4 -4
  25. package/runtime/daytona/custom-system-CLAUDE.md +39 -0
  26. package/runtime/docker/Dockerfile.box +22 -0
  27. package/runtime/docker/apps/cli/share/agentbox-setup/SKILL.md +1 -1
  28. package/runtime/docker/packages/ctl/dist/bin.cjs +1118 -71
  29. package/runtime/docker/packages/sandbox-docker/scripts/agentbox-codex-hooks.json +66 -35
  30. package/runtime/docker/packages/sandbox-docker/scripts/claude-managed-settings.json +62 -1
  31. package/runtime/docker/packages/sandbox-docker/scripts/custom-system-CLAUDE.md +15 -4
  32. package/runtime/docker/packages/sandbox-docker/scripts/gh-shim +263 -0
  33. package/runtime/docker/packages/sandbox-docker/scripts/git-shim +131 -0
  34. package/runtime/docker/packages/sandbox-docker/scripts/opencode-agentbox-plugin.js +76 -0
  35. package/runtime/hetzner/agentbox-codex-hooks.json +66 -35
  36. package/runtime/hetzner/agentbox-setup-skill.md +1 -1
  37. package/runtime/hetzner/claude-managed-settings.json +62 -1
  38. package/runtime/hetzner/ctl.cjs +1118 -71
  39. package/runtime/hetzner/custom-system-CLAUDE.md +26 -14
  40. package/runtime/hetzner/gh-shim +263 -0
  41. package/runtime/hetzner/git-shim +131 -0
  42. package/runtime/hetzner/opencode-agentbox-plugin.js +76 -0
  43. package/runtime/hetzner/scripts/install-box.sh +11 -2
  44. package/runtime/relay/bin.cjs +927 -36
  45. package/share/agentbox-setup/SKILL.md +1 -1
  46. package/share/host-skills/agentbox/SKILL.md +29 -0
  47. package/share/host-skills/agentbox-info/SKILL.md +211 -0
  48. package/share/host-skills/codex/agentbox.md +35 -0
  49. package/share/host-skills/opencode/agentbox.md +26 -0
  50. package/dist/_cloud-attach-DMVH6GWO.js +0 -12
  51. package/dist/chunk-7KOEFGN2.js.map +0 -1
  52. package/dist/chunk-NAVL4R34.js.map +0 -1
  53. package/dist/chunk-NW5NYTQM.js.map +0 -1
  54. package/dist/chunk-UK72UQ5U.js.map +0 -1
  55. package/dist/chunk-V5KZGB5V.js.map +0 -1
  56. package/dist/dist-QZGJIBT5.js.map +0 -1
  57. package/dist/dist-R67WMLCF.js.map +0 -1
  58. /package/dist/{_cloud-attach-DMVH6GWO.js.map → _cloud-attach-T727ZPRV.js.map} +0 -0
  59. /package/dist/{cloud-poller-ZIWSADJB-JXFRJUEM.js.map → cloud-poller-SUNA6ZQC-2RG5WPRN.js.map} +0 -0
  60. /package/dist/{dist-ETCFRVPA.js.map → dist-LOZBWMBF.js.map} +0 -0
@@ -1,15 +1,39 @@
1
1
  #!/usr/bin/env node
2
+ import {
3
+ DEFAULT_BOX_IMAGE,
4
+ GitWorktreeError,
5
+ STATE_DIR,
6
+ STATE_FILE,
7
+ allocateProjectIndex,
8
+ buildImage,
9
+ computeDockerContextFingerprint,
10
+ detectGitRepos,
11
+ ensureImage,
12
+ findBox,
13
+ imageExists,
14
+ pickFreshBranch,
15
+ preparedMatches,
16
+ readCliStamp,
17
+ readPreparedDockerState,
18
+ readState,
19
+ recordBox,
20
+ removeBoxRecord,
21
+ writePreparedDockerState
22
+ } from "./chunk-BGK32PZE.js";
2
23
 
3
24
  // ../../packages/sandbox-docker/dist/index.js
4
25
  import { randomBytes as randomBytes3 } from "crypto";
5
26
  import { mkdir as mkdir7, stat as stat6 } from "fs/promises";
6
27
  import { homedir as homedir9 } from "os";
7
- import { basename as basename2, join as join10, resolve as resolve4 } from "path";
8
- import { execa as execa14 } from "execa";
28
+ import { basename as basename2, join as join10, resolve as resolve3 } from "path";
29
+ import { execa as execa13 } from "execa";
9
30
 
10
31
  // ../../packages/ctl/dist/index.js
11
32
  import { readFile } from "fs/promises";
12
33
  import { parse as parseYaml } from "yaml";
34
+ import { readFile as readFile2 } from "fs/promises";
35
+ import { parse as parseYaml2 } from "yaml";
36
+ var BOX_STATUS_EVENT = "box-status";
13
37
  function renderStatusTable(rows) {
14
38
  if (rows.length === 0) return "(no services configured)";
15
39
  const headers = ["NAME", "STATE", "PID", "RESTARTS", "LAST EXIT", "BLOCKED ON", "COMMAND"];
@@ -372,7 +396,7 @@ function assertBool(raw, where) {
372
396
  if (typeof raw !== "boolean") throw new ConfigError(`${where} must be a boolean`);
373
397
  return raw;
374
398
  }
375
- var TOP_LEVEL_KEYS = /* @__PURE__ */ new Set(["services", "tasks", "ide", "defaults"]);
399
+ var TOP_LEVEL_KEYS = /* @__PURE__ */ new Set(["services", "tasks", "ide", "defaults", "carry"]);
376
400
  function validateUnitGraph(tasks, services) {
377
401
  const names = /* @__PURE__ */ new Set();
378
402
  for (const t of tasks) {
@@ -488,27 +512,217 @@ async function loadConfig(path) {
488
512
  }
489
513
  return parseConfig(text);
490
514
  }
515
+ var CarryConfigError = class extends Error {
516
+ constructor(message) {
517
+ super(message);
518
+ this.name = "CarryConfigError";
519
+ }
520
+ };
521
+ var ITEM_KEYS = /* @__PURE__ */ new Set(["src", "dest", "mode", "user", "optional"]);
522
+ function parseUser(raw, where) {
523
+ if (raw === void 0 || raw === null) return void 0;
524
+ let n;
525
+ if (typeof raw === "number") {
526
+ if (!Number.isInteger(raw) || raw < 0) {
527
+ throw new CarryConfigError(`${where}.user must be a non-negative integer uid (got ${String(raw)})`);
528
+ }
529
+ n = raw;
530
+ } else if (typeof raw === "string") {
531
+ const trimmed = raw.trim();
532
+ if (!/^[0-9]+$/.test(trimmed)) {
533
+ throw new CarryConfigError(
534
+ `${where}.user "${raw}" must be a numeric uid (e.g. 1000). Usernames not supported \u2014 look up the uid first.`
535
+ );
536
+ }
537
+ n = parseInt(trimmed, 10);
538
+ } else {
539
+ throw new CarryConfigError(`${where}.user must be a non-negative integer uid`);
540
+ }
541
+ if (n > 65535) {
542
+ throw new CarryConfigError(`${where}.user must be between 0 and 65535 (got ${String(n)})`);
543
+ }
544
+ return n;
545
+ }
546
+ function isPlainObject2(v) {
547
+ return typeof v === "object" && v !== null && !Array.isArray(v);
548
+ }
549
+ function assertSrcShape(src, where) {
550
+ if (src.length === 0) {
551
+ throw new CarryConfigError(`${where}.src must not be empty`);
552
+ }
553
+ if (!src.startsWith("/") && !src.startsWith("~/") && !src.startsWith("./")) {
554
+ throw new CarryConfigError(
555
+ `${where}.src "${src}" must start with /, ~/, or ./ (bare relative paths are rejected to avoid surprises)`
556
+ );
557
+ }
558
+ }
559
+ function assertDestShape(dest, where) {
560
+ if (dest.length === 0) {
561
+ throw new CarryConfigError(`${where}.dest must not be empty`);
562
+ }
563
+ if (!dest.startsWith("/") && !dest.startsWith("~/")) {
564
+ throw new CarryConfigError(
565
+ `${where}.dest "${dest}" must start with / or ~/ (relative box-side paths are not allowed)`
566
+ );
567
+ }
568
+ }
569
+ function parseMode(raw, where) {
570
+ if (raw === void 0 || raw === null) return void 0;
571
+ let n;
572
+ if (typeof raw === "number") {
573
+ if (!Number.isInteger(raw) || raw < 0) {
574
+ throw new CarryConfigError(`${where}.mode must be a non-negative integer (got ${String(raw)})`);
575
+ }
576
+ n = raw;
577
+ } else if (typeof raw === "string") {
578
+ const trimmed = raw.trim();
579
+ if (trimmed.length === 0) {
580
+ throw new CarryConfigError(`${where}.mode must not be empty`);
581
+ }
582
+ const cleaned = trimmed.startsWith("0o") || trimmed.startsWith("0O") ? trimmed.slice(2) : trimmed;
583
+ if (!/^[0-7]+$/.test(cleaned)) {
584
+ throw new CarryConfigError(
585
+ `${where}.mode "${raw}" must be an octal number (e.g. 0o600, "0600", "600")`
586
+ );
587
+ }
588
+ n = parseInt(cleaned, 8);
589
+ } else {
590
+ throw new CarryConfigError(`${where}.mode must be a number or octal string`);
591
+ }
592
+ if (n < 0 || n > 4095) {
593
+ throw new CarryConfigError(`${where}.mode must be between 0 and 0o7777 (got ${n.toString(8)})`);
594
+ }
595
+ return n;
596
+ }
597
+ function parseShorthand(raw, where) {
598
+ const eq = raw.indexOf("=");
599
+ let src;
600
+ let dest;
601
+ if (eq === -1) {
602
+ src = raw;
603
+ dest = raw;
604
+ } else {
605
+ src = raw.slice(0, eq);
606
+ dest = raw.slice(eq + 1);
607
+ }
608
+ src = src.trim();
609
+ dest = dest.trim();
610
+ assertSrcShape(src, where);
611
+ if (eq === -1) {
612
+ if (src.startsWith("./")) {
613
+ throw new CarryConfigError(
614
+ `${where} shorthand "${raw}" must specify an explicit dest (use "src=dest") when src starts with ./`
615
+ );
616
+ }
617
+ }
618
+ assertDestShape(dest, where);
619
+ return { src, dest, optional: false };
620
+ }
621
+ function parseMapping(raw, where) {
622
+ for (const key of Object.keys(raw)) {
623
+ if (!ITEM_KEYS.has(key)) {
624
+ throw new CarryConfigError(`${where} has unknown key "${key}"`);
625
+ }
626
+ }
627
+ const srcRaw = raw.src;
628
+ if (typeof srcRaw !== "string") {
629
+ throw new CarryConfigError(`${where}.src must be a string`);
630
+ }
631
+ const src = srcRaw.trim();
632
+ assertSrcShape(src, where);
633
+ let dest;
634
+ if (raw.dest === void 0 || raw.dest === null) {
635
+ if (src.startsWith("./")) {
636
+ throw new CarryConfigError(
637
+ `${where}.dest is required when src starts with ./ (no sensible in-box default)`
638
+ );
639
+ }
640
+ dest = src;
641
+ } else {
642
+ if (typeof raw.dest !== "string") {
643
+ throw new CarryConfigError(`${where}.dest must be a string`);
644
+ }
645
+ dest = raw.dest.trim();
646
+ }
647
+ assertDestShape(dest, where);
648
+ const mode = parseMode(raw.mode, where);
649
+ const user = parseUser(raw.user, where);
650
+ let optional = false;
651
+ if (raw.optional !== void 0 && raw.optional !== null) {
652
+ if (typeof raw.optional !== "boolean") {
653
+ throw new CarryConfigError(`${where}.optional must be a boolean`);
654
+ }
655
+ optional = raw.optional;
656
+ }
657
+ const out = { src, dest, optional };
658
+ if (mode !== void 0) out.mode = mode;
659
+ if (user !== void 0) out.user = user;
660
+ return out;
661
+ }
662
+ function parseCarryRaw(raw) {
663
+ if (raw === void 0 || raw === null) return [];
664
+ if (!Array.isArray(raw)) {
665
+ throw new CarryConfigError("carry must be a list of strings or mappings");
666
+ }
667
+ const out = [];
668
+ for (const [i, item] of raw.entries()) {
669
+ const where = `carry[${String(i)}]`;
670
+ if (typeof item === "string") {
671
+ out.push(parseShorthand(item, where));
672
+ } else if (isPlainObject2(item)) {
673
+ out.push(parseMapping(item, where));
674
+ } else {
675
+ throw new CarryConfigError(`${where} must be a string or mapping`);
676
+ }
677
+ }
678
+ return out;
679
+ }
680
+ function parseCarrySection(text) {
681
+ let doc;
682
+ try {
683
+ doc = parseYaml2(text);
684
+ } catch (err) {
685
+ throw new CarryConfigError(
686
+ `yaml parse error: ${err instanceof Error ? err.message : String(err)}`
687
+ );
688
+ }
689
+ if (doc === null || doc === void 0) return [];
690
+ if (!isPlainObject2(doc)) {
691
+ throw new CarryConfigError("top-level config must be a mapping");
692
+ }
693
+ return parseCarryRaw(doc.carry);
694
+ }
695
+ async function loadCarrySection(path) {
696
+ let text;
697
+ try {
698
+ text = await readFile2(path, "utf8");
699
+ } catch (err) {
700
+ if (err.code === "ENOENT") return [];
701
+ throw err;
702
+ }
703
+ return parseCarrySection(text);
704
+ }
491
705
 
492
706
  // ../../packages/sandbox-docker/dist/index.js
493
707
  import { spawnSync } from "child_process";
494
- import { mkdir as mkdir22, mkdtemp, readdir as readdir3, readFile as readFile23, rm as rm2, stat as stat4, writeFile as writeFile3 } from "fs/promises";
495
- import { homedir as homedir22, tmpdir } from "os";
496
- import { join as join23, relative } from "path";
708
+ import { mkdir as mkdir22, mkdtemp, readdir as readdir3, readFile as readFile24, rm as rm2, stat as stat3, writeFile as writeFile3 } from "fs/promises";
709
+ import { homedir as homedir2, tmpdir } from "os";
710
+ import { join as join22, relative } from "path";
497
711
  import { setTimeout as delay } from "timers/promises";
498
712
  import { execa as execa3 } from "execa";
499
- import { execa as execa4 } from "execa";
713
+ import { execa as execa2 } from "execa";
500
714
  import { mkdir as mkdir3, readFile as readFile4 } from "fs/promises";
501
715
  import { homedir as homedir3 } from "os";
502
716
  import { join as join4 } from "path";
503
717
  import { execa as execa22 } from "execa";
504
718
 
505
719
  // ../../packages/config/dist/index.js
506
- import { parse as parseYaml2 } from "yaml";
720
+ import { parse as parseYaml3 } from "yaml";
507
721
  import { createHash } from "crypto";
508
722
  import { realpath, stat } from "fs/promises";
509
723
  import { homedir } from "os";
510
724
  import { basename, dirname, join, resolve } from "path";
511
- import { readFile as readFile2 } from "fs/promises";
725
+ import { readFile as readFile3 } from "fs/promises";
512
726
  import { parse as parseYaml22 } from "yaml";
513
727
  import { mkdir, readFile as readFile22, rename, rm, stat as stat2, writeFile } from "fs/promises";
514
728
  import { dirname as dirname2, isAbsolute, join as join2 } from "path";
@@ -533,7 +747,8 @@ var BUILT_IN_DEFAULTS = {
533
747
  memory: 0,
534
748
  cpus: 0,
535
749
  pidsLimit: 0,
536
- disk: ""
750
+ disk: "",
751
+ bundleDepth: void 0
537
752
  },
538
753
  checkpoint: {
539
754
  maxLayers: 3
@@ -582,6 +797,10 @@ var BUILT_IN_DEFAULTS = {
582
797
  maxRunningBoxes: 5,
583
798
  idleMinutes: 5
584
799
  },
800
+ queue: {
801
+ enabled: true,
802
+ maxConcurrent: 5
803
+ },
585
804
  maintenance: {
586
805
  pruneProjectConfigs: true,
587
806
  pruneProjectConfigsEvery: 50
@@ -690,6 +909,11 @@ var KEY_REGISTRY = [
690
909
  description: "Best-effort writable-layer size for new boxes, e.g. '10G'. No-op on overlay2 / the macOS engines.",
691
910
  advanced: true
692
911
  },
912
+ {
913
+ key: "box.bundleDepth",
914
+ type: "int",
915
+ description: "Cap git bundle history shipped to cloud sandboxes (daytona, hetzner). 0 = full history. Unset = adaptive default (last 200 commits; re-bundle at 100 if the bundle exceeds 20 MB). Ignored for docker (which bind-mounts .git/)."
916
+ },
693
917
  {
694
918
  key: "claude.sessionName",
695
919
  type: "string",
@@ -797,6 +1021,16 @@ var KEY_REGISTRY = [
797
1021
  type: "int",
798
1022
  description: "Minutes a box must be continuously idle (claude state) before it is eligible for auto-pause."
799
1023
  },
1024
+ {
1025
+ key: "queue.enabled",
1026
+ type: "bool",
1027
+ description: "Run `agentbox claude|codex|opencode -i <prompt>` jobs through the host-wide background queue (FIFO, capped by queue.maxConcurrent)."
1028
+ },
1029
+ {
1030
+ key: "queue.maxConcurrent",
1031
+ type: "int",
1032
+ description: "Max number of simultaneously-running boxes (across providers) before background `-i` jobs queue up instead of starting immediately. Per-invocation override: `--max-running <n>`."
1033
+ },
800
1034
  {
801
1035
  key: "maintenance.pruneProjectConfigs",
802
1036
  type: "bool",
@@ -818,7 +1052,7 @@ var UserConfigError = class extends Error {
818
1052
  this.name = "UserConfigError";
819
1053
  }
820
1054
  };
821
- function isPlainObject2(v) {
1055
+ function isPlainObject3(v) {
822
1056
  return typeof v === "object" && v !== null && !Array.isArray(v);
823
1057
  }
824
1058
  var RENAMED_KEYS = /* @__PURE__ */ new Map([["box.snapshot", "box.hostSnapshot"]]);
@@ -875,25 +1109,34 @@ function coerceTypedValue(raw, desc, where) {
875
1109
  function parseUserConfig(text, where) {
876
1110
  let doc;
877
1111
  try {
878
- doc = parseYaml2(text);
1112
+ doc = parseYaml3(text);
879
1113
  } catch (err) {
880
1114
  throw new UserConfigError(
881
1115
  `${where}: yaml parse error: ${err instanceof Error ? err.message : String(err)}`
882
1116
  );
883
1117
  }
884
1118
  if (doc === null || doc === void 0) return {};
885
- if (!isPlainObject2(doc)) {
1119
+ if (!isPlainObject3(doc)) {
886
1120
  throw new UserConfigError(`${where}: top-level must be a mapping`);
887
1121
  }
888
1122
  return parseUserConfigObject(doc, where);
889
1123
  }
890
1124
  function parseUserConfigObject(doc, where) {
891
1125
  if (doc === null || doc === void 0) return {};
892
- if (!isPlainObject2(doc)) {
1126
+ if (!isPlainObject3(doc)) {
893
1127
  throw new UserConfigError(`${where}: must be a mapping`);
894
1128
  }
895
1129
  const out = {};
896
1130
  for (const [branchName, branchRaw] of Object.entries(doc)) {
1131
+ if (branchName === "schema") {
1132
+ if (branchRaw !== void 0 && branchRaw !== null) {
1133
+ if (typeof branchRaw !== "number" || !Number.isInteger(branchRaw)) {
1134
+ throw new UserConfigError(`${where}.schema: must be an integer (got ${String(branchRaw)})`);
1135
+ }
1136
+ out.schema = branchRaw;
1137
+ }
1138
+ continue;
1139
+ }
897
1140
  const branchSpec = BRANCHES.get(branchName);
898
1141
  if (!branchSpec) {
899
1142
  throw new UserConfigError(
@@ -901,7 +1144,7 @@ function parseUserConfigObject(doc, where) {
901
1144
  );
902
1145
  }
903
1146
  if (branchRaw === null || branchRaw === void 0) continue;
904
- if (!isPlainObject2(branchRaw)) {
1147
+ if (!isPlainObject3(branchRaw)) {
905
1148
  throw new UserConfigError(`${where}.${branchName}: must be a mapping`);
906
1149
  }
907
1150
  const branchOut = {};
@@ -978,9 +1221,9 @@ function lookupKeyOrThrow(key) {
978
1221
  }
979
1222
  return desc;
980
1223
  }
981
- var STATE_DIR = join(homedir(), ".agentbox");
982
- var GLOBAL_CONFIG_FILE = join(STATE_DIR, "config.yaml");
983
- var PROJECTS_DIR = join(STATE_DIR, "projects");
1224
+ var STATE_DIR2 = join(homedir(), ".agentbox");
1225
+ var GLOBAL_CONFIG_FILE = join(STATE_DIR2, "config.yaml");
1226
+ var PROJECTS_DIR = join(STATE_DIR2, "projects");
984
1227
  var WORKSPACE_CONFIG_BASENAME = "agentbox.yaml";
985
1228
  async function findProjectRoot(cwd) {
986
1229
  const start = await canonicalize(cwd);
@@ -1042,7 +1285,7 @@ async function configPathFor(scope, cwd) {
1042
1285
  async function loadOptionalUserConfig(path) {
1043
1286
  let text;
1044
1287
  try {
1045
- text = await readFile2(path, "utf8");
1288
+ text = await readFile3(path, "utf8");
1046
1289
  } catch (err) {
1047
1290
  if (err.code === "ENOENT") return {};
1048
1291
  throw err;
@@ -1053,7 +1296,7 @@ async function loadProjectAgentboxDefaults(workspacePath) {
1053
1296
  const path = workspaceConfigFile(workspacePath);
1054
1297
  let text;
1055
1298
  try {
1056
- text = await readFile2(path, "utf8");
1299
+ text = await readFile3(path, "utf8");
1057
1300
  } catch (err) {
1058
1301
  if (err.code === "ENOENT") return {};
1059
1302
  throw err;
@@ -1161,6 +1404,7 @@ async function setConfigValue(scope, key, value, cwd, opts = {}) {
1161
1404
  const path = await configPathFor(scope, cwd);
1162
1405
  const current = await readExistingDoc(path);
1163
1406
  setLeaf(current, key, coerced);
1407
+ stampSchema(current);
1164
1408
  parseUserConfig(stringifyYaml(current), path);
1165
1409
  await atomicWriteYaml(path, current);
1166
1410
  if (scope === "project") {
@@ -1177,6 +1421,7 @@ async function unsetConfigValue(scope, key, cwd) {
1177
1421
  const current = await readExistingDoc(path);
1178
1422
  const existed = unsetLeaf(current, key);
1179
1423
  if (!existed) return { path, existed: false };
1424
+ stampSchema(current);
1180
1425
  await atomicWriteYaml(path, current);
1181
1426
  if (scope === "project") {
1182
1427
  const root = (await findProjectRoot(cwd)).root;
@@ -1287,6 +1532,12 @@ async function readExistingDoc(path) {
1287
1532
  }
1288
1533
  return parseUserConfig(text, path);
1289
1534
  }
1535
+ var CURRENT_CONFIG_SCHEMA = 1;
1536
+ function stampSchema(doc) {
1537
+ if (typeof doc.schema !== "number") {
1538
+ doc.schema = CURRENT_CONFIG_SCHEMA;
1539
+ }
1540
+ }
1290
1541
  function setLeaf(doc, key, value) {
1291
1542
  const idx = key.indexOf(".");
1292
1543
  const branch = key.slice(0, idx);
@@ -1341,181 +1592,7 @@ async function touchProjectMeta(absPath) {
1341
1592
  // ../../packages/sandbox-docker/dist/index.js
1342
1593
  import { chmod, mkdir as mkdir32, readFile as readFile32 } from "fs/promises";
1343
1594
  import { join as join32 } from "path";
1344
- import { execa as execa42 } from "execa";
1345
-
1346
- // ../../packages/sandbox-core/dist/index.js
1347
- import { mkdir as mkdir2, readFile as readFile3, writeFile as writeFile2 } from "fs/promises";
1348
- import { homedir as homedir2 } from "os";
1349
- import { dirname as dirname3, join as join3 } from "path";
1350
- import { execa } from "execa";
1351
- import { readdir as readdir2, stat as stat3 } from "fs/promises";
1352
- import { join as join22 } from "path";
1353
- var STATE_DIR2 = join3(homedir2(), ".agentbox");
1354
- var STATE_FILE = join3(STATE_DIR2, "state.json");
1355
- var EMPTY = { version: 1, boxes: [] };
1356
- async function readState(path = STATE_FILE) {
1357
- try {
1358
- const raw = await readFile3(path, "utf8");
1359
- const parsed = JSON.parse(raw);
1360
- if (parsed.version !== 1 || !Array.isArray(parsed.boxes)) {
1361
- throw new Error(`unrecognized state file shape at ${path}`);
1362
- }
1363
- for (const b of parsed.boxes) {
1364
- b.provider ??= "docker";
1365
- if ((b.provider ?? "docker") === "docker" && !b.docker) {
1366
- b.docker = projectDockerFields(b);
1367
- }
1368
- }
1369
- return parsed;
1370
- } catch (err) {
1371
- if (err.code === "ENOENT") {
1372
- return { ...EMPTY };
1373
- }
1374
- throw err;
1375
- }
1376
- }
1377
- async function writeState(state, path = STATE_FILE) {
1378
- await mkdir2(dirname3(path), { recursive: true });
1379
- await writeFile2(path, JSON.stringify(state, null, 2) + "\n", "utf8");
1380
- }
1381
- async function recordBox(box, path = STATE_FILE) {
1382
- const toWrite = (box.provider ?? "docker") === "docker" && !box.docker ? { ...box, docker: projectDockerFields(box) } : box;
1383
- const state = await readState(path);
1384
- const next = {
1385
- version: 1,
1386
- boxes: [...state.boxes.filter((b) => b.id !== toWrite.id), toWrite]
1387
- };
1388
- await writeState(next, path);
1389
- }
1390
- function projectDockerFields(box) {
1391
- return {
1392
- container: box.container,
1393
- image: box.image,
1394
- snapshotDir: box.snapshotDir ?? null,
1395
- socketPath: box.socketPath,
1396
- claudeConfigVolume: box.claudeConfigVolume,
1397
- codexConfigVolume: box.codexConfigVolume,
1398
- opencodeConfigVolume: box.opencodeConfigVolume,
1399
- vscodeServerVolume: box.vscodeServerVolume,
1400
- cursorServerVolume: box.cursorServerVolume,
1401
- vncHostPort: box.vncHostPort,
1402
- webHostPort: box.webHostPort,
1403
- portlessAlias: box.portlessAlias,
1404
- portlessUrl: box.portlessUrl,
1405
- dockerVolume: box.dockerVolume,
1406
- dockerCacheShared: box.dockerCacheShared,
1407
- checkpointImage: box.checkpointImage
1408
- };
1409
- }
1410
- async function removeBoxRecord(id, path = STATE_FILE) {
1411
- const state = await readState(path);
1412
- const before = state.boxes.length;
1413
- const next = {
1414
- version: 1,
1415
- boxes: state.boxes.filter((b) => b.id !== id)
1416
- };
1417
- if (next.boxes.length === before) return false;
1418
- await writeState(next, path);
1419
- return true;
1420
- }
1421
- function findBox(idOrName, state) {
1422
- const q = idOrName.trim();
1423
- if (q.length === 0) return { kind: "none" };
1424
- const exactId = state.boxes.find((b) => b.id === q);
1425
- if (exactId) return { kind: "ok", box: exactId };
1426
- const prefixMatches = state.boxes.filter((b) => b.id.startsWith(q));
1427
- if (prefixMatches.length === 1) return { kind: "ok", box: prefixMatches[0] };
1428
- if (prefixMatches.length > 1) return { kind: "ambiguous", matches: prefixMatches };
1429
- const byName = state.boxes.find((b) => b.name === q);
1430
- if (byName) return { kind: "ok", box: byName };
1431
- const byContainer = state.boxes.find((b) => b.container === q);
1432
- if (byContainer) return { kind: "ok", box: byContainer };
1433
- return { kind: "none" };
1434
- }
1435
- function allocateProjectIndex(state, projectRoot) {
1436
- let max = 0;
1437
- for (const b of state.boxes) {
1438
- if (b.projectRoot !== projectRoot) continue;
1439
- if (typeof b.projectIndex === "number" && b.projectIndex > max) {
1440
- max = b.projectIndex;
1441
- }
1442
- }
1443
- return max + 1;
1444
- }
1445
- function autoPickProjectBox(state, projectRoot) {
1446
- const matches = state.boxes.filter((b) => b.projectRoot === projectRoot);
1447
- if (matches.length === 0) return { kind: "none" };
1448
- if (matches.length === 1) return { kind: "ok", box: matches[0] };
1449
- return { kind: "ambiguous", matches };
1450
- }
1451
- function resolveBoxRef(ref, state, projectRoot) {
1452
- if (ref === void 0) {
1453
- if (projectRoot === void 0) return { kind: "none" };
1454
- return autoPickProjectBox(state, projectRoot);
1455
- }
1456
- const trimmed = ref.trim();
1457
- if (projectRoot !== void 0 && /^[1-9][0-9]*$/.test(trimmed)) {
1458
- const idx = Number.parseInt(trimmed, 10);
1459
- const hit = state.boxes.find(
1460
- (b) => b.projectRoot === projectRoot && b.projectIndex === idx
1461
- );
1462
- return hit ? { kind: "ok", box: hit } : { kind: "none" };
1463
- }
1464
- return findBox(trimmed, state);
1465
- }
1466
- async function detectGitRepos(workspace) {
1467
- const out = [];
1468
- if (await isGitDir(join22(workspace, ".git"))) {
1469
- out.push({ kind: "root", hostMainRepo: workspace, relPathFromWorkspace: "" });
1470
- }
1471
- let entries;
1472
- try {
1473
- entries = await readdir2(workspace, { withFileTypes: true });
1474
- } catch {
1475
- return out;
1476
- }
1477
- for (const e of entries) {
1478
- if (!e.isDirectory() || e.name.startsWith(".")) continue;
1479
- const sub = join22(workspace, e.name);
1480
- if (await isGitDir(join22(sub, ".git"))) {
1481
- out.push({ kind: "nested", hostMainRepo: sub, relPathFromWorkspace: e.name });
1482
- }
1483
- }
1484
- return out;
1485
- }
1486
- async function isGitDir(path) {
1487
- try {
1488
- const s = await stat3(path);
1489
- return s.isDirectory();
1490
- } catch {
1491
- return false;
1492
- }
1493
- }
1494
- async function pickFreshBranch(hostMainRepo, base) {
1495
- let candidate = base;
1496
- let suffix = 2;
1497
- while (await branchExists(hostMainRepo, candidate)) {
1498
- candidate = `${base}-${String(suffix++)}`;
1499
- if (suffix > 100) throw new GitWorktreeError(`could not find a free branch name near ${base}`);
1500
- }
1501
- return candidate;
1502
- }
1503
- async function branchExists(hostMainRepo, name) {
1504
- const result = await execa(
1505
- "git",
1506
- ["-C", hostMainRepo, "show-ref", "--verify", "--quiet", `refs/heads/${name}`],
1507
- { reject: false }
1508
- );
1509
- return result.exitCode === 0;
1510
- }
1511
- var GitWorktreeError = class extends Error {
1512
- constructor(message) {
1513
- super(message);
1514
- this.name = "GitWorktreeError";
1515
- }
1516
- };
1517
-
1518
- // ../../packages/sandbox-docker/dist/index.js
1595
+ import { execa as execa4 } from "execa";
1519
1596
  import { spawnSync as spawnSync2 } from "child_process";
1520
1597
  import { stat as stat22 } from "fs/promises";
1521
1598
  import { homedir as homedir32 } from "os";
@@ -1526,7 +1603,7 @@ import { stat as stat32 } from "fs/promises";
1526
1603
  import { homedir as homedir4 } from "os";
1527
1604
  import { join as join5 } from "path";
1528
1605
  import { execa as execa6 } from "execa";
1529
- import { randomBytes } from "crypto";
1606
+ import { randomBytes as randomBytes2 } from "crypto";
1530
1607
  import { execa as execa7 } from "execa";
1531
1608
  import { existsSync } from "fs";
1532
1609
  import { readFile as readFile42 } from "fs/promises";
@@ -1534,41 +1611,202 @@ import { homedir as homedir5 } from "os";
1534
1611
  import { join as join6 } from "path";
1535
1612
  import { execa as execa8 } from "execa";
1536
1613
  import { execa as execa9 } from "execa";
1537
- import { existsSync as existsSync2 } from "fs";
1538
- import { fileURLToPath } from "url";
1539
- import { dirname as dirname4, resolve as resolve2 } from "path";
1540
- import { execa as execa10 } from "execa";
1541
- import { mkdir as mkdir4, readdir as readdir22, rm as rm22, stat as stat42 } from "fs/promises";
1614
+ import { mkdir as mkdir4, readdir as readdir22, rm as rm22, stat as stat4 } from "fs/promises";
1542
1615
  import { homedir as homedir6, platform } from "os";
1543
- import { join as join7, resolve as resolve22 } from "path";
1616
+ import { join as join7, resolve as resolve2 } from "path";
1544
1617
  import { mkdir as mkdir5, mkdtemp as mkdtemp2, readFile as readFile5, readdir as readdir32, rm as rm3, writeFile as writeFile22 } from "fs/promises";
1545
1618
  import { homedir as homedir7, tmpdir as tmpdir2 } from "os";
1546
1619
  import { basename as basename3, join as join8 } from "path";
1547
- import { execa as execa11 } from "execa";
1620
+ import { execa as execa10 } from "execa";
1548
1621
  import { stat as stat5 } from "fs/promises";
1622
+ import { execa as execa11 } from "execa";
1549
1623
  import { execa as execa12 } from "execa";
1550
- import { execa as execa13 } from "execa";
1551
1624
  import { spawn } from "child_process";
1552
- import { randomBytes as randomBytes2 } from "crypto";
1553
- import { existsSync as existsSync3, openSync } from "fs";
1554
- import { mkdir as mkdir6, readFile as readFile6, unlink, writeFile as writeFile32 } from "fs/promises";
1625
+ import { randomBytes as randomBytes22 } from "crypto";
1626
+ import { existsSync as existsSync2, openSync } from "fs";
1627
+ import { mkdir as mkdir6, readFile as readFile6, unlink as unlink2, writeFile as writeFile32 } from "fs/promises";
1555
1628
  import { request as httpRequest } from "http";
1556
1629
  import { homedir as homedir8 } from "os";
1557
- import { dirname as dirname22, join as join9, resolve as resolve3 } from "path";
1630
+ import { dirname as dirname3, join as join9, resolve as resolve22 } from "path";
1558
1631
  import { setTimeout as delay2 } from "timers/promises";
1559
- import { fileURLToPath as fileURLToPath2 } from "url";
1632
+ import { fileURLToPath } from "url";
1560
1633
 
1561
1634
  // ../../packages/relay/dist/index.js
1562
- import { execa as execa2 } from "execa";
1635
+ import { createHash as createHash2, randomBytes } from "crypto";
1636
+ import { execa } from "execa";
1637
+ import { spawn as spawn4 } from "child_process";
1638
+ import {
1639
+ mkdir as mkdir2,
1640
+ readdir as readdir2,
1641
+ readFile as readFile23,
1642
+ rename as rename2,
1643
+ unlink,
1644
+ writeFile as writeFile2
1645
+ } from "fs/promises";
1646
+ import { join as join3 } from "path";
1563
1647
  var DEFAULT_RELAY_PORT = 8787;
1564
1648
  var RELAY_CONTAINER_NAME = "agentbox-relay";
1565
1649
  var RELAY_NETWORK_NAME = "agentbox-net";
1566
1650
  var RELAY_IMAGE_REF = "agentbox/relay:dev";
1567
1651
  var DEFAULT_HOST_ACTION_MAX_AGE_MS = 15 * 60 * 1e3;
1652
+ function hashRpcParams(params) {
1653
+ return createHash2("sha256").update(canonicalJson(params)).digest("hex");
1654
+ }
1655
+ function canonicalJson(v) {
1656
+ if (v === null) return "null";
1657
+ if (typeof v === "undefined") return "null";
1658
+ if (typeof v === "number") return Number.isFinite(v) ? String(v) : "null";
1659
+ if (typeof v === "boolean") return v ? "true" : "false";
1660
+ if (typeof v === "string") return JSON.stringify(v);
1661
+ if (Array.isArray(v)) return "[" + v.map(canonicalJson).join(",") + "]";
1662
+ if (typeof v === "object") {
1663
+ const entries = Object.entries(v).filter(([k]) => k !== "hostInitiated").filter(([, val]) => val !== void 0).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
1664
+ return "{" + entries.map(([k, val]) => JSON.stringify(k) + ":" + canonicalJson(val)).join(",") + "}";
1665
+ }
1666
+ return "null";
1667
+ }
1668
+ var GH_PR_OPS = [
1669
+ "create",
1670
+ "view",
1671
+ "list",
1672
+ "comment",
1673
+ "review",
1674
+ "merge",
1675
+ "checkout",
1676
+ "close",
1677
+ "reopen"
1678
+ ];
1568
1679
  var MAX_BODY_BYTES = 1024 * 1024;
1680
+ var QUEUE_DIR = join3(STATE_DIR, "queue");
1681
+ async function loadQueueConfig() {
1682
+ const d = BUILT_IN_DEFAULTS.queue;
1683
+ let global = {};
1684
+ try {
1685
+ global = parseUserConfig(await readFile23(GLOBAL_CONFIG_FILE, "utf8"), GLOBAL_CONFIG_FILE);
1686
+ } catch {
1687
+ }
1688
+ const q = global.queue ?? {};
1689
+ return {
1690
+ enabled: q.enabled ?? d.enabled,
1691
+ maxConcurrent: q.maxConcurrent ?? d.maxConcurrent
1692
+ };
1693
+ }
1694
+ async function writeJob(job) {
1695
+ await mkdir2(QUEUE_DIR, { recursive: true });
1696
+ const final = join3(QUEUE_DIR, `${job.id}.json`);
1697
+ const tmp = `${final}.tmp.${String(process.pid)}.${String(Date.now())}`;
1698
+ await writeFile2(tmp, JSON.stringify(job, null, 2) + "\n", "utf8");
1699
+ await rename2(tmp, final);
1700
+ }
1701
+ async function readJob(id) {
1702
+ try {
1703
+ const raw = await readFile23(join3(QUEUE_DIR, `${id}.json`), "utf8");
1704
+ return JSON.parse(raw);
1705
+ } catch (err) {
1706
+ if (err.code === "ENOENT") return null;
1707
+ throw err;
1708
+ }
1709
+ }
1710
+ async function deleteJob(id) {
1711
+ try {
1712
+ await unlink(join3(QUEUE_DIR, `${id}.json`));
1713
+ } catch (err) {
1714
+ if (err.code !== "ENOENT") throw err;
1715
+ }
1716
+ }
1717
+ async function loadQueue() {
1718
+ let entries;
1719
+ try {
1720
+ entries = await readdir2(QUEUE_DIR);
1721
+ } catch (err) {
1722
+ if (err.code === "ENOENT") return [];
1723
+ throw err;
1724
+ }
1725
+ const out = [];
1726
+ for (const name of entries) {
1727
+ if (!name.endsWith(".json")) continue;
1728
+ try {
1729
+ const raw = await readFile23(join3(QUEUE_DIR, name), "utf8");
1730
+ out.push(JSON.parse(raw));
1731
+ } catch {
1732
+ }
1733
+ }
1734
+ out.sort((a, b) => a.createdAt < b.createdAt ? -1 : a.createdAt > b.createdAt ? 1 : 0);
1735
+ return out;
1736
+ }
1737
+ var RUNNING_COUNT_CACHE_MS = 3e3;
1738
+ var runningCountCache = null;
1739
+ async function defaultCountRunningBoxes() {
1740
+ const now = Date.now();
1741
+ if (runningCountCache && runningCountCache.expiresAt > now) {
1742
+ return runningCountCache.value;
1743
+ }
1744
+ const value = await uncachedCountRunningBoxes();
1745
+ runningCountCache = { value, expiresAt: now + RUNNING_COUNT_CACHE_MS };
1746
+ return value;
1747
+ }
1748
+ async function uncachedCountRunningBoxes() {
1749
+ let boxes;
1750
+ try {
1751
+ boxes = (await readState(STATE_FILE)).boxes;
1752
+ } catch {
1753
+ return 0;
1754
+ }
1755
+ if (boxes.length === 0) return 0;
1756
+ let count = 0;
1757
+ const dockerBoxes = [];
1758
+ for (const b of boxes) {
1759
+ const provider = b.provider ?? "docker";
1760
+ if (provider === "docker") {
1761
+ dockerBoxes.push(b);
1762
+ } else {
1763
+ count += 1;
1764
+ }
1765
+ }
1766
+ if (dockerBoxes.length > 0) {
1767
+ const states = await Promise.all(dockerBoxes.map((b) => inspectDockerState(b.container)));
1768
+ for (const s of states) {
1769
+ if (s === "running") count += 1;
1770
+ }
1771
+ }
1772
+ return count;
1773
+ }
1774
+ function inspectDockerState(containerName) {
1775
+ return new Promise((resolveP) => {
1776
+ const child = spawn4("docker", ["inspect", "--format", "{{.State.Status}}", containerName], {
1777
+ stdio: ["ignore", "pipe", "pipe"]
1778
+ });
1779
+ let out = "";
1780
+ let settled = false;
1781
+ const finish = (state) => {
1782
+ if (settled) return;
1783
+ settled = true;
1784
+ resolveP(state);
1785
+ };
1786
+ const timer = setTimeout(() => {
1787
+ child.kill("SIGTERM");
1788
+ finish("other");
1789
+ }, 1e4);
1790
+ child.stdout?.on("data", (c) => {
1791
+ out += c.toString("utf8");
1792
+ });
1793
+ child.on("error", () => {
1794
+ clearTimeout(timer);
1795
+ finish("other");
1796
+ });
1797
+ child.on("close", () => {
1798
+ clearTimeout(timer);
1799
+ finish(out.trim() === "running" ? "running" : "other");
1800
+ });
1801
+ });
1802
+ }
1803
+ var QUEUE_LOGS_DIR = join3(STATE_DIR, "logs");
1804
+ function queueLogPath(id) {
1805
+ return join3(QUEUE_LOGS_DIR, `queue-${id}.log`);
1806
+ }
1569
1807
 
1570
1808
  // ../../packages/sandbox-docker/dist/index.js
1571
- import { execa as execa16 } from "execa";
1809
+ import { execa as execa15 } from "execa";
1572
1810
  import { readdir as readdir4, rm as rm4, stat as stat7 } from "fs/promises";
1573
1811
  import { join as join12 } from "path";
1574
1812
 
@@ -1624,14 +1862,17 @@ var AmbiguousBoxError = class extends Error {
1624
1862
  };
1625
1863
 
1626
1864
  // ../../packages/sandbox-docker/dist/index.js
1627
- import { execa as execa15 } from "execa";
1865
+ import { execa as execa14 } from "execa";
1628
1866
  import { join as join11 } from "path";
1629
1867
  import { homedir as homedir10 } from "os";
1630
1868
  import { join as join13 } from "path";
1869
+ import { execa as execa16 } from "execa";
1870
+ import { existsSync as existsSync3, mkdirSync, renameSync, statSync } from "fs";
1871
+ import { basename as basename32, dirname as dirname22, posix, resolve as resolve4 } from "path";
1631
1872
  import { execa as execa17 } from "execa";
1632
1873
  import { copyFile, mkdtemp as mkdtemp3, readdir as readdir5, readFile as readFile7, rm as rm5, stat as stat8, writeFile as writeFile4 } from "fs/promises";
1633
1874
  import { homedir as homedir11, tmpdir as tmpdir3 } from "os";
1634
- import { basename as basename32, join as join14, relative as relative2 } from "path";
1875
+ import { basename as basename4, join as join14, relative as relative2 } from "path";
1635
1876
  import { execa as execa18 } from "execa";
1636
1877
  function isHostPathHookCommand(command, hostHome) {
1637
1878
  if (typeof command !== "string" || command.length === 0) return false;
@@ -1758,16 +1999,16 @@ function rewritePluginPaths(value, hostPluginsPrefix) {
1758
1999
  }
1759
2000
  return value;
1760
2001
  }
1761
- function isPlainObject3(v) {
2002
+ function isPlainObject4(v) {
1762
2003
  return v !== null && typeof v === "object" && !Array.isArray(v);
1763
2004
  }
1764
2005
  function additiveMerge(hostRoot, boxRoot, hostPluginsPrefix, selectMap, withMap) {
1765
2006
  const hostMap = selectMap(hostRoot);
1766
2007
  const boxMap = selectMap(boxRoot);
1767
- if (!isPlainObject3(boxMap)) {
2008
+ if (!isPlainObject4(boxMap)) {
1768
2009
  return { data: hostRoot, changed: false, addedKeys: [] };
1769
2010
  }
1770
- const base = isPlainObject3(hostMap) ? { ...hostMap } : {};
2011
+ const base = isPlainObject4(hostMap) ? { ...hostMap } : {};
1771
2012
  const addedKeys = [];
1772
2013
  for (const [key, value] of Object.entries(boxMap)) {
1773
2014
  if (Object.prototype.hasOwnProperty.call(base, key)) continue;
@@ -1782,7 +2023,7 @@ function additiveMerge(hostRoot, boxRoot, hostPluginsPrefix, selectMap, withMap)
1782
2023
  function mergeKnownMarketplaces(hostJson, boxJson, opts) {
1783
2024
  const prefix = `${opts.hostHome}/.claude/plugins/`;
1784
2025
  return additiveMerge(
1785
- isPlainObject3(hostJson) ? hostJson : {},
2026
+ isPlainObject4(hostJson) ? hostJson : {},
1786
2027
  boxJson,
1787
2028
  prefix,
1788
2029
  (root) => root,
@@ -1791,24 +2032,24 @@ function mergeKnownMarketplaces(hostJson, boxJson, opts) {
1791
2032
  }
1792
2033
  function mergeInstalledPlugins(hostJson, boxJson, opts) {
1793
2034
  const prefix = `${opts.hostHome}/.claude/plugins/`;
1794
- const hostRoot = isPlainObject3(hostJson) ? hostJson : { plugins: {} };
2035
+ const hostRoot = isPlainObject4(hostJson) ? hostJson : { plugins: {} };
1795
2036
  return additiveMerge(
1796
2037
  hostRoot,
1797
2038
  boxJson,
1798
2039
  prefix,
1799
- (root) => isPlainObject3(root) ? root["plugins"] : void 0,
2040
+ (root) => isPlainObject4(root) ? root["plugins"] : void 0,
1800
2041
  (host, merged) => ({ ...host, plugins: merged })
1801
2042
  );
1802
2043
  }
1803
2044
  function referencedPluginVersionKeys(installedPluginsJson) {
1804
2045
  const keys = /* @__PURE__ */ new Set();
1805
- if (!isPlainObject3(installedPluginsJson)) return keys;
2046
+ if (!isPlainObject4(installedPluginsJson)) return keys;
1806
2047
  const plugins = installedPluginsJson["plugins"];
1807
- if (!isPlainObject3(plugins)) return keys;
2048
+ if (!isPlainObject4(plugins)) return keys;
1808
2049
  for (const entries of Object.values(plugins)) {
1809
2050
  if (!Array.isArray(entries)) continue;
1810
2051
  for (const entry of entries) {
1811
- if (!isPlainObject3(entry)) continue;
2052
+ if (!isPlainObject4(entry)) continue;
1812
2053
  const installPath = entry["installPath"];
1813
2054
  if (typeof installPath !== "string") continue;
1814
2055
  const segments = installPath.split("/").filter((s) => s.length > 0);
@@ -1819,7 +2060,7 @@ function referencedPluginVersionKeys(installedPluginsJson) {
1819
2060
  return keys;
1820
2061
  }
1821
2062
  async function dockerInfo() {
1822
- const result = await execa4("docker", ["info"], { reject: false });
2063
+ const result = await execa2("docker", ["info"], { reject: false });
1823
2064
  if (result.exitCode !== 0) {
1824
2065
  throw new Error(
1825
2066
  `docker info failed (exit ${String(result.exitCode)}). Is the Docker daemon running?
@@ -1866,6 +2107,9 @@ async function runBox(spec) {
1866
2107
  // on the macOS engines). Boxes use it to reach the host relay process.
1867
2108
  "--add-host=host.docker.internal:host-gateway"
1868
2109
  ];
2110
+ if (process.env.AGENTBOX === "1") {
2111
+ args.push("--user", "0");
2112
+ }
1869
2113
  const lim = spec.limits;
1870
2114
  if (lim) {
1871
2115
  if (lim.memoryBytes && lim.memoryBytes > 0) {
@@ -1892,11 +2136,11 @@ async function runBox(spec) {
1892
2136
  args.push("-e", `${k}=${val}`);
1893
2137
  }
1894
2138
  args.push(spec.image, "sleep", "infinity");
1895
- const { stdout } = await execa4("docker", args);
2139
+ const { stdout } = await execa2("docker", args);
1896
2140
  return stdout.trim();
1897
2141
  }
1898
2142
  async function dockerStorageDriver() {
1899
- const result = await execa4("docker", ["info", "--format", "{{.Driver}}"], { reject: false });
2143
+ const result = await execa2("docker", ["info", "--format", "{{.Driver}}"], { reject: false });
1900
2144
  if (result.exitCode !== 0) return "";
1901
2145
  return (result.stdout ?? "").trim();
1902
2146
  }
@@ -1905,7 +2149,7 @@ async function execInBox(container, cmd, opts = {}) {
1905
2149
  if (opts.detach) args.push("-d");
1906
2150
  if (opts.user) args.push("--user", opts.user);
1907
2151
  args.push(container, ...cmd);
1908
- const result = await execa4("docker", args, {
2152
+ const result = await execa2("docker", args, {
1909
2153
  reject: false,
1910
2154
  ...opts.timeoutMs ? { timeout: opts.timeoutMs } : {}
1911
2155
  });
@@ -1916,49 +2160,49 @@ async function execInBox(container, cmd, opts = {}) {
1916
2160
  };
1917
2161
  }
1918
2162
  async function removeContainer(container) {
1919
- await execa4("docker", ["rm", "-f", container], { reject: false });
2163
+ await execa2("docker", ["rm", "-f", container], { reject: false });
1920
2164
  }
1921
2165
  async function removeVolume(name) {
1922
- await execa4("docker", ["volume", "rm", name], { reject: false });
2166
+ await execa2("docker", ["volume", "rm", name], { reject: false });
1923
2167
  }
1924
2168
  async function removeImage(ref, opts = {}) {
1925
2169
  const args = ["image", "rm"];
1926
2170
  if (opts.force !== false) args.push("-f");
1927
2171
  args.push(ref);
1928
- const result = await execa4("docker", args, { reject: false });
2172
+ const result = await execa2("docker", args, { reject: false });
1929
2173
  return result.exitCode === 0;
1930
2174
  }
1931
2175
  async function containerExists(name) {
1932
- const result = await execa4("docker", ["container", "inspect", "--format", "{{.Id}}", name], {
2176
+ const result = await execa2("docker", ["container", "inspect", "--format", "{{.Id}}", name], {
1933
2177
  reject: false
1934
2178
  });
1935
2179
  return result.exitCode === 0;
1936
2180
  }
1937
2181
  async function volumeExists(name) {
1938
- const result = await execa4("docker", ["volume", "inspect", name], { reject: false });
2182
+ const result = await execa2("docker", ["volume", "inspect", name], { reject: false });
1939
2183
  return result.exitCode === 0;
1940
2184
  }
1941
2185
  async function ensureVolume(name) {
1942
2186
  if (await volumeExists(name)) return;
1943
- await execa4("docker", ["volume", "create", name]);
2187
+ await execa2("docker", ["volume", "create", name]);
1944
2188
  }
1945
2189
  async function removeNetwork(name) {
1946
- await execa4("docker", ["network", "rm", name], { reject: false });
2190
+ await execa2("docker", ["network", "rm", name], { reject: false });
1947
2191
  }
1948
2192
  async function pauseContainer(name) {
1949
- await execa4("docker", ["pause", name]);
2193
+ await execa2("docker", ["pause", name]);
1950
2194
  }
1951
2195
  async function unpauseContainer(name) {
1952
- await execa4("docker", ["unpause", name]);
2196
+ await execa2("docker", ["unpause", name]);
1953
2197
  }
1954
2198
  async function stopContainer(name) {
1955
- await execa4("docker", ["stop", name]);
2199
+ await execa2("docker", ["stop", name]);
1956
2200
  }
1957
2201
  async function startContainer(name) {
1958
- await execa4("docker", ["start", name]);
2202
+ await execa2("docker", ["start", name]);
1959
2203
  }
1960
2204
  async function inspectContainerStatus(name) {
1961
- const result = await execa4("docker", ["inspect", "--format", "{{.State.Status}}", name], {
2205
+ const result = await execa2("docker", ["inspect", "--format", "{{.State.Status}}", name], {
1962
2206
  reject: false
1963
2207
  });
1964
2208
  if (result.exitCode !== 0) return "missing";
@@ -1979,7 +2223,7 @@ async function inspectContainerStatus(name) {
1979
2223
  }
1980
2224
  }
1981
2225
  async function inspectContainer(name) {
1982
- const result = await execa4("docker", ["inspect", name], { reject: false });
2226
+ const result = await execa2("docker", ["inspect", name], { reject: false });
1983
2227
  if (result.exitCode !== 0) return null;
1984
2228
  try {
1985
2229
  const parsed = JSON.parse(result.stdout ?? "null");
@@ -1989,7 +2233,7 @@ async function inspectContainer(name) {
1989
2233
  }
1990
2234
  }
1991
2235
  async function inspectVolumeMountpoint(name) {
1992
- const result = await execa4("docker", ["volume", "inspect", "--format", "{{.Mountpoint}}", name], {
2236
+ const result = await execa2("docker", ["volume", "inspect", "--format", "{{.Mountpoint}}", name], {
1993
2237
  reject: false
1994
2238
  });
1995
2239
  if (result.exitCode !== 0) return null;
@@ -1997,7 +2241,7 @@ async function inspectVolumeMountpoint(name) {
1997
2241
  }
1998
2242
  var AGENTBOX_PREFIX = "agentbox-";
1999
2243
  async function listAgentboxContainers() {
2000
- const result = await execa4(
2244
+ const result = await execa2(
2001
2245
  "docker",
2002
2246
  ["ps", "-a", "--filter", `name=^${AGENTBOX_PREFIX}`, "--format", "{{.Names}}"],
2003
2247
  { reject: false }
@@ -2006,7 +2250,7 @@ async function listAgentboxContainers() {
2006
2250
  return (result.stdout ?? "").split("\n").map((s) => s.trim()).filter((s) => s.startsWith(AGENTBOX_PREFIX));
2007
2251
  }
2008
2252
  async function publishedHostPort(container, containerPort) {
2009
- const result = await execa4("docker", ["port", container, `${String(containerPort)}/tcp`], {
2253
+ const result = await execa2("docker", ["port", container, `${String(containerPort)}/tcp`], {
2010
2254
  reject: false
2011
2255
  });
2012
2256
  if (result.exitCode !== 0) return null;
@@ -2016,7 +2260,7 @@ async function publishedHostPort(container, containerPort) {
2016
2260
  return m ? Number(m[1]) : null;
2017
2261
  }
2018
2262
  async function listAgentboxVolumes() {
2019
- const result = await execa4(
2263
+ const result = await execa2(
2020
2264
  "docker",
2021
2265
  ["volume", "ls", "--filter", `name=^${AGENTBOX_PREFIX}`, "--format", "{{.Name}}"],
2022
2266
  { reject: false }
@@ -2388,6 +2632,123 @@ var ExportError = class extends Error {
2388
2632
  stdout;
2389
2633
  stderr;
2390
2634
  };
2635
+ async function copyCarryPathsToBox(opts) {
2636
+ const log = opts.onLog ?? (() => {
2637
+ });
2638
+ let copied = 0;
2639
+ const errors = [];
2640
+ const applied = [];
2641
+ for (const [i, entry] of opts.entries.entries()) {
2642
+ const where = `carry[${String(i)}] "${entry.rawSrc}"`;
2643
+ if (entry.kind === "missing") {
2644
+ log(`${where}: skipped (missing on host, optional)`);
2645
+ continue;
2646
+ }
2647
+ try {
2648
+ await copyOneEntry(opts.container, entry);
2649
+ copied += 1;
2650
+ applied.push({ src: entry.absSrc, dest: entry.absDest, bytes: entry.bytes ?? 0 });
2651
+ } catch (err) {
2652
+ const msg = err instanceof Error ? err.message : String(err);
2653
+ errors.push(`${where}: ${msg}`);
2654
+ log(`${where}: failed: ${msg}`);
2655
+ }
2656
+ }
2657
+ return { copied, errors, applied };
2658
+ }
2659
+ var BOX_HOME = "/home/vscode";
2660
+ async function copyOneEntry(container, entry) {
2661
+ if (entry.kind === "missing") return;
2662
+ const boxDest = entry.absDest.startsWith("~/") ? `${BOX_HOME}/${entry.absDest.slice(2)}` : entry.absDest;
2663
+ const boxDestParent = boxDest.endsWith("/") ? boxDest.slice(0, -1) : boxDest;
2664
+ const parentDir = entry.kind === "dir" ? boxDestParent : dirnameUnix(boxDestParent);
2665
+ const mkdir8 = await execa22(
2666
+ "docker",
2667
+ ["exec", "--user", "0:0", container, "mkdir", "-p", parentDir],
2668
+ { reject: false }
2669
+ );
2670
+ if (mkdir8.exitCode !== 0) {
2671
+ throw new Error(`mkdir -p ${parentDir} failed: ${String(mkdir8.stderr).slice(0, 300)}`);
2672
+ }
2673
+ if (entry.kind === "file") {
2674
+ const cp = await execa22(
2675
+ "docker",
2676
+ ["cp", entry.absSrc, `${container}:${boxDest}`],
2677
+ { reject: false }
2678
+ );
2679
+ if (cp.exitCode !== 0) {
2680
+ throw new Error(`docker cp failed: ${String(cp.stderr).slice(0, 300)}`);
2681
+ }
2682
+ } else {
2683
+ const packed = await execa22(
2684
+ "tar",
2685
+ ["-C", entry.absSrc, "-cf", "-", "."],
2686
+ { encoding: "buffer", reject: false }
2687
+ );
2688
+ if (packed.exitCode !== 0) {
2689
+ throw new Error(`tar pack failed: ${String(packed.stderr).slice(0, 300)}`);
2690
+ }
2691
+ const extract = await execa22(
2692
+ "docker",
2693
+ [
2694
+ "exec",
2695
+ "-i",
2696
+ "--user",
2697
+ "0:0",
2698
+ container,
2699
+ "tar",
2700
+ "-xf",
2701
+ "-",
2702
+ "-C",
2703
+ boxDest,
2704
+ "--no-same-permissions",
2705
+ "--no-same-owner",
2706
+ "-m"
2707
+ ],
2708
+ { input: packed.stdout, reject: false }
2709
+ );
2710
+ if (extract.exitCode !== 0) {
2711
+ throw new Error(`tar extract failed: ${String(extract.stderr).slice(0, 300)}`);
2712
+ }
2713
+ }
2714
+ if (entry.mode !== void 0) {
2715
+ const modeStr = entry.mode.toString(8).padStart(4, "0");
2716
+ const chmod2 = await execa22(
2717
+ "docker",
2718
+ ["exec", "--user", "0:0", container, "chmod", "-R", modeStr, boxDest],
2719
+ { reject: false }
2720
+ );
2721
+ if (chmod2.exitCode !== 0) {
2722
+ throw new Error(`chmod failed: ${String(chmod2.stderr).slice(0, 300)}`);
2723
+ }
2724
+ }
2725
+ const uid = entry.user ?? 1e3;
2726
+ const chown = await execa22(
2727
+ "docker",
2728
+ ["exec", "--user", "0:0", container, "chown", "-R", `${String(uid)}:${String(uid)}`, boxDest],
2729
+ { reject: false }
2730
+ );
2731
+ if (chown.exitCode !== 0) {
2732
+ throw new Error(`chown failed: ${String(chown.stderr).slice(0, 300)}`);
2733
+ }
2734
+ if (boxDest.startsWith(BOX_HOME + "/") && dirnameUnix(boxDest) !== BOX_HOME) {
2735
+ const safeDest = boxDest.replace(/'/g, `'\\''`);
2736
+ const script = `set -e; parent="$(dirname '${safeDest}')"; while [ "$parent" != "${BOX_HOME}" ] && [ "$parent" != "/" ]; do chown ${String(uid)}:${String(uid)} "$parent"; parent="$(dirname "$parent")"; done`;
2737
+ const chownParents = await execa22(
2738
+ "docker",
2739
+ ["exec", "--user", "0:0", container, "bash", "-c", script],
2740
+ { reject: false }
2741
+ );
2742
+ if (chownParents.exitCode !== 0) {
2743
+ throw new Error(`chown parents failed: ${String(chownParents.stderr).slice(0, 300)}`);
2744
+ }
2745
+ }
2746
+ }
2747
+ function dirnameUnix(p) {
2748
+ const i = p.lastIndexOf("/");
2749
+ if (i <= 0) return "/";
2750
+ return p.slice(0, i);
2751
+ }
2391
2752
  var SHARED_CLAUDE_VOLUME = "agentbox-claude-config";
2392
2753
  var DEFAULT_CLAUDE_SESSION = "claude";
2393
2754
  var CONTAINER_CLAUDE_DIR = "/home/vscode/.claude";
@@ -2403,7 +2764,7 @@ function resolveClaudeVolume(opts) {
2403
2764
  }
2404
2765
  async function pathExists(p) {
2405
2766
  try {
2406
- await stat4(p);
2767
+ await stat3(p);
2407
2768
  return true;
2408
2769
  } catch {
2409
2770
  return false;
@@ -2427,10 +2788,10 @@ async function findBrokenSymlinks(root) {
2427
2788
  return;
2428
2789
  }
2429
2790
  for (const ent of entries) {
2430
- const full = join23(dir, ent.name);
2791
+ const full = join22(dir, ent.name);
2431
2792
  if (ent.isSymbolicLink()) {
2432
2793
  try {
2433
- await stat4(full);
2794
+ await stat3(full);
2434
2795
  } catch {
2435
2796
  broken.push(relative(root, full));
2436
2797
  }
@@ -2447,13 +2808,13 @@ async function ensureClaudeVolume(spec, opts) {
2447
2808
  await ensureVolume(spec.volume);
2448
2809
  const created = !existed;
2449
2810
  if (!opts.syncFromHost) return { created, synced: false };
2450
- const hostClaude = join23(homedir22(), ".claude");
2811
+ const hostClaude = join22(homedir2(), ".claude");
2451
2812
  if (!await pathExists(hostClaude)) return { created, synced: false };
2452
- const hostClaudeJson = join23(homedir22(), ".claude.json");
2813
+ const hostClaudeJson = join22(homedir2(), ".claude.json");
2453
2814
  const hasJson = await pathExists(hostClaudeJson);
2454
2815
  const seedClaudeJson = !await volumeHasClaudeJson(spec.volume, opts.image);
2455
- const hostHome = homedir22();
2456
- const hostAgents = join23(homedir22(), ".agents");
2816
+ const hostHome = homedir2();
2817
+ const hostAgents = join22(homedir2(), ".agents");
2457
2818
  const hasAgents = await pathExists(hostAgents);
2458
2819
  const args = [
2459
2820
  "run",
@@ -2471,15 +2832,15 @@ async function ensureClaudeVolume(spec, opts) {
2471
2832
  ];
2472
2833
  if (hasJson && seedClaudeJson) args.push("-v", `${hostClaudeJson}:/src-claude-json:ro`);
2473
2834
  if (hasAgents) args.push("-v", `${hostAgents}:/.agents:ro`);
2474
- const filterDir = await mkdtemp(join23(tmpdir(), "agentbox-claude-filter-"));
2835
+ const filterDir = await mkdtemp(join22(tmpdir(), "agentbox-claude-filter-"));
2475
2836
  let filteredHookCount = 0;
2476
2837
  let installMethodFixed = false;
2477
2838
  let aliasedProjectKey = false;
2478
2839
  let workspaceTrusted = false;
2479
2840
  try {
2480
2841
  const settingsResult = await maybeFilterTo(
2481
- join23(hostClaude, "settings.json"),
2482
- join23(filterDir, "settings.json"),
2842
+ join22(hostClaude, "settings.json"),
2843
+ join22(filterDir, "settings.json"),
2483
2844
  hostHome
2484
2845
  );
2485
2846
  filteredHookCount += settingsResult.removedHooks;
@@ -2487,7 +2848,7 @@ async function ensureClaudeVolume(spec, opts) {
2487
2848
  } else if (hasJson) {
2488
2849
  const jsonResult = await maybeFilterTo(
2489
2850
  hostClaudeJson,
2490
- join23(filterDir, "_claude.json"),
2851
+ join22(filterDir, "_claude.json"),
2491
2852
  hostHome,
2492
2853
  {
2493
2854
  setInstallMethodNative: true,
@@ -2501,7 +2862,7 @@ async function ensureClaudeVolume(spec, opts) {
2501
2862
  workspaceTrusted = jsonResult.workspaceTrusted;
2502
2863
  } else {
2503
2864
  await writeFile3(
2504
- join23(filterDir, "_claude.json"),
2865
+ join22(filterDir, "_claude.json"),
2505
2866
  JSON.stringify(
2506
2867
  {
2507
2868
  installMethod: "native",
@@ -2603,7 +2964,7 @@ async function maybeFilterTo(src, dest, hostHome, opts = {}) {
2603
2964
  };
2604
2965
  let parsed;
2605
2966
  try {
2606
- parsed = JSON.parse(await readFile23(src, "utf8"));
2967
+ parsed = JSON.parse(await readFile24(src, "utf8"));
2607
2968
  } catch {
2608
2969
  return zero;
2609
2970
  }
@@ -2664,14 +3025,14 @@ var PLUGIN_INSTALL_BACKOFF_MIN = Math.round(PLUGIN_INSTALL_BACKOFF_MS / 6e4);
2664
3025
  var NPM_CACHE_DIR = "/home/vscode/.claude/.agentbox-npm-cache";
2665
3026
  async function isFile(p) {
2666
3027
  try {
2667
- return (await stat4(p)).isFile();
3028
+ return (await stat3(p)).isFile();
2668
3029
  } catch {
2669
3030
  return false;
2670
3031
  }
2671
3032
  }
2672
3033
  async function isRecentFailMarker(p) {
2673
3034
  try {
2674
- const st = await stat4(p);
3035
+ const st = await stat3(p);
2675
3036
  return Date.now() - st.mtimeMs < PLUGIN_INSTALL_BACKOFF_MS;
2676
3037
  } catch {
2677
3038
  return false;
@@ -2679,14 +3040,14 @@ async function isRecentFailMarker(p) {
2679
3040
  }
2680
3041
  async function isDir(p) {
2681
3042
  try {
2682
- return (await stat4(p)).isDirectory();
3043
+ return (await stat3(p)).isDirectory();
2683
3044
  } catch {
2684
3045
  return false;
2685
3046
  }
2686
3047
  }
2687
3048
  async function readReferencedPluginKeys(installedPluginsJsonPath) {
2688
3049
  try {
2689
- const raw = await readFile23(installedPluginsJsonPath, "utf8");
3050
+ const raw = await readFile24(installedPluginsJsonPath, "utf8");
2690
3051
  return referencedPluginVersionKeys(JSON.parse(raw));
2691
3052
  } catch {
2692
3053
  return /* @__PURE__ */ new Set();
@@ -2694,7 +3055,7 @@ async function readReferencedPluginKeys(installedPluginsJsonPath) {
2694
3055
  }
2695
3056
  async function scanPluginCacheForRebuild(cacheRoot) {
2696
3057
  const referenced = await readReferencedPluginKeys(
2697
- join23(cacheRoot, "..", "installed_plugins.json")
3058
+ join22(cacheRoot, "..", "installed_plugins.json")
2698
3059
  );
2699
3060
  let marketplaces;
2700
3061
  try {
@@ -2704,7 +3065,7 @@ async function scanPluginCacheForRebuild(cacheRoot) {
2704
3065
  }
2705
3066
  for (const m of marketplaces) {
2706
3067
  if (!m.isDirectory()) continue;
2707
- const mPath = join23(cacheRoot, m.name);
3068
+ const mPath = join22(cacheRoot, m.name);
2708
3069
  let plugins;
2709
3070
  try {
2710
3071
  plugins = await readdir3(mPath, { withFileTypes: true });
@@ -2713,7 +3074,7 @@ async function scanPluginCacheForRebuild(cacheRoot) {
2713
3074
  }
2714
3075
  for (const p of plugins) {
2715
3076
  if (!p.isDirectory()) continue;
2716
- const pPath = join23(mPath, p.name);
3077
+ const pPath = join22(mPath, p.name);
2717
3078
  let versions;
2718
3079
  try {
2719
3080
  versions = await readdir3(pPath, { withFileTypes: true });
@@ -2723,10 +3084,10 @@ async function scanPluginCacheForRebuild(cacheRoot) {
2723
3084
  for (const v of versions) {
2724
3085
  if (!v.isDirectory()) continue;
2725
3086
  if (referenced.size > 0 && !referenced.has(`${m.name}/${p.name}/${v.name}`)) continue;
2726
- const vPath = join23(pPath, v.name);
2727
- if (!await isFile(join23(vPath, "package.json"))) continue;
2728
- if (await isFile(join23(vPath, PLUGIN_INSTALLED_MARKER))) continue;
2729
- if (await isRecentFailMarker(join23(vPath, PLUGIN_FAILED_MARKER))) continue;
3087
+ const vPath = join22(pPath, v.name);
3088
+ if (!await isFile(join22(vPath, "package.json"))) continue;
3089
+ if (await isFile(join22(vPath, PLUGIN_INSTALLED_MARKER))) continue;
3090
+ if (await isRecentFailMarker(join22(vPath, PLUGIN_FAILED_MARKER))) continue;
2730
3091
  return true;
2731
3092
  }
2732
3093
  }
@@ -3018,67 +3379,31 @@ async function waitForTmuxPaneContent(container, sessionName, timeoutMs = 2e4) {
3018
3379
  await delay(400);
3019
3380
  }
3020
3381
  }
3021
- function buildTmuxSessionArgs(sessionName) {
3022
- const s = sessionName;
3382
+ function tmuxConfigSubcommands(sessionName) {
3023
3383
  return [
3024
- // Server-global (no -t): primary prefix Ctrl+a (dashboard parity), keep
3025
- // tmux's default Ctrl+b as a secondary prefix so users with existing
3026
- // muscle memory / integrations aren't broken. `d` is the same key under
3027
- // both prefixes (single key table) -> Ctrl+a d AND Ctrl+b d both detach
3028
- // (and `d` is already tmux's built-in detach key — bound explicitly so
3029
- // the contract is visible). `send-prefix` / `send-prefix -2` let a
3030
- // double-tap of either prefix reach Claude as that literal key.
3031
- ";",
3032
- "set",
3033
- "-g",
3034
- "prefix",
3035
- "C-a",
3036
- ";",
3037
- "set",
3038
- "-g",
3039
- "prefix2",
3040
- "C-b",
3041
- ";",
3042
- "bind-key",
3043
- "C-a",
3044
- "send-prefix",
3045
- ";",
3046
- "bind-key",
3047
- "C-b",
3048
- "send-prefix",
3049
- "-2",
3050
- ";",
3051
- "bind-key",
3052
- "d",
3053
- "detach-client",
3054
- // Modified-key reporting: without `extended-keys on`, tmux strips the
3055
- // modifier from Shift+Enter / Ctrl+Enter / etc. so Claude Code can't
3056
- // distinguish them from a plain Enter — pressing Shift+Enter submits the
3057
- // prompt instead of inserting a newline. `csi-u` is the format Claude
3058
- // Code recognises after `/terminal-setup`. Server-global so it survives
3059
- // grouped sibling sessions (e.g. the dashboard's `<name>-dash`).
3060
- ";",
3061
- "set",
3062
- "-g",
3063
- "extended-keys",
3064
- "on",
3065
- ";",
3066
- "set",
3067
- "-as",
3068
- "terminal-features",
3069
- ",*:extkeys",
3070
- // Hide the inner tmux status bar — the wrapped-pty footer (for
3071
- // `agentbox claude` / `agentbox shell`) and the dashboard's own status
3072
- // row already show the box name + detach hint; without `status off`
3073
- // they double up.
3074
- ";",
3075
- "set",
3076
- "-t",
3077
- s,
3078
- "status",
3079
- "off"
3384
+ ["set", "-g", "prefix", "C-a"],
3385
+ ["set", "-g", "prefix2", "C-b"],
3386
+ ["bind-key", "C-a", "send-prefix"],
3387
+ ["bind-key", "C-b", "send-prefix", "-2"],
3388
+ ["bind-key", "d", "detach-client"],
3389
+ ["set", "-g", "extended-keys", "on"],
3390
+ ["set", "-as", "terminal-features", ",*:extkeys"],
3391
+ ["set", "-t", sessionName, "status", "off"]
3080
3392
  ];
3081
3393
  }
3394
+ function buildTmuxSessionArgs(sessionName) {
3395
+ const out = [];
3396
+ for (const sub of tmuxConfigSubcommands(sessionName)) {
3397
+ out.push(";", ...sub);
3398
+ }
3399
+ return out;
3400
+ }
3401
+ function buildTmuxConfigShellSnippet(sessionName) {
3402
+ return tmuxConfigSubcommands(sessionName).map((sub) => `tmux ${sub.map(shellSingleQuoteIfNeeded).join(" ")}`).join("; ");
3403
+ }
3404
+ function shellSingleQuoteIfNeeded(s) {
3405
+ return /^[A-Za-z0-9_:.\/=+-]+$/.test(s) ? s : "'" + s.replace(/'/g, "'\\''") + "'";
3406
+ }
3082
3407
  function buildShellArgv(container) {
3083
3408
  const term = process.env["TERM"] ?? "xterm-256color";
3084
3409
  return ["exec", "-it", "-e", `TERM=${term}`, "--user", CONTAINER_USER, container, "bash", "-l"];
@@ -3197,14 +3522,14 @@ async function listChildDirs(dir) {
3197
3522
  }
3198
3523
  async function readJsonFile(path) {
3199
3524
  try {
3200
- return JSON.parse(await readFile23(path, "utf8"));
3525
+ return JSON.parse(await readFile24(path, "utf8"));
3201
3526
  } catch {
3202
3527
  return void 0;
3203
3528
  }
3204
3529
  }
3205
3530
  async function pullClaudeExtras(spec, opts) {
3206
- const hostHome = homedir22();
3207
- const hostClaude = join23(hostHome, ".claude");
3531
+ const hostHome = homedir2();
3532
+ const hostClaude = join22(hostHome, ".claude");
3208
3533
  const inventoryScript = [
3209
3534
  "for cat in skills agents commands; do",
3210
3535
  ' [ -d "/src/$cat" ] || continue;',
@@ -3266,7 +3591,7 @@ async function pullClaudeExtras(spec, opts) {
3266
3591
  const newItems = [];
3267
3592
  const applyPaths = [];
3268
3593
  for (const cat of PULL_DIR_CATEGORIES) {
3269
- const hostNames = await listChildDirs(join23(hostClaude, cat));
3594
+ const hostNames = await listChildDirs(join22(hostClaude, cat));
3270
3595
  const excludes = cat === "skills" ? SKILL_EXCLUDE_PREFIXES : [];
3271
3596
  for (const name of pickNewItems(boxDirs[cat] ?? [], hostNames, excludes)) {
3272
3597
  newItems.push({ category: cat, name });
@@ -3274,8 +3599,8 @@ async function pullClaudeExtras(spec, opts) {
3274
3599
  }
3275
3600
  }
3276
3601
  const hostPluginKeys = [];
3277
- for (const m of await listChildDirs(join23(hostClaude, "plugins", "cache"))) {
3278
- for (const p of await listChildDirs(join23(hostClaude, "plugins", "cache", m))) {
3602
+ for (const m of await listChildDirs(join22(hostClaude, "plugins", "cache"))) {
3603
+ for (const p of await listChildDirs(join22(hostClaude, "plugins", "cache", m))) {
3279
3604
  hostPluginKeys.push(`${m}/${p}`);
3280
3605
  }
3281
3606
  }
@@ -3283,8 +3608,8 @@ async function pullClaudeExtras(spec, opts) {
3283
3608
  newItems.push({ category: "plugins", name: key });
3284
3609
  applyPaths.push({ src: `/src/plugins/cache/${key}`, dest: `/dst/plugins/cache/${key}` });
3285
3610
  }
3286
- const hostInstalled = await readJsonFile(join23(hostClaude, "plugins", "installed_plugins.json"));
3287
- const hostMarkets = await readJsonFile(join23(hostClaude, "plugins", "known_marketplaces.json"));
3611
+ const hostInstalled = await readJsonFile(join22(hostClaude, "plugins", "installed_plugins.json"));
3612
+ const hostMarkets = await readJsonFile(join22(hostClaude, "plugins", "known_marketplaces.json"));
3288
3613
  const mergedInstalled = mergeInstalledPlugins(hostInstalled, boxJson["installed_plugins"], {
3289
3614
  hostHome
3290
3615
  });
@@ -3327,17 +3652,17 @@ async function pullClaudeExtras(spec, opts) {
3327
3652
  }
3328
3653
  }
3329
3654
  if (mergedMarkets.changed || mergedInstalled.changed) {
3330
- await mkdir22(join23(hostClaude, "plugins"), { recursive: true });
3655
+ await mkdir22(join22(hostClaude, "plugins"), { recursive: true });
3331
3656
  if (mergedMarkets.changed) {
3332
3657
  await writeFile3(
3333
- join23(hostClaude, "plugins", "known_marketplaces.json"),
3658
+ join22(hostClaude, "plugins", "known_marketplaces.json"),
3334
3659
  `${JSON.stringify(mergedMarkets.data, null, 2)}
3335
3660
  `
3336
3661
  );
3337
3662
  }
3338
3663
  if (mergedInstalled.changed) {
3339
3664
  await writeFile3(
3340
- join23(hostClaude, "plugins", "installed_plugins.json"),
3665
+ join22(hostClaude, "plugins", "installed_plugins.json"),
3341
3666
  `${JSON.stringify(mergedInstalled.data, null, 2)}
3342
3667
  `
3343
3668
  );
@@ -3345,7 +3670,7 @@ async function pullClaudeExtras(spec, opts) {
3345
3670
  }
3346
3671
  return { newItems, mergedRegistries };
3347
3672
  }
3348
- var CREDENTIALS_BACKUP_FILE = join32(STATE_DIR2, "claude-credentials.json");
3673
+ var CREDENTIALS_BACKUP_FILE = join32(STATE_DIR, "claude-credentials.json");
3349
3674
  async function hostBackupHasCredentials(path = CREDENTIALS_BACKUP_FILE) {
3350
3675
  try {
3351
3676
  const parsed = JSON.parse(await readFile32(path, "utf8"));
@@ -3377,8 +3702,8 @@ echo "EXTRACTED=$EXTRACTED SEEDED=$SEEDED VOLREAL=$VOL_REAL"
3377
3702
  `;
3378
3703
  async function syncClaudeCredentials(spec, opts) {
3379
3704
  try {
3380
- await mkdir32(STATE_DIR2, { recursive: true });
3381
- const { stdout } = await execa42("docker", [
3705
+ await mkdir32(STATE_DIR, { recursive: true });
3706
+ const { stdout } = await execa4("docker", [
3382
3707
  "run",
3383
3708
  "--rm",
3384
3709
  "--user",
@@ -3386,7 +3711,7 @@ async function syncClaudeCredentials(spec, opts) {
3386
3711
  "-v",
3387
3712
  `${spec.volume}:/dst`,
3388
3713
  "-v",
3389
- `${STATE_DIR2}:/host-state`,
3714
+ `${STATE_DIR}:/host-state`,
3390
3715
  "-e",
3391
3716
  `ISOLATE=${opts.isolate ? "yes" : "no"}`,
3392
3717
  opts.image,
@@ -3529,9 +3854,10 @@ ${(install.stdout ?? "").toString().slice(-600)}`
3529
3854
  }
3530
3855
  return { installed: true };
3531
3856
  }
3857
+ var CODEX_AGENTBOX_FLAGS = ["--enable", "hooks", "--dangerously-bypass-hook-trust"];
3532
3858
  async function startCodexSession(opts) {
3533
3859
  const sessionName = opts.sessionName ?? DEFAULT_CODEX_SESSION;
3534
- const cmd = ["codex", ...opts.codexArgs].map(shQuote2).join(" ");
3860
+ const cmd = ["codex", ...CODEX_AGENTBOX_FLAGS, ...opts.codexArgs].map(shQuote2).join(" ");
3535
3861
  const term = process.env["TERM"] ?? "xterm-256color";
3536
3862
  const envFlags = ["-e", `TERM=${term}`];
3537
3863
  for (const k of CODEX_FORWARDED_ENV_KEYS) {
@@ -3726,6 +4052,8 @@ var SHARED_OPENCODE_VOLUME = "agentbox-opencode-config";
3726
4052
  var DEFAULT_OPENCODE_SESSION = "opencode";
3727
4053
  var CONTAINER_OPENCODE_DIR = "/home/vscode/.local/share/opencode";
3728
4054
  var CONTAINER_OPENCODE_CONFIG_DIR = "/home/vscode/.local/share/opencode/config";
4055
+ var CONTAINER_OPENCODE_STATE_HOME = "/home/vscode/.local/share/opencode/.state";
4056
+ var IN_BOX_OPENCODE_PLUGIN_PATH = "/usr/local/share/agentbox/opencode-agentbox-plugin.js";
3729
4057
  function resolveOpencodeVolume(opts) {
3730
4058
  if (opts.isolate) {
3731
4059
  return { volume: `${SHARED_OPENCODE_VOLUME}-${opts.boxId}` };
@@ -3751,13 +4079,16 @@ async function ensureOpencodeVolume(spec, opts) {
3751
4079
  const created = !existed;
3752
4080
  const hostData = join5(homedir4(), ".local", "share", "opencode");
3753
4081
  const hostConfig = join5(homedir4(), ".config", "opencode");
4082
+ const hostState = join5(homedir4(), ".local", "state", "opencode");
3754
4083
  const hasData = await pathExists3(hostData);
3755
4084
  const hasConfig = await pathExists3(hostConfig);
3756
- const willSync = opts.syncFromHost && (hasData || hasConfig);
4085
+ const hasState = await pathExists3(hostState);
4086
+ const willSync = opts.syncFromHost && (hasData || hasConfig || hasState);
3757
4087
  if (willSync) {
3758
4088
  const args = ["run", "--rm", "--user", "0", "-v", `${spec.volume}:/dst`];
3759
4089
  if (hasData) args.push("-v", `${hostData}:/src-data:ro`);
3760
4090
  if (hasConfig) args.push("-v", `${hostConfig}:/src-config:ro`);
4091
+ if (hasState) args.push("-v", `${hostState}:/src-state:ro`);
3761
4092
  const steps = [];
3762
4093
  if (hasData) {
3763
4094
  steps.push(
@@ -3767,6 +4098,11 @@ async function ensureOpencodeVolume(spec, opts) {
3767
4098
  if (hasConfig) {
3768
4099
  steps.push("mkdir -p /dst/config && rsync -a /src-config/ /dst/config/");
3769
4100
  }
4101
+ if (hasState) {
4102
+ steps.push(
4103
+ "mkdir -p /dst/.state/opencode && rsync -a --update --exclude=locks /src-state/ /dst/.state/opencode/"
4104
+ );
4105
+ }
3770
4106
  steps.push("chown -R 1000:1000 /dst");
3771
4107
  args.push(opts.image, "sh", "-c", steps.join(" && "));
3772
4108
  await execa6("docker", args);
@@ -3790,6 +4126,25 @@ async function ensureOpencodeVolume(spec, opts) {
3790
4126
  );
3791
4127
  return { created, synced: false };
3792
4128
  }
4129
+ async function seedOpencodePlugin(volume, image) {
4130
+ try {
4131
+ const { stdout } = await execa6("docker", [
4132
+ "run",
4133
+ "--rm",
4134
+ "--user",
4135
+ "0",
4136
+ "-v",
4137
+ `${volume}:/dst`,
4138
+ image,
4139
+ "sh",
4140
+ "-c",
4141
+ `{ [ -f ${IN_BOX_OPENCODE_PLUGIN_PATH} ] && mkdir -p /dst/config/plugins && cp -a ${IN_BOX_OPENCODE_PLUGIN_PATH} /dst/config/plugins/agentbox-state.js && chown -R 1000:1000 /dst/config/plugins && echo SEEDED; } || true`
4142
+ ]);
4143
+ return { seeded: stdout.includes("SEEDED") };
4144
+ } catch {
4145
+ return { seeded: false };
4146
+ }
4147
+ }
3793
4148
  var OPENCODE_FORWARDED_ENV_KEYS = [
3794
4149
  "ANTHROPIC_API_KEY",
3795
4150
  "OPENAI_API_KEY",
@@ -3800,7 +4155,10 @@ var OPENCODE_FORWARDED_ENV_KEYS = [
3800
4155
  "GROQ_API_KEY"
3801
4156
  ];
3802
4157
  function buildOpencodeMounts(spec, hostEnv) {
3803
- const env = { OPENCODE_CONFIG_DIR: CONTAINER_OPENCODE_CONFIG_DIR };
4158
+ const env = {
4159
+ OPENCODE_CONFIG_DIR: CONTAINER_OPENCODE_CONFIG_DIR,
4160
+ XDG_STATE_HOME: CONTAINER_OPENCODE_STATE_HOME
4161
+ };
3804
4162
  for (const k of OPENCODE_FORWARDED_ENV_KEYS) {
3805
4163
  const v = hostEnv[k];
3806
4164
  if (typeof v === "string" && v.length > 0) env[k] = v;
@@ -3915,6 +4273,8 @@ function buildOpencodeLoginRunArgv(opts) {
3915
4273
  "DISPLAY=",
3916
4274
  "-e",
3917
4275
  `OPENCODE_CONFIG_DIR=${CONTAINER_OPENCODE_CONFIG_DIR}`,
4276
+ "-e",
4277
+ `XDG_STATE_HOME=${CONTAINER_OPENCODE_STATE_HOME}`,
3918
4278
  "-v",
3919
4279
  `${opts.volume}:${CONTAINER_OPENCODE_DIR}`,
3920
4280
  "--user",
@@ -4105,7 +4465,7 @@ async function launchVncDaemon(container, timeoutMs = 5e3) {
4105
4465
  }
4106
4466
  var VNC_PASSWORD_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
4107
4467
  function generateVncPassword() {
4108
- const bytes = randomBytes(8);
4468
+ const bytes = randomBytes2(8);
4109
4469
  let out = "";
4110
4470
  for (let i = 0; i < 8; i++) {
4111
4471
  out += VNC_PASSWORD_ALPHABET[bytes[i] % VNC_PASSWORD_ALPHABET.length];
@@ -4124,6 +4484,10 @@ function buildVncUrls(record, engine) {
4124
4484
  if (record.vncHostPort) {
4125
4485
  urls.loopbackUrl = `http://127.0.0.1:${String(record.vncHostPort)}/vnc.html?${qs}`;
4126
4486
  }
4487
+ if (record.portlessVncAlias) {
4488
+ const base = record.portlessVncUrl ?? `https://${record.portlessVncAlias}.localhost`;
4489
+ urls.portlessUrl = `${base}/vnc.html?${qs}`;
4490
+ }
4127
4491
  return urls;
4128
4492
  }
4129
4493
  var WEB_CONTAINER_PORT = 80;
@@ -4206,6 +4570,7 @@ async function seedWorkspace(opts) {
4206
4570
  for (const r of opts.repos) {
4207
4571
  const main = r.repo.hostMainRepo;
4208
4572
  const wt = r.gitWorktreePath;
4573
+ const baseRef = r.repo.kind === "root" ? opts.fromBranch ?? "HEAD" : "HEAD";
4209
4574
  const add = await execa7(
4210
4575
  "docker",
4211
4576
  [
@@ -4221,7 +4586,7 @@ async function seedWorkspace(opts) {
4221
4586
  "-b",
4222
4587
  r.branch,
4223
4588
  wt,
4224
- "HEAD"
4589
+ baseRef
4225
4590
  ],
4226
4591
  { reject: false }
4227
4592
  );
@@ -4515,79 +4880,6 @@ async function isProxyRunning() {
4515
4880
  return false;
4516
4881
  }
4517
4882
  }
4518
- var DEFAULT_BOX_IMAGE = "agentbox/box:dev";
4519
- var here = dirname4(fileURLToPath(import.meta.url));
4520
- function resolveDockerBuild() {
4521
- const override = process.env.AGENTBOX_DOCKER_CONTEXT;
4522
- if (override && existsSync2(resolve2(override, "Dockerfile.box"))) {
4523
- return { dockerfile: resolve2(override, "Dockerfile.box"), context: override };
4524
- }
4525
- const staged = resolve2(here, "..", "runtime", "docker");
4526
- if (existsSync2(resolve2(staged, "Dockerfile.box"))) {
4527
- return { dockerfile: resolve2(staged, "Dockerfile.box"), context: staged };
4528
- }
4529
- const packageRoot = resolve2(here, "..");
4530
- return {
4531
- dockerfile: resolve2(packageRoot, "Dockerfile.box"),
4532
- context: resolve2(packageRoot, "..", "..")
4533
- };
4534
- }
4535
- var { dockerfile: DOCKERFILE_PATH_RESOLVED, context: BUILD_CONTEXT_DIR_RESOLVED } = resolveDockerBuild();
4536
- var DOCKERFILE_PATH = DOCKERFILE_PATH_RESOLVED;
4537
- var BUILD_CONTEXT_DIR = BUILD_CONTEXT_DIR_RESOLVED;
4538
- async function imageExists(ref) {
4539
- const result = await execa9("docker", ["image", "inspect", ref], { reject: false });
4540
- return result.exitCode === 0;
4541
- }
4542
- async function imageInfo(ref = DEFAULT_BOX_IMAGE) {
4543
- const result = await execa9(
4544
- "docker",
4545
- ["image", "inspect", "--format", "{{.Size}}|{{.Created}}", ref],
4546
- { reject: false }
4547
- );
4548
- if (result.exitCode !== 0) return { ref, exists: false };
4549
- const [sizeStr, createdAt] = result.stdout.trim().split("|");
4550
- const sizeBytes = sizeStr ? Number.parseInt(sizeStr, 10) : NaN;
4551
- return {
4552
- ref,
4553
- exists: true,
4554
- sizeBytes: Number.isFinite(sizeBytes) ? sizeBytes : void 0,
4555
- createdAt: createdAt && createdAt.length > 0 ? createdAt : void 0
4556
- };
4557
- }
4558
- async function buildImage(opts = {}) {
4559
- const ref = opts.ref ?? DEFAULT_BOX_IMAGE;
4560
- const dockerfile = opts.dockerfile ?? DOCKERFILE_PATH;
4561
- const contextDir = opts.contextDir ?? BUILD_CONTEXT_DIR;
4562
- const subprocess = execa9("docker", ["build", "-t", ref, "-f", dockerfile, contextDir], {
4563
- stderr: "pipe",
4564
- stdout: "pipe"
4565
- });
4566
- if (opts.onProgress) {
4567
- const forward = (chunk) => {
4568
- const text = typeof chunk === "string" ? chunk : chunk.toString("utf8");
4569
- for (const line of text.split(/\r?\n/)) {
4570
- if (line.length > 0) opts.onProgress?.(line);
4571
- }
4572
- };
4573
- subprocess.stdout?.on("data", forward);
4574
- subprocess.stderr?.on("data", forward);
4575
- }
4576
- await subprocess;
4577
- return ref;
4578
- }
4579
- async function ensureImage(ref = DEFAULT_BOX_IMAGE, opts = {}) {
4580
- if (await imageExists(ref)) {
4581
- return { ref, built: false };
4582
- }
4583
- await buildImage({
4584
- ref,
4585
- dockerfile: opts.dockerfile,
4586
- contextDir: opts.contextDir,
4587
- onProgress: opts.onProgress
4588
- });
4589
- return { ref, built: true };
4590
- }
4591
4883
  var EXCLUDE_DIRS = /* @__PURE__ */ new Set([
4592
4884
  "node_modules",
4593
4885
  ".next",
@@ -4633,12 +4925,12 @@ async function findExcludedDirs(root, excluded = EXCLUDE_DIRS) {
4633
4925
  return matches;
4634
4926
  }
4635
4927
  async function createSnapshot(opts) {
4636
- const source = resolve22(opts.source);
4637
- const destination = resolve22(opts.destination);
4928
+ const source = resolve2(opts.source);
4929
+ const destination = resolve2(opts.destination);
4638
4930
  const excluded = opts.excluded ?? EXCLUDE_DIRS;
4639
4931
  await mkdir4(SNAPSHOTS_ROOT, { recursive: true });
4640
4932
  const cpArgs = platform() === "darwin" ? ["-cR"] : ["-R"];
4641
- await execa10("cp", [...cpArgs, `${source}/`, destination]);
4933
+ await execa9("cp", [...cpArgs, `${source}/`, destination]);
4642
4934
  const toPrune = await findExcludedDirs(destination, excluded);
4643
4935
  await Promise.all(toPrune.map((p) => rm22(p, { recursive: true, force: true })));
4644
4936
  return { destination, prunedPaths: toPrune };
@@ -4659,7 +4951,7 @@ async function readManifest(dir) {
4659
4951
  try {
4660
4952
  const raw = await readFile5(join8(dir, "manifest.json"), "utf8");
4661
4953
  const m = JSON.parse(raw);
4662
- if (m.schema !== 2) return null;
4954
+ if (m.schema !== 2 && m.schema !== 3) return null;
4663
4955
  return m;
4664
4956
  } catch {
4665
4957
  return null;
@@ -4747,7 +5039,7 @@ async function runCleanup(container, log) {
4747
5039
  }
4748
5040
  }
4749
5041
  async function inspectImageConfig(imageRef) {
4750
- const r = await execa11("docker", ["image", "inspect", imageRef], { reject: false });
5042
+ const r = await execa10("docker", ["image", "inspect", imageRef], { reject: false });
4751
5043
  if (r.exitCode !== 0) {
4752
5044
  throw new CheckpointError(`docker image inspect ${imageRef} failed`, r.stdout, r.stderr);
4753
5045
  }
@@ -4823,14 +5115,14 @@ async function createCheckpoint(opts) {
4823
5115
  await runCleanup(box.container, log);
4824
5116
  if (type === "layered") {
4825
5117
  log(`docker commit ${box.container} -> ${tag} (layered)`);
4826
- const r = await execa11("docker", ["commit", box.container, tag], { reject: false });
5118
+ const r = await execa10("docker", ["commit", box.container, tag], { reject: false });
4827
5119
  if (r.exitCode !== 0) {
4828
5120
  throw new CheckpointError(`docker commit failed for ${box.container}`, r.stdout, r.stderr);
4829
5121
  }
4830
5122
  } else {
4831
5123
  log(`docker commit ${box.container} -> <intermediate> (flattened path)`);
4832
5124
  const intermediate = `${tag}-intermediate`;
4833
- const commit = await execa11("docker", ["commit", box.container, intermediate], {
5125
+ const commit = await execa10("docker", ["commit", box.container, intermediate], {
4834
5126
  reject: false
4835
5127
  });
4836
5128
  if (commit.exitCode !== 0) {
@@ -4843,8 +5135,15 @@ async function createCheckpoint(opts) {
4843
5135
  }
4844
5136
  }
4845
5137
  const base = (box.gitWorktrees ?? []).some((w) => w.kind === "root") ? "worktree" : "workspace";
5138
+ const prepared = readPreparedDockerState();
5139
+ let baseFingerprint = prepared?.base?.contextSha256;
5140
+ if (!baseFingerprint) {
5141
+ const fp = await computeDockerContextFingerprint();
5142
+ baseFingerprint = fp?.contextSha256;
5143
+ }
5144
+ const stamp = readCliStamp();
4846
5145
  const manifest = {
4847
- schema: 2,
5146
+ schema: 3,
4848
5147
  name,
4849
5148
  type,
4850
5149
  image: tag,
@@ -4854,6 +5153,9 @@ async function createCheckpoint(opts) {
4854
5153
  sourceBoxId: box.id,
4855
5154
  sourceBoxName: box.name,
4856
5155
  worktrees: box.gitWorktrees,
5156
+ baseProvider: "docker",
5157
+ baseFingerprint,
5158
+ cliVersion: stamp.cliVersion,
4857
5159
  createdAt: (/* @__PURE__ */ new Date()).toISOString()
4858
5160
  };
4859
5161
  await writeFile22(join8(dir, "manifest.json"), JSON.stringify(manifest, null, 2) + "\n", "utf8");
@@ -4865,7 +5167,7 @@ async function createCheckpoint(opts) {
4865
5167
  }
4866
5168
  async function flattenImage(sourceTag, destTag, log) {
4867
5169
  const tmpName = `agentbox-flatten-${Date.now().toString(36)}`;
4868
- const create = await execa11(
5170
+ const create = await execa10(
4869
5171
  "docker",
4870
5172
  ["create", "--name", tmpName, sourceTag, "sleep", "0"],
4871
5173
  { reject: false }
@@ -4877,7 +5179,7 @@ async function flattenImage(sourceTag, destTag, log) {
4877
5179
  try {
4878
5180
  const rootfsPath = join8(scratch, "rootfs.tar");
4879
5181
  log(`exporting rootfs of ${sourceTag} to ${rootfsPath}`);
4880
- const exp = await execa11("docker", ["export", "-o", rootfsPath, tmpName], { reject: false });
5182
+ const exp = await execa10("docker", ["export", "-o", rootfsPath, tmpName], { reject: false });
4881
5183
  if (exp.exitCode !== 0) {
4882
5184
  throw new CheckpointError(`docker export failed`, exp.stdout, exp.stderr);
4883
5185
  }
@@ -4890,7 +5192,7 @@ async function flattenImage(sourceTag, destTag, log) {
4890
5192
  ];
4891
5193
  await writeFile22(join8(scratch, "Dockerfile"), lines.join("\n") + "\n", "utf8");
4892
5194
  log(`building flattened ${destTag} from rootfs.tar (FROM scratch)`);
4893
- const build = await execa11(
5195
+ const build = await execa10(
4894
5196
  "docker",
4895
5197
  ["build", "-t", destTag, "-f", join8(scratch, "Dockerfile"), scratch],
4896
5198
  { reject: false }
@@ -4899,7 +5201,7 @@ async function flattenImage(sourceTag, destTag, log) {
4899
5201
  throw new CheckpointError(`flatten docker build failed`, build.stdout, build.stderr);
4900
5202
  }
4901
5203
  } finally {
4902
- await execa11("docker", ["rm", "-f", tmpName], { reject: false });
5204
+ await execa10("docker", ["rm", "-f", tmpName], { reject: false });
4903
5205
  await rm3(scratch, { recursive: true, force: true });
4904
5206
  }
4905
5207
  }
@@ -4943,7 +5245,7 @@ async function pathExists4(p) {
4943
5245
  }
4944
5246
  async function writeBoxEnvFile(container, env) {
4945
5247
  const body = formatBoxEnvBody(env);
4946
- const result = await execa12(
5248
+ const result = await execa11(
4947
5249
  "docker",
4948
5250
  ["exec", "--user", "root", "-i", container, "sh", "-c", "umask 022 && cat > /etc/agentbox/box.env"],
4949
5251
  { input: body, reject: false }
@@ -4967,7 +5269,7 @@ function shellSingleQuote(s) {
4967
5269
  return `'${s.replace(/'/g, `'\\''`)}'`;
4968
5270
  }
4969
5271
  async function ensureHomeOwnedByVscode(container) {
4970
- await execa13(
5272
+ await execa12(
4971
5273
  "docker",
4972
5274
  [
4973
5275
  "exec",
@@ -5017,7 +5319,7 @@ async function ensureRelay(opts = {}) {
5017
5319
  return ENDPOINT;
5018
5320
  }
5019
5321
  if (existingPid !== null) {
5020
- await unlink(PID_FILE).catch(() => {
5322
+ await unlink2(PID_FILE).catch(() => {
5021
5323
  });
5022
5324
  }
5023
5325
  const relayBin = resolveRelayBin();
@@ -5053,16 +5355,16 @@ async function ensureRelay(opts = {}) {
5053
5355
  }
5054
5356
  function resolveRelayBin() {
5055
5357
  const override = process.env.AGENTBOX_RELAY_BIN;
5056
- if (override && existsSync3(override)) return override;
5057
- const here2 = dirname22(fileURLToPath2(import.meta.url));
5358
+ if (override && existsSync2(override)) return override;
5359
+ const here = dirname3(fileURLToPath(import.meta.url));
5058
5360
  const candidates = [
5059
- resolve3(here2, "..", "runtime", "relay", "bin.cjs"),
5060
- resolve3(here2, "..", "..", "relay", "dist", "bin.cjs"),
5061
- resolve3(here2, "..", "..", "..", "@agentbox", "relay", "dist", "bin.cjs"),
5062
- resolve3(here2, "..", "..", "node_modules", "@agentbox", "relay", "dist", "bin.cjs")
5361
+ resolve22(here, "..", "runtime", "relay", "bin.cjs"),
5362
+ resolve22(here, "..", "..", "relay", "dist", "bin.cjs"),
5363
+ resolve22(here, "..", "..", "..", "@agentbox", "relay", "dist", "bin.cjs"),
5364
+ resolve22(here, "..", "..", "node_modules", "@agentbox", "relay", "dist", "bin.cjs")
5063
5365
  ];
5064
5366
  for (const c of candidates) {
5065
- if (existsSync3(c)) return c;
5367
+ if (existsSync2(c)) return c;
5066
5368
  }
5067
5369
  throw new Error(
5068
5370
  `could not locate @agentbox/relay bin; tried:
@@ -5071,17 +5373,17 @@ function resolveRelayBin() {
5071
5373
  }
5072
5374
  function resolveCliEntry() {
5073
5375
  const override = process.env.AGENTBOX_CLI_ENTRY;
5074
- if (override && existsSync3(override)) return override;
5075
- const here2 = dirname22(fileURLToPath2(import.meta.url));
5376
+ if (override && existsSync2(override)) return override;
5377
+ const here = dirname3(fileURLToPath(import.meta.url));
5076
5378
  const candidates = [
5077
5379
  // Bundled CLI (dev + published): this module IS bundled into the CLI
5078
5380
  // entry, so the entry is index.js next to this file.
5079
- resolve3(here2, "index.js"),
5080
- resolve3(here2, "..", "..", "..", "apps", "cli", "dist", "index.js"),
5081
- resolve3(here2, "..", "..", "..", "..", "dist", "index.js")
5381
+ resolve22(here, "index.js"),
5382
+ resolve22(here, "..", "..", "..", "apps", "cli", "dist", "index.js"),
5383
+ resolve22(here, "..", "..", "..", "..", "dist", "index.js")
5082
5384
  ];
5083
5385
  for (const c of candidates) {
5084
- if (existsSync3(c)) return c;
5386
+ if (existsSync2(c)) return c;
5085
5387
  }
5086
5388
  return null;
5087
5389
  }
@@ -5091,7 +5393,7 @@ async function stopRelay() {
5091
5393
  return { stopped: false, pid: null };
5092
5394
  }
5093
5395
  if (!await processAlive(pid)) {
5094
- await unlink(PID_FILE).catch(() => {
5396
+ await unlink2(PID_FILE).catch(() => {
5095
5397
  });
5096
5398
  return { stopped: false, pid };
5097
5399
  }
@@ -5109,7 +5411,7 @@ async function stopRelay() {
5109
5411
  } catch {
5110
5412
  }
5111
5413
  }
5112
- await unlink(PID_FILE).catch(() => {
5414
+ await unlink2(PID_FILE).catch(() => {
5113
5415
  });
5114
5416
  return { stopped: true, pid };
5115
5417
  }
@@ -5200,7 +5502,7 @@ async function processAlive(pid) {
5200
5502
  }
5201
5503
  }
5202
5504
  function generateRelayToken() {
5203
- return randomBytes2(32).toString("hex");
5505
+ return randomBytes22(32).toString("hex");
5204
5506
  }
5205
5507
  async function registerBoxWithRelay(args) {
5206
5508
  const worktrees = (args.worktrees ?? []).map((w) => ({
@@ -5249,6 +5551,20 @@ async function clearRelayNotice(boxId, id) {
5249
5551
  } catch {
5250
5552
  }
5251
5553
  }
5554
+ async function mintHostInitiatedToken(boxId, method, paramsHash, ttlMs) {
5555
+ try {
5556
+ const body = await adminPostForJson("/admin/host-initiated/mint", {
5557
+ boxId,
5558
+ method,
5559
+ paramsHash,
5560
+ ...typeof ttlMs === "number" ? { ttlMs } : {}
5561
+ });
5562
+ const token = body?.token;
5563
+ return typeof token === "string" && token.length > 0 ? token : null;
5564
+ } catch {
5565
+ return null;
5566
+ }
5567
+ }
5252
5568
  async function adminPost(path, body) {
5253
5569
  const json = JSON.stringify(body);
5254
5570
  await new Promise((resolveP, rejectP) => {
@@ -5501,7 +5817,7 @@ function generateBoxId() {
5501
5817
  return randomBytes3(4).toString("hex");
5502
5818
  }
5503
5819
  function sanitizeBasename(workspacePath) {
5504
- const raw = basename2(resolve4(workspacePath));
5820
+ const raw = basename2(resolve3(workspacePath));
5505
5821
  return raw.toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/-+/g, "-").replace(/^[-._]+|[-._]+$/g, "").slice(0, 30).replace(/[-._]+$/, "");
5506
5822
  }
5507
5823
  function defaultBoxName(workspacePath, id) {
@@ -5532,7 +5848,7 @@ async function buildIdentityMounts() {
5532
5848
  async function createBox(opts) {
5533
5849
  const log = opts.onLog ?? (() => {
5534
5850
  });
5535
- const workspace = resolve4(opts.workspacePath);
5851
+ const workspace = resolve3(opts.workspacePath);
5536
5852
  if (!await pathExists5(workspace)) {
5537
5853
  throw new Error(`workspace does not exist: ${workspace}`);
5538
5854
  }
@@ -5567,6 +5883,27 @@ async function createBox(opts) {
5567
5883
  log(
5568
5884
  `starting from checkpoint ${opts.checkpointRef} (${head.manifest.type}, ${String(chain.length)} layer(s), image ${head.manifest.image})`
5569
5885
  );
5886
+ const ckptFingerprint = head.manifest.baseFingerprint;
5887
+ const ckptCliVersion = head.manifest.cliVersion ?? "unknown";
5888
+ if (head.manifest.schema === 2) {
5889
+ log(
5890
+ `WARNING: checkpoint '${opts.checkpointRef}' was captured before checkpoint versioning landed.
5891
+ Its base-image layers may be older than your current base image. If the box is missing
5892
+ expected updates, remove the checkpoint with \`agentbox checkpoint rm ${opts.checkpointRef}\` and recreate it.`
5893
+ );
5894
+ } else {
5895
+ const prepared = readPreparedDockerState();
5896
+ const currentFingerprint = prepared?.base?.contextSha256 ?? (await computeDockerContextFingerprint())?.contextSha256;
5897
+ if (ckptFingerprint && currentFingerprint && ckptFingerprint !== currentFingerprint) {
5898
+ log(
5899
+ `WARNING: checkpoint '${opts.checkpointRef}' was captured against an older base image.
5900
+ captured: cli ${ckptCliVersion}, fingerprint ${ckptFingerprint.slice(0, 12)}
5901
+ current : cli ${prepared?.base?.cliVersion ?? "unknown"}, fingerprint ${currentFingerprint.slice(0, 12)}
5902
+ The restored box will keep the old base layers and will NOT include base-image updates.
5903
+ To pick up updates: \`agentbox checkpoint rm ${opts.checkpointRef}\` and recreate from a fresh box.`
5904
+ );
5905
+ }
5906
+ }
5570
5907
  }
5571
5908
  const imageRef = checkpointImage ?? opts.image ?? DEFAULT_BOX_IMAGE;
5572
5909
  const ensureRef = checkpointImage ? opts.image ?? DEFAULT_BOX_IMAGE : imageRef;
@@ -5716,6 +6053,8 @@ async function createBox(opts) {
5716
6053
  if (opencodeEnsured.synced) log(`synced ${opencodeSpec.volume} from ~/.config + ~/.local/share opencode`);
5717
6054
  else if (opencodeEnsured.created) log(`created empty volume ${opencodeSpec.volume} (no host opencode)`);
5718
6055
  else log(`reusing volume ${opencodeSpec.volume}`);
6056
+ const opencodePlugin = await seedOpencodePlugin(opencodeSpec.volume, ensureRef);
6057
+ if (opencodePlugin.seeded) log(`seeded agentbox-state plugin into ${opencodeSpec.volume}`);
5719
6058
  opencodeMounts = buildOpencodeMounts(opencodeSpec, process.env);
5720
6059
  opencodeConfigVolume = opencodeSpec.volume;
5721
6060
  }
@@ -5771,10 +6110,15 @@ async function createBox(opts) {
5771
6110
  }
5772
6111
  }
5773
6112
  const relayEnv = relayUp ? {
5774
- // host.docker.internal resolves to the host (where the relay node
5775
- // process is running). The matching `--add-host` is set in runBox.
5776
- AGENTBOX_RELAY_URL: `http://host.docker.internal:8787`,
5777
- AGENTBOX_RELAY_TOKEN: relayToken
6113
+ // The in-box ctl client always talks to its own in-box relay/forwarder
6114
+ // on AGENTBOX_BOX_RELAY_PORT (default 8788). For docker boxes the
6115
+ // forwarder transparently proxies to the host relay at
6116
+ // host.docker.internal:8787 (the matching `--add-host` is set in
6117
+ // runBox). This keeps :8787 inside the box free for a nested
6118
+ // agentbox to claim its own host relay.
6119
+ AGENTBOX_RELAY_URL: `http://127.0.0.1:8788`,
6120
+ AGENTBOX_RELAY_TOKEN: relayToken,
6121
+ AGENTBOX_HOST_RELAY_URL: `http://host.docker.internal:8787`
5778
6122
  } : {};
5779
6123
  const vncEnabled = opts.vnc?.enabled !== false;
5780
6124
  const vncPassword = vncEnabled ? generateVncPassword() : void 0;
@@ -5839,7 +6183,12 @@ async function createBox(opts) {
5839
6183
  if (!checkpointImage) {
5840
6184
  if (repoCarryOvers.length > 0) {
5841
6185
  try {
5842
- await seedWorkspace({ container: containerName, repos: repoCarryOvers, onLog: log });
6186
+ await seedWorkspace({
6187
+ container: containerName,
6188
+ repos: repoCarryOvers,
6189
+ fromBranch: opts.fromBranch,
6190
+ onLog: log
6191
+ });
5843
6192
  log("seeded /workspace from in-container git worktree(s)");
5844
6193
  } catch (err) {
5845
6194
  log(
@@ -5878,7 +6227,7 @@ async function createBox(opts) {
5878
6227
  }
5879
6228
  if (opts.withPlaywright) {
5880
6229
  log("installing @playwright/cli@latest (--with-playwright)");
5881
- const result = await execa14(
6230
+ const result = await execa13(
5882
6231
  "docker",
5883
6232
  [
5884
6233
  "exec",
@@ -5923,6 +6272,20 @@ async function createBox(opts) {
5923
6272
  log(`copied ${String(copied)}/${String(opts.envFilesToImport.length)} selected env/config file(s)`);
5924
6273
  }
5925
6274
  }
6275
+ let carrySummary;
6276
+ if (opts.carry && opts.carry.length > 0) {
6277
+ log(`carry: copying ${String(opts.carry.length)} host path(s) into the box`);
6278
+ const result = await copyCarryPathsToBox({
6279
+ container: containerName,
6280
+ entries: opts.carry,
6281
+ onLog: log
6282
+ });
6283
+ log(`carry: copied ${String(result.copied)}/${String(opts.carry.length)} entry/entries`);
6284
+ for (const err of result.errors) log(`carry: ${err}`);
6285
+ if (result.applied.length > 0) {
6286
+ carrySummary = { count: result.applied.length, entries: result.applied };
6287
+ }
6288
+ }
5926
6289
  let vncHostPort = null;
5927
6290
  if (vncEnabled) {
5928
6291
  const vnc = await launchVncDaemon(containerName);
@@ -5939,7 +6302,9 @@ async function createBox(opts) {
5939
6302
  }
5940
6303
  let portlessAliasName;
5941
6304
  let portlessUrl;
5942
- if (opts.portless === true && webHostPort) {
6305
+ let portlessVncAliasName;
6306
+ let portlessVncUrl;
6307
+ if (opts.portless === true && (webHostPort || vncEnabled && vncHostPort)) {
5943
6308
  try {
5944
6309
  const engine = await detectEngine();
5945
6310
  if (engine === "orbstack") {
@@ -5948,15 +6313,29 @@ async function createBox(opts) {
5948
6313
  const portless = await detectPortless();
5949
6314
  if (!portless.installed) {
5950
6315
  log("portless not installed \u2014 run `npm install -g portless` for a <name>.localhost URL");
5951
- } else if (await portlessAlias(name, webHostPort)) {
5952
- portlessAliasName = name;
5953
- portlessUrl = await portlessGetUrl(name);
5954
- log(`portless alias ${portlessUrl} -> 127.0.0.1:${String(webHostPort)}`);
5955
- if (!portless.proxyRunning) {
6316
+ } else {
6317
+ if (webHostPort) {
6318
+ if (await portlessAlias(name, webHostPort)) {
6319
+ portlessAliasName = name;
6320
+ portlessUrl = await portlessGetUrl(name);
6321
+ log(`portless alias ${portlessUrl} -> 127.0.0.1:${String(webHostPort)}`);
6322
+ } else {
6323
+ log("portless alias failed (best-effort) \u2014 box still reachable on the loopback URL");
6324
+ }
6325
+ }
6326
+ if (vncEnabled && vncHostPort) {
6327
+ const vncAlias = `vnc-${name}`;
6328
+ if (await portlessAlias(vncAlias, vncHostPort)) {
6329
+ portlessVncAliasName = vncAlias;
6330
+ portlessVncUrl = await portlessGetUrl(vncAlias);
6331
+ log(`portless alias ${portlessVncUrl} -> 127.0.0.1:${String(vncHostPort)}`);
6332
+ } else {
6333
+ log("portless vnc alias failed (best-effort) \u2014 VNC still reachable on the loopback URL");
6334
+ }
6335
+ }
6336
+ if (!portless.proxyRunning && (portlessAliasName || portlessVncAliasName)) {
5956
6337
  log(`portless proxy not running \u2014 start it with \`${portlessStartHint()}\``);
5957
6338
  }
5958
- } else {
5959
- log("portless alias failed (best-effort) \u2014 box still reachable on the loopback URL");
5960
6339
  }
5961
6340
  }
5962
6341
  } catch (err) {
@@ -5980,6 +6359,7 @@ async function createBox(opts) {
5980
6359
  gitWorktrees: gitWorktreeRecords.length > 0 ? gitWorktreeRecords : void 0,
5981
6360
  withPlaywright: opts.withPlaywright ? true : void 0,
5982
6361
  withEnv: opts.withEnv ? true : void 0,
6362
+ carry: carrySummary,
5983
6363
  vncEnabled: vncEnabled ? true : void 0,
5984
6364
  vncContainerPort: vncEnabled ? VNC_CONTAINER_PORT : void 0,
5985
6365
  vncHostPort: vncHostPort ?? void 0,
@@ -5988,6 +6368,8 @@ async function createBox(opts) {
5988
6368
  webHostPort: webHostPort ?? void 0,
5989
6369
  portlessAlias: portlessAliasName,
5990
6370
  portlessUrl,
6371
+ portlessVncAlias: portlessVncAliasName,
6372
+ portlessVncUrl,
5991
6373
  dockerVolume,
5992
6374
  dockerCacheShared: dockerCacheShared || void 0,
5993
6375
  projectRoot: opts.projectRoot,
@@ -6046,7 +6428,7 @@ function parseShellSessionList(stdout) {
6046
6428
  return out;
6047
6429
  }
6048
6430
  async function listShellSessions(container, user) {
6049
- const res = await execa15(
6431
+ const res = await execa14(
6050
6432
  "docker",
6051
6433
  [
6052
6434
  "exec",
@@ -6070,7 +6452,7 @@ async function startShellSession(opts) {
6070
6452
  const login = opts.login !== false;
6071
6453
  const term = process.env["TERM"] ?? "xterm-256color";
6072
6454
  const cmd = login ? "bash -l" : "bash";
6073
- const result = await execa15(
6455
+ const result = await execa14(
6074
6456
  "docker",
6075
6457
  [
6076
6458
  "exec",
@@ -6119,7 +6501,7 @@ function buildShellSessionAttachArgv(container, sessionName, user) {
6119
6501
  }
6120
6502
  async function shellSessionInfo(container, sessionName, user) {
6121
6503
  const name = sessionName ?? DEFAULT_SHELL_SESSION;
6122
- const has = await execa15(
6504
+ const has = await execa14(
6123
6505
  "docker",
6124
6506
  ["exec", "--user", user ?? CONTAINER_USER, container, "tmux", "has-session", "-t", name],
6125
6507
  { reject: false }
@@ -6127,7 +6509,7 @@ async function shellSessionInfo(container, sessionName, user) {
6127
6509
  return { running: has.exitCode === 0, sessionName: name };
6128
6510
  }
6129
6511
  async function killShellSession(container, sessionName, user) {
6130
- const res = await execa15(
6512
+ const res = await execa14(
6131
6513
  "docker",
6132
6514
  [
6133
6515
  "exec",
@@ -6149,7 +6531,7 @@ async function getBoxEndpoints(record, engine, persisted) {
6149
6531
  const endpoints = [];
6150
6532
  if (record.vncEnabled && record.vncPassword) {
6151
6533
  const vncUrls = buildVncUrls(record, engine);
6152
- const url = vncUrls.orbUrl ?? vncUrls.loopbackUrl;
6534
+ const url = engine === "orbstack" ? vncUrls.orbUrl ?? vncUrls.loopbackUrl : vncUrls.portlessUrl ?? vncUrls.loopbackUrl;
6153
6535
  endpoints.push({
6154
6536
  kind: "vnc",
6155
6537
  name: "vnc",
@@ -6216,18 +6598,30 @@ async function listBoxes() {
6216
6598
  const portlessWebUrl = b.portlessAlias !== void 0 ? b.portlessUrl ?? `https://${b.portlessAlias}.localhost` : void 0;
6217
6599
  const cachedWebUrl = webPort > 0 ? b.cloud?.previewUrls?.[webPort] : void 0;
6218
6600
  const webUrl = portlessWebUrl ?? cachedWebUrl;
6601
+ const portlessVncBase = b.portlessVncAlias !== void 0 ? b.portlessVncUrl ?? `https://${b.portlessVncAlias}.localhost` : void 0;
6602
+ const vncUrl = portlessVncBase && b.vncPassword ? `${portlessVncBase}/vnc.html?autoconnect=1&password=${encodeURIComponent(b.vncPassword)}` : void 0;
6603
+ const cloudEndpoints = [];
6604
+ if (webUrl) {
6605
+ cloudEndpoints.push({
6606
+ kind: "web",
6607
+ name: "web",
6608
+ containerPort: webPort,
6609
+ url: webUrl,
6610
+ reachable: true
6611
+ });
6612
+ }
6613
+ if (b.vncEnabled && b.vncPassword) {
6614
+ cloudEndpoints.push({
6615
+ kind: "vnc",
6616
+ name: "vnc",
6617
+ containerPort: b.vncContainerPort ?? 6080,
6618
+ ...vncUrl ? { url: vncUrl, reachable: true } : { reachable: false }
6619
+ });
6620
+ }
6219
6621
  const endpoints2 = {
6220
6622
  domain: webUrl ? safeHost(webUrl) : "",
6221
6623
  domainIsOrb: false,
6222
- endpoints: webUrl ? [
6223
- {
6224
- kind: "web",
6225
- name: "web",
6226
- containerPort: webPort,
6227
- url: webUrl,
6228
- reachable: true
6229
- }
6230
- ] : []
6624
+ endpoints: cloudEndpoints
6231
6625
  };
6232
6626
  return {
6233
6627
  ...b,
@@ -6351,19 +6745,31 @@ async function startBox(idOrName) {
6351
6745
  box.webHostPort = freshWebPort;
6352
6746
  await recordBox(box);
6353
6747
  }
6354
- if (box.portlessAlias && box.webHostPort) {
6355
- try {
6356
- const portless = await detectPortless();
6357
- if (portless.installed) {
6748
+ }
6749
+ if (box.portlessAlias && box.webHostPort || box.portlessVncAlias && box.vncHostPort) {
6750
+ try {
6751
+ const portless = await detectPortless();
6752
+ if (portless.installed) {
6753
+ let dirty = false;
6754
+ if (box.portlessAlias && box.webHostPort) {
6358
6755
  await portlessAlias(box.portlessAlias, box.webHostPort);
6359
6756
  const url = await portlessGetUrl(box.portlessAlias);
6360
6757
  if (url !== box.portlessUrl) {
6361
6758
  box.portlessUrl = url;
6362
- await recordBox(box);
6759
+ dirty = true;
6363
6760
  }
6364
6761
  }
6365
- } catch {
6762
+ if (box.portlessVncAlias && box.vncHostPort) {
6763
+ await portlessAlias(box.portlessVncAlias, box.vncHostPort);
6764
+ const url = await portlessGetUrl(box.portlessVncAlias);
6765
+ if (url !== box.portlessVncUrl) {
6766
+ box.portlessVncUrl = url;
6767
+ dirty = true;
6768
+ }
6769
+ }
6770
+ if (dirty) await recordBox(box);
6366
6771
  }
6772
+ } catch {
6367
6773
  }
6368
6774
  }
6369
6775
  if (box.relayToken) {
@@ -6395,7 +6801,7 @@ async function getBoxHostPaths(idOrName) {
6395
6801
  }
6396
6802
  async function dirSizeBytes(path) {
6397
6803
  try {
6398
- const result = await execa16("du", ["-sk", path], { reject: false });
6804
+ const result = await execa15("du", ["-sk", path], { reject: false });
6399
6805
  if (result.exitCode !== 0) return null;
6400
6806
  const sizeKb = Number.parseInt((result.stdout ?? "").split(/\s+/)[0] ?? "", 10);
6401
6807
  if (Number.isNaN(sizeKb)) return null;
@@ -6463,6 +6869,12 @@ async function destroyBox(idOrName, opts = {}) {
6463
6869
  } catch {
6464
6870
  }
6465
6871
  }
6872
+ if (box.portlessVncAlias) {
6873
+ try {
6874
+ await portlessUnalias(box.portlessVncAlias);
6875
+ } catch {
6876
+ }
6877
+ }
6466
6878
  const ownsWorktrees = !box.checkpointImage;
6467
6879
  if (ownsWorktrees) {
6468
6880
  for (const w of box.gitWorktrees ?? []) {
@@ -6537,7 +6949,7 @@ async function listBoxDirs() {
6537
6949
  }
6538
6950
  }
6539
6951
  async function listCheckpointImageTags() {
6540
- const r = await execa16(
6952
+ const r = await execa15(
6541
6953
  "docker",
6542
6954
  ["image", "ls", "--format", "{{.Repository}}:{{.Tag}}", `${CHECKPOINT_IMAGE_PREFIX}*`],
6543
6955
  { reject: false }
@@ -6645,7 +7057,7 @@ async function pruneBoxes(opts = {}) {
6645
7057
  } catch {
6646
7058
  }
6647
7059
  try {
6648
- await execa16("docker", ["image", "rm", RELAY_IMAGE_REF], { reject: false });
7060
+ await execa15("docker", ["image", "rm", RELAY_IMAGE_REF], { reject: false });
6649
7061
  } catch {
6650
7062
  }
6651
7063
  try {
@@ -6707,7 +7119,7 @@ function splitPair(raw) {
6707
7119
  return [parts[0].trim(), parts[1].trim()];
6708
7120
  }
6709
7121
  async function duBytes(path) {
6710
- const result = await execa17("du", ["-sk", path], { reject: false });
7122
+ const result = await execa16("du", ["-sk", path], { reject: false });
6711
7123
  if (result.exitCode !== 0) return null;
6712
7124
  const kb = Number.parseInt((result.stdout ?? "").split(/\s+/)[0] ?? "", 10);
6713
7125
  return Number.isNaN(kb) ? null : kb * 1024;
@@ -6720,7 +7132,7 @@ async function volumeSizeBytes(name) {
6720
7132
  const sz = await duBytes(live);
6721
7133
  if (sz !== null) return sz;
6722
7134
  }
6723
- const df = await execa17(
7135
+ const df = await execa16(
6724
7136
  "docker",
6725
7137
  ["system", "df", "-v", "--format", "{{json .Volumes}}"],
6726
7138
  { reject: false }
@@ -6741,7 +7153,7 @@ async function volumeSizeBytes(name) {
6741
7153
  return null;
6742
7154
  }
6743
7155
  async function imageBytes(tag) {
6744
- const r = await execa17("docker", ["image", "inspect", tag, "--format", "{{.Size}}"], {
7156
+ const r = await execa16("docker", ["image", "inspect", tag, "--format", "{{.Size}}"], {
6745
7157
  reject: false
6746
7158
  });
6747
7159
  if (r.exitCode !== 0) return null;
@@ -6752,7 +7164,7 @@ async function projectCheckpointImageBytes(projectRoot, name) {
6752
7164
  return imageBytes(checkpointImageTag(projectRoot, name));
6753
7165
  }
6754
7166
  async function allCheckpointImagesBytes() {
6755
- const r = await execa17(
7167
+ const r = await execa16(
6756
7168
  "docker",
6757
7169
  [
6758
7170
  "image",
@@ -6804,7 +7216,7 @@ function reconcileLimits(persisted, dockerJson) {
6804
7216
  };
6805
7217
  }
6806
7218
  async function containerWritableBytes(container) {
6807
- const r = await execa17(
7219
+ const r = await execa16(
6808
7220
  "docker",
6809
7221
  ["ps", "-a", "--filter", `name=^${container}$`, "--format", "{{.Size}}", "--size"],
6810
7222
  { reject: false }
@@ -6851,7 +7263,7 @@ async function boxResourceStats(record) {
6851
7263
  if (await inspectContainerStatus(record.container) !== "running") {
6852
7264
  return base;
6853
7265
  }
6854
- const proc = await execa17(
7266
+ const proc = await execa16(
6855
7267
  "docker",
6856
7268
  ["stats", "--no-stream", "--format", "{{json .}}", record.container],
6857
7269
  { reject: false }
@@ -6886,6 +7298,125 @@ async function boxResourceStats(record) {
6886
7298
  blockWriteBytes: blkPair ? parseDockerSize(blkPair[1]) : null
6887
7299
  };
6888
7300
  }
7301
+ function posixDirname(p) {
7302
+ return posix.dirname(p) || "/";
7303
+ }
7304
+ function asText(s) {
7305
+ if (s === void 0) return "";
7306
+ if (typeof s === "string") return s;
7307
+ return Buffer.from(s).toString("utf8");
7308
+ }
7309
+ async function uploadToBox(box, hostSrc, boxDst) {
7310
+ const srcAbs = resolve4(hostSrc);
7311
+ if (!existsSync3(srcAbs)) throw new Error(`source not found: ${hostSrc}`);
7312
+ const srcBasename = basename32(srcAbs);
7313
+ const srcParent = dirname22(srcAbs);
7314
+ let boxParent;
7315
+ let finalName;
7316
+ if (boxDst.endsWith("/")) {
7317
+ boxParent = boxDst.replace(/\/+$/, "") || "/";
7318
+ finalName = srcBasename;
7319
+ } else {
7320
+ const isDir2 = await execa17(
7321
+ "docker",
7322
+ ["exec", box.container, "test", "-d", boxDst],
7323
+ { reject: false }
7324
+ );
7325
+ if (isDir2.exitCode === 0) {
7326
+ boxParent = boxDst.replace(/\/+$/, "") || "/";
7327
+ finalName = srcBasename;
7328
+ } else {
7329
+ boxParent = posixDirname(boxDst);
7330
+ finalName = posix.basename(boxDst);
7331
+ }
7332
+ }
7333
+ const finalPath = boxParent === "/" ? `/${finalName}` : `${boxParent}/${finalName}`;
7334
+ const mk = await execa17(
7335
+ "docker",
7336
+ ["exec", "--user", "root", box.container, "mkdir", "-p", boxParent],
7337
+ { reject: false }
7338
+ );
7339
+ if (mk.exitCode !== 0) {
7340
+ throw new Error(`mkdir -p ${boxParent} in box failed: ${asText(mk.stderr).slice(0, 300)}`);
7341
+ }
7342
+ const packed = await execa17("tar", ["-C", srcParent, "-cf", "-", srcBasename], {
7343
+ encoding: "buffer",
7344
+ reject: false,
7345
+ env: { ...process.env, COPYFILE_DISABLE: "1" }
7346
+ });
7347
+ if (packed.exitCode !== 0) {
7348
+ throw new Error(`tar pack failed: ${asText(packed.stderr).slice(0, 300)}`);
7349
+ }
7350
+ const extract = await execa17(
7351
+ "docker",
7352
+ ["exec", "-i", "--user", "root", box.container, "tar", "-xf", "-", "-C", boxParent],
7353
+ { input: packed.stdout, reject: false }
7354
+ );
7355
+ if (extract.exitCode !== 0) {
7356
+ throw new Error(`tar extract in box failed: ${asText(extract.stderr).slice(0, 300)}`);
7357
+ }
7358
+ if (finalName !== srcBasename) {
7359
+ const initial = boxParent === "/" ? `/${srcBasename}` : `${boxParent}/${srcBasename}`;
7360
+ const mv = await execa17(
7361
+ "docker",
7362
+ ["exec", "--user", "root", box.container, "mv", initial, finalPath],
7363
+ { reject: false }
7364
+ );
7365
+ if (mv.exitCode !== 0) {
7366
+ throw new Error(
7367
+ `rename ${initial} -> ${finalPath} in box failed: ${asText(mv.stderr).slice(0, 300)}`
7368
+ );
7369
+ }
7370
+ }
7371
+ const chown = await execa17(
7372
+ "docker",
7373
+ ["exec", "--user", "root", box.container, "chown", "-R", "1000:1000", finalPath],
7374
+ { reject: false }
7375
+ );
7376
+ if (chown.exitCode !== 0) {
7377
+ return {
7378
+ finalPath,
7379
+ warn: `chown ${finalPath} to vscode (uid 1000) failed; ownership inside the box may be root.`
7380
+ };
7381
+ }
7382
+ return { finalPath };
7383
+ }
7384
+ async function downloadFromBox(box, boxSrc, hostDst) {
7385
+ const srcBasename = posix.basename(boxSrc);
7386
+ const srcParent = posixDirname(boxSrc);
7387
+ const dstAbs = resolve4(hostDst);
7388
+ let hostParent;
7389
+ let finalName;
7390
+ const dstExists = existsSync3(dstAbs);
7391
+ if (hostDst.endsWith("/") || dstExists && statSync(dstAbs).isDirectory()) {
7392
+ hostParent = dstAbs;
7393
+ finalName = srcBasename;
7394
+ } else {
7395
+ hostParent = dirname22(dstAbs);
7396
+ finalName = basename32(dstAbs);
7397
+ }
7398
+ mkdirSync(hostParent, { recursive: true });
7399
+ const finalPath = posix.join(hostParent, finalName);
7400
+ const packed = await execa17(
7401
+ "docker",
7402
+ ["exec", box.container, "tar", "-C", srcParent, "-cf", "-", srcBasename],
7403
+ { encoding: "buffer", reject: false }
7404
+ );
7405
+ if (packed.exitCode !== 0) {
7406
+ throw new Error(`tar pack in box failed: ${asText(packed.stderr).slice(0, 300)}`);
7407
+ }
7408
+ const extract = await execa17("tar", ["-xf", "-", "-C", hostParent], {
7409
+ input: packed.stdout,
7410
+ reject: false
7411
+ });
7412
+ if (extract.exitCode !== 0) {
7413
+ throw new Error(`tar extract on host failed: ${asText(extract.stderr).slice(0, 300)}`);
7414
+ }
7415
+ if (finalName !== srcBasename) {
7416
+ renameSync(posix.join(hostParent, srcBasename), finalPath);
7417
+ }
7418
+ return { finalPath };
7419
+ }
6889
7420
  var dockerProvider = {
6890
7421
  name: "docker",
6891
7422
  async create(req) {
@@ -6895,6 +7426,7 @@ var dockerProvider = {
6895
7426
  name: req.name,
6896
7427
  useSnapshot: po.useSnapshot ?? false,
6897
7428
  checkpointRef: req.checkpointRef,
7429
+ fromBranch: req.fromBranch,
6898
7430
  image: req.image,
6899
7431
  onLog: req.onLog,
6900
7432
  claudeConfig: po.claudeConfig,
@@ -6904,6 +7436,7 @@ var dockerProvider = {
6904
7436
  withPlaywright: req.withPlaywright,
6905
7437
  withEnv: req.withEnv,
6906
7438
  envFilesToImport: req.envFilesToImport,
7439
+ carry: req.carry,
6907
7440
  vnc: req.vnc,
6908
7441
  docker: po.sharedCache !== void 0 ? { sharedCache: po.sharedCache } : void 0,
6909
7442
  portless: po.portless,
@@ -6954,6 +7487,14 @@ var dockerProvider = {
6954
7487
  const r = await execInBox(box.container, argv, opts?.user ? { user: opts.user } : {});
6955
7488
  return { exitCode: r.exitCode, stdout: r.stdout, stderr: r.stderr };
6956
7489
  },
7490
+ async uploadPath(box, hostSrc, boxDst) {
7491
+ const r = await uploadToBox(box, hostSrc, boxDst);
7492
+ return { finalPath: r.finalPath };
7493
+ },
7494
+ async downloadPath(box, boxSrc, hostDst) {
7495
+ const r = await downloadFromBox(box, boxSrc, hostDst);
7496
+ return { finalPath: r.finalPath };
7497
+ },
6957
7498
  async resolveUrl(box, opts) {
6958
7499
  if (box.webContainerPort === void 0) {
6959
7500
  throw new Error(
@@ -6976,13 +7517,33 @@ var dockerProvider = {
6976
7517
  },
6977
7518
  async prepare(opts) {
6978
7519
  const ref = DEFAULT_BOX_IMAGE;
6979
- if (!opts.force && await imageExists(ref)) {
6980
- opts.onLog?.(`docker image ${ref} already built \u2014 skipping (use --force to rebuild)`);
6981
- return {};
7520
+ const fingerprint = await computeDockerContextFingerprint();
7521
+ const prepared = readPreparedDockerState();
7522
+ if (!opts.force) {
7523
+ const exists = await imageExists(ref);
7524
+ if (exists && fingerprint && preparedMatches(prepared, fingerprint.contextSha256)) {
7525
+ opts.onLog?.(
7526
+ `docker image ${ref} up to date (fingerprint ${fingerprint.contextSha256.slice(0, 12)}) \u2014 skipping (use --force to rebuild)`
7527
+ );
7528
+ return {};
7529
+ }
7530
+ if (exists && !fingerprint) {
7531
+ opts.onLog?.(
7532
+ `docker image ${ref} present but build context could not be fingerprinted \u2014 skipping (use --force to rebuild)`
7533
+ );
7534
+ return {};
7535
+ }
6982
7536
  }
6983
7537
  opts.onLog?.(`building docker image ${ref}\u2026`);
6984
7538
  await buildImage({ ref, onProgress: opts.onLog });
6985
- opts.onLog?.(`docker image ${ref} built`);
7539
+ if (fingerprint) {
7540
+ writePreparedDockerState({ imageRef: ref, contextSha256: fingerprint.contextSha256 });
7541
+ opts.onLog?.(
7542
+ `docker image ${ref} built; recorded fingerprint ${fingerprint.contextSha256.slice(0, 12)}`
7543
+ );
7544
+ } else {
7545
+ opts.onLog?.(`docker image ${ref} built (fingerprint unavailable, prepared state not written)`);
7546
+ }
6986
7547
  return {};
6987
7548
  }
6988
7549
  };
@@ -7028,7 +7589,7 @@ function emptyResult(warnings = []) {
7028
7589
  }, warnings };
7029
7590
  }
7030
7591
  async function tarballFromDir(stageDir, agent) {
7031
- const tarballPath = join14(tmpdir3(), `agentbox-${agent}-${basename32(stageDir)}.tar.gz`);
7592
+ const tarballPath = join14(tmpdir3(), `agentbox-${agent}-${basename4(stageDir)}.tar.gz`);
7032
7593
  await execa18("tar", ["-czf", tarballPath, "-C", stageDir, "."], {
7033
7594
  env: { ...process.env, COPYFILE_DISABLE: "1" }
7034
7595
  });
@@ -7291,6 +7852,12 @@ async function stageOpencodeCredentialsForUpload(opts = {}) {
7291
7852
  if (!await pathExists7(hostAuth)) return emptyResult();
7292
7853
  return stageSingleFileTarball("opencode-creds", hostAuth, "auth.json");
7293
7854
  }
7855
+ async function stageOpencodeStateForUpload(opts = {}) {
7856
+ const hostHome = opts.hostHome ?? homedir11();
7857
+ const hostModel = join14(hostHome, ".local", "state", "opencode", "model.json");
7858
+ if (!await pathExists7(hostModel)) return emptyResult();
7859
+ return stageSingleFileTarball("opencode-state", hostModel, "model.json");
7860
+ }
7294
7861
  function browserSessionActive(stdout, exitCode) {
7295
7862
  return exitCode === 0 && !/no active sessions/i.test(stdout);
7296
7863
  }
@@ -7330,25 +7897,24 @@ export {
7330
7897
  listProjectsConfigured,
7331
7898
  pruneOrphanProjectConfigs,
7332
7899
  bumpProjectGcCounter,
7900
+ BOX_STATUS_EVENT,
7333
7901
  renderStatusTable,
7334
7902
  renderTaskTable,
7335
7903
  renderPortsTable,
7336
- STATE_DIR2 as STATE_DIR,
7337
- STATE_FILE,
7338
- readState,
7339
- recordBox,
7340
- removeBoxRecord,
7341
- findBox,
7342
- allocateProjectIndex,
7343
- autoPickProjectBox,
7344
- resolveBoxRef,
7345
- detectGitRepos,
7346
- pickFreshBranch,
7347
- GitWorktreeError,
7904
+ loadCarrySection,
7348
7905
  DEFAULT_RELAY_PORT,
7349
7906
  RELAY_CONTAINER_NAME,
7350
7907
  RELAY_NETWORK_NAME,
7351
7908
  RELAY_IMAGE_REF,
7909
+ hashRpcParams,
7910
+ GH_PR_OPS,
7911
+ loadQueueConfig,
7912
+ writeJob,
7913
+ readJob,
7914
+ deleteJob,
7915
+ loadQueue,
7916
+ defaultCountRunningBoxes,
7917
+ queueLogPath,
7352
7918
  resolveAgentLauncher,
7353
7919
  BoxNotFoundError,
7354
7920
  AmbiguousBoxError,
@@ -7390,6 +7956,7 @@ export {
7390
7956
  buildDashboardAttachArgv,
7391
7957
  waitForTmuxPaneContent,
7392
7958
  buildTmuxSessionArgs,
7959
+ buildTmuxConfigShellSnippet,
7393
7960
  buildShellArgv,
7394
7961
  buildClaudeLoginRunArgv,
7395
7962
  runInteractiveClaudeLogin,
@@ -7422,6 +7989,7 @@ export {
7422
7989
  DEFAULT_OPENCODE_SESSION,
7423
7990
  resolveOpencodeVolume,
7424
7991
  ensureOpencodeVolume,
7992
+ seedOpencodePlugin,
7425
7993
  OPENCODE_FORWARDED_ENV_KEYS,
7426
7994
  buildOpencodeMounts,
7427
7995
  OpencodeSessionError,
@@ -7459,10 +8027,6 @@ export {
7459
8027
  installPortless,
7460
8028
  startPortlessProxy,
7461
8029
  resolvePortlessHostStateDir,
7462
- DEFAULT_BOX_IMAGE,
7463
- imageExists,
7464
- imageInfo,
7465
- ensureImage,
7466
8030
  EXCLUDE_DIRS,
7467
8031
  SNAPSHOTS_ROOT,
7468
8032
  snapshotPathFor,
@@ -7485,6 +8049,7 @@ export {
7485
8049
  forgetBoxFromRelay,
7486
8050
  setRelayNotice,
7487
8051
  clearRelayNotice,
8052
+ mintHostInitiatedToken,
7488
8053
  rehydrateRelayRegistry,
7489
8054
  IDE_FLAVORS,
7490
8055
  ideProfile,
@@ -7533,6 +8098,8 @@ export {
7533
8098
  allCheckpointImagesBytes,
7534
8099
  agentboxHomeBytes,
7535
8100
  boxResourceStats,
8101
+ uploadToBox,
8102
+ downloadFromBox,
7536
8103
  dockerProvider,
7537
8104
  stageClaudeStaticForUpload,
7538
8105
  stageClaudeCredentialsForUpload,
@@ -7540,7 +8107,8 @@ export {
7540
8107
  stageCodexCredentialsForUpload,
7541
8108
  stageOpencodeStaticForUpload,
7542
8109
  stageOpencodeCredentialsForUpload,
8110
+ stageOpencodeStateForUpload,
7543
8111
  browserSessionActive,
7544
8112
  ensureBoxBrowser
7545
8113
  };
7546
- //# sourceMappingURL=chunk-NAVL4R34.js.map
8114
+ //# sourceMappingURL=chunk-6OZDFNBF.js.map