@madarco/agentbox 0.7.0 → 0.9.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 (77) hide show
  1. package/dist/_cloud-attach-ZXBCNWJX.js +13 -0
  2. package/dist/{chunk-NW5NYTQM.js → chunk-BXQMIEHC.js} +459 -110
  3. package/dist/chunk-BXQMIEHC.js.map +1 -0
  4. package/dist/{chunk-UK72UQ5U.js → chunk-G3H2L3O2.js} +55 -4
  5. package/dist/chunk-G3H2L3O2.js.map +1 -0
  6. package/dist/{chunk-7KOEFGN2.js → chunk-GU5LW4B5.js} +385 -31
  7. package/dist/chunk-GU5LW4B5.js.map +1 -0
  8. package/dist/chunk-KL36BRN4.js +455 -0
  9. package/dist/chunk-KL36BRN4.js.map +1 -0
  10. package/dist/{chunk-V5KZGB5V.js → chunk-LEV3KICD.js} +18 -2
  11. package/dist/chunk-LEV3KICD.js.map +1 -0
  12. package/dist/chunk-MTVI44DW.js +662 -0
  13. package/dist/chunk-MTVI44DW.js.map +1 -0
  14. package/dist/{chunk-NAVL4R34.js → chunk-NCJP5MTN.js} +1281 -556
  15. package/dist/chunk-NCJP5MTN.js.map +1 -0
  16. package/dist/{cloud-poller-ZIWSADJB-JXFRJUEM.js → cloud-poller-SUNA6ZQC-2RG5WPRN.js} +2 -2
  17. package/dist/{dist-ETCFRVPA.js → dist-32EZBYG4.js} +50 -20
  18. package/dist/{dist-R67WMLCF.js → dist-CX5CGVEB.js} +120 -10
  19. package/dist/dist-CX5CGVEB.js.map +1 -0
  20. package/dist/{dist-QZGJIBT5.js → dist-GDHP34ZK.js} +141 -75
  21. package/dist/dist-GDHP34ZK.js.map +1 -0
  22. package/dist/dist-XML54CNB.js +849 -0
  23. package/dist/dist-XML54CNB.js.map +1 -0
  24. package/dist/index.js +3881 -867
  25. package/dist/index.js.map +1 -1
  26. package/dist/prepared-state-CL4CWXQA-H5THETIM.js +18 -0
  27. package/dist/prepared-state-CL4CWXQA-H5THETIM.js.map +1 -0
  28. package/package.json +7 -5
  29. package/runtime/daytona/custom-system-CLAUDE.md +39 -0
  30. package/runtime/docker/Dockerfile.box +22 -0
  31. package/runtime/docker/apps/cli/share/agentbox-setup/SKILL.md +1 -1
  32. package/runtime/docker/packages/ctl/dist/bin.cjs +1214 -98
  33. package/runtime/docker/packages/sandbox-docker/scripts/agentbox-codex-hooks.json +66 -35
  34. package/runtime/docker/packages/sandbox-docker/scripts/agentbox-vnc-start +15 -1
  35. package/runtime/docker/packages/sandbox-docker/scripts/claude-managed-settings.json +62 -1
  36. package/runtime/docker/packages/sandbox-docker/scripts/custom-system-CLAUDE.md +15 -4
  37. package/runtime/docker/packages/sandbox-docker/scripts/gh-shim +263 -0
  38. package/runtime/docker/packages/sandbox-docker/scripts/git-shim +131 -0
  39. package/runtime/docker/packages/sandbox-docker/scripts/opencode-agentbox-plugin.js +76 -0
  40. package/runtime/hetzner/agentbox-codex-hooks.json +66 -35
  41. package/runtime/hetzner/agentbox-setup-skill.md +1 -1
  42. package/runtime/hetzner/agentbox-vnc-start +15 -1
  43. package/runtime/hetzner/claude-managed-settings.json +62 -1
  44. package/runtime/hetzner/ctl.cjs +1214 -98
  45. package/runtime/hetzner/custom-system-CLAUDE.md +26 -14
  46. package/runtime/hetzner/gh-shim +263 -0
  47. package/runtime/hetzner/git-shim +131 -0
  48. package/runtime/hetzner/opencode-agentbox-plugin.js +76 -0
  49. package/runtime/hetzner/scripts/install-box.sh +11 -2
  50. package/runtime/relay/bin.cjs +1146 -63
  51. package/runtime/vercel/agentbox-checkpoint-cleanup +52 -0
  52. package/runtime/vercel/agentbox-codex-hooks.json +68 -0
  53. package/runtime/vercel/agentbox-open +28 -0
  54. package/runtime/vercel/agentbox-setup-skill.md +196 -0
  55. package/runtime/vercel/agentbox-vnc-start +91 -0
  56. package/runtime/vercel/claude-managed-settings.json +115 -0
  57. package/runtime/vercel/ctl.cjs +23466 -0
  58. package/runtime/vercel/custom-system-CLAUDE.md +50 -0
  59. package/runtime/vercel/gh-shim +263 -0
  60. package/runtime/vercel/git-shim +131 -0
  61. package/runtime/vercel/scripts/provision.sh +274 -0
  62. package/share/agentbox-setup/SKILL.md +1 -1
  63. package/share/host-skills/agentbox/SKILL.md +29 -0
  64. package/share/host-skills/agentbox-info/SKILL.md +211 -0
  65. package/share/host-skills/codex/agentbox.md +35 -0
  66. package/share/host-skills/opencode/agentbox.md +26 -0
  67. package/dist/_cloud-attach-DMVH6GWO.js +0 -12
  68. package/dist/chunk-7KOEFGN2.js.map +0 -1
  69. package/dist/chunk-NAVL4R34.js.map +0 -1
  70. package/dist/chunk-NW5NYTQM.js.map +0 -1
  71. package/dist/chunk-UK72UQ5U.js.map +0 -1
  72. package/dist/chunk-V5KZGB5V.js.map +0 -1
  73. package/dist/dist-QZGJIBT5.js.map +0 -1
  74. package/dist/dist-R67WMLCF.js.map +0 -1
  75. /package/dist/{_cloud-attach-DMVH6GWO.js.map → _cloud-attach-ZXBCNWJX.js.map} +0 -0
  76. /package/dist/{cloud-poller-ZIWSADJB-JXFRJUEM.js.map → cloud-poller-SUNA6ZQC-2RG5WPRN.js.map} +0 -0
  77. /package/dist/{dist-ETCFRVPA.js.map → dist-32EZBYG4.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-KL36BRN4.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";
@@ -522,6 +736,7 @@ var BUILT_IN_DEFAULTS = {
522
736
  defaultCheckpointDocker: "",
523
737
  defaultCheckpointDaytona: "",
524
738
  defaultCheckpointHetzner: "",
739
+ defaultCheckpointVercel: "",
525
740
  withPlaywright: false,
526
741
  withEnv: false,
527
742
  vnc: true,
@@ -533,7 +748,11 @@ var BUILT_IN_DEFAULTS = {
533
748
  memory: 0,
534
749
  cpus: 0,
535
750
  pidsLimit: 0,
536
- disk: ""
751
+ disk: "",
752
+ bundleDepth: void 0,
753
+ vercelVcpus: 2,
754
+ vercelTimeoutMs: 27e5,
755
+ vercelNetworkPolicy: ""
537
756
  },
538
757
  checkpoint: {
539
758
  maxLayers: 3
@@ -582,6 +801,15 @@ var BUILT_IN_DEFAULTS = {
582
801
  maxRunningBoxes: 5,
583
802
  idleMinutes: 5
584
803
  },
804
+ queue: {
805
+ enabled: true,
806
+ maxConcurrent: 5,
807
+ maxWorking: 0,
808
+ idleGraceSeconds: 15
809
+ },
810
+ cloud: {
811
+ useCurrentBranch: false
812
+ },
585
813
  maintenance: {
586
814
  pruneProjectConfigs: true,
587
815
  pruneProjectConfigsEvery: 50
@@ -591,8 +819,8 @@ var KEY_REGISTRY = [
591
819
  {
592
820
  key: "box.provider",
593
821
  type: "enum",
594
- enumValues: ["docker", "daytona", "hetzner"],
595
- description: "Sandbox backend new boxes are created on: local Docker containers, Daytona Cloud sandboxes, or Hetzner Cloud VPSes."
822
+ enumValues: ["docker", "daytona", "hetzner", "vercel"],
823
+ description: "Sandbox backend new boxes are created on: local Docker containers, Daytona Cloud sandboxes, Hetzner Cloud VPSes, or Vercel Sandboxes."
596
824
  },
597
825
  {
598
826
  key: "box.hostSnapshot",
@@ -622,6 +850,12 @@ var KEY_REGISTRY = [
622
850
  description: "Per-provider override of `box.defaultCheckpoint` for hetzner. Wins over the global when set; set via `agentbox checkpoint set-default --provider hetzner`.",
623
851
  advanced: true
624
852
  },
853
+ {
854
+ key: "box.defaultCheckpointVercel",
855
+ type: "string",
856
+ description: "Per-provider override of `box.defaultCheckpoint` for vercel. Wins over the global when set; set via `agentbox checkpoint set-default --provider vercel`.",
857
+ advanced: true
858
+ },
625
859
  {
626
860
  key: "checkpoint.maxLayers",
627
861
  type: "int",
@@ -690,6 +924,26 @@ var KEY_REGISTRY = [
690
924
  description: "Best-effort writable-layer size for new boxes, e.g. '10G'. No-op on overlay2 / the macOS engines.",
691
925
  advanced: true
692
926
  },
927
+ {
928
+ key: "box.bundleDepth",
929
+ type: "int",
930
+ 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/)."
931
+ },
932
+ {
933
+ key: "box.vercelVcpus",
934
+ type: "int",
935
+ description: "vCPUs for new --provider vercel boxes (Vercel couples RAM at 2048 MB/vCPU). Default 2. Vercel only accepts specific counts (e.g. 1, 2, 4, 8) \u2014 an unsupported value fails create with a 400. Vercel-only; ignored by other providers."
936
+ },
937
+ {
938
+ key: "box.vercelTimeoutMs",
939
+ type: "int",
940
+ description: "Max session length (ms) for new --provider vercel boxes before the VM auto-snapshots; persistent mode auto-resumes on the next call. Default 2700000 (45 min, the Hobby ceiling). Vercel-only."
941
+ },
942
+ {
943
+ key: "box.vercelNetworkPolicy",
944
+ type: "string",
945
+ description: "Egress lock for new --provider vercel boxes: 'allow-all' (default, unset), 'deny-all', or a comma-separated domain allowlist (e.g. 'github.com,*.npmjs.org') that denies everything else. Vercel-only; ignored by other providers."
946
+ },
693
947
  {
694
948
  key: "claude.sessionName",
695
949
  type: "string",
@@ -797,6 +1051,31 @@ var KEY_REGISTRY = [
797
1051
  type: "int",
798
1052
  description: "Minutes a box must be continuously idle (claude state) before it is eligible for auto-pause."
799
1053
  },
1054
+ {
1055
+ key: "queue.enabled",
1056
+ type: "bool",
1057
+ description: "Run `agentbox claude|codex|opencode -i <prompt>` jobs through the host-wide background queue (FIFO, capped by queue.maxConcurrent)."
1058
+ },
1059
+ {
1060
+ key: "queue.maxConcurrent",
1061
+ type: "int",
1062
+ 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>`."
1063
+ },
1064
+ {
1065
+ key: "queue.maxWorking",
1066
+ type: "int",
1067
+ description: "Max agents actively working/thinking (quota-consuming) at once before background `-i` jobs queue. 0 = disabled (use the queue.maxConcurrent running-box gate). Counts all boxes, foreground + queued. Per-invocation override: `--max-working <n>`."
1068
+ },
1069
+ {
1070
+ key: "queue.idleGraceSeconds",
1071
+ type: "int",
1072
+ description: "Seconds an agent must stay non-working before it frees its working slot (debounce against brief idle flaps between turns). Only used when queue.maxWorking > 0."
1073
+ },
1074
+ {
1075
+ key: "cloud.useCurrentBranch",
1076
+ type: "bool",
1077
+ description: "On cloud providers (daytona/hetzner), start new boxes on the host's current branch instead of forking a new agentbox/<box-name> branch. Overridden by an explicit --use-branch / --from-branch."
1078
+ },
800
1079
  {
801
1080
  key: "maintenance.pruneProjectConfigs",
802
1081
  type: "bool",
@@ -818,7 +1097,7 @@ var UserConfigError = class extends Error {
818
1097
  this.name = "UserConfigError";
819
1098
  }
820
1099
  };
821
- function isPlainObject2(v) {
1100
+ function isPlainObject3(v) {
822
1101
  return typeof v === "object" && v !== null && !Array.isArray(v);
823
1102
  }
824
1103
  var RENAMED_KEYS = /* @__PURE__ */ new Map([["box.snapshot", "box.hostSnapshot"]]);
@@ -875,25 +1154,34 @@ function coerceTypedValue(raw, desc, where) {
875
1154
  function parseUserConfig(text, where) {
876
1155
  let doc;
877
1156
  try {
878
- doc = parseYaml2(text);
1157
+ doc = parseYaml3(text);
879
1158
  } catch (err) {
880
1159
  throw new UserConfigError(
881
1160
  `${where}: yaml parse error: ${err instanceof Error ? err.message : String(err)}`
882
1161
  );
883
1162
  }
884
1163
  if (doc === null || doc === void 0) return {};
885
- if (!isPlainObject2(doc)) {
1164
+ if (!isPlainObject3(doc)) {
886
1165
  throw new UserConfigError(`${where}: top-level must be a mapping`);
887
1166
  }
888
1167
  return parseUserConfigObject(doc, where);
889
1168
  }
890
1169
  function parseUserConfigObject(doc, where) {
891
1170
  if (doc === null || doc === void 0) return {};
892
- if (!isPlainObject2(doc)) {
1171
+ if (!isPlainObject3(doc)) {
893
1172
  throw new UserConfigError(`${where}: must be a mapping`);
894
1173
  }
895
1174
  const out = {};
896
1175
  for (const [branchName, branchRaw] of Object.entries(doc)) {
1176
+ if (branchName === "schema") {
1177
+ if (branchRaw !== void 0 && branchRaw !== null) {
1178
+ if (typeof branchRaw !== "number" || !Number.isInteger(branchRaw)) {
1179
+ throw new UserConfigError(`${where}.schema: must be an integer (got ${String(branchRaw)})`);
1180
+ }
1181
+ out.schema = branchRaw;
1182
+ }
1183
+ continue;
1184
+ }
897
1185
  const branchSpec = BRANCHES.get(branchName);
898
1186
  if (!branchSpec) {
899
1187
  throw new UserConfigError(
@@ -901,7 +1189,7 @@ function parseUserConfigObject(doc, where) {
901
1189
  );
902
1190
  }
903
1191
  if (branchRaw === null || branchRaw === void 0) continue;
904
- if (!isPlainObject2(branchRaw)) {
1192
+ if (!isPlainObject3(branchRaw)) {
905
1193
  throw new UserConfigError(`${where}.${branchName}: must be a mapping`);
906
1194
  }
907
1195
  const branchOut = {};
@@ -978,9 +1266,9 @@ function lookupKeyOrThrow(key) {
978
1266
  }
979
1267
  return desc;
980
1268
  }
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");
1269
+ var STATE_DIR2 = join(homedir(), ".agentbox");
1270
+ var GLOBAL_CONFIG_FILE = join(STATE_DIR2, "config.yaml");
1271
+ var PROJECTS_DIR = join(STATE_DIR2, "projects");
984
1272
  var WORKSPACE_CONFIG_BASENAME = "agentbox.yaml";
985
1273
  async function findProjectRoot(cwd) {
986
1274
  const start = await canonicalize(cwd);
@@ -1042,7 +1330,7 @@ async function configPathFor(scope, cwd) {
1042
1330
  async function loadOptionalUserConfig(path) {
1043
1331
  let text;
1044
1332
  try {
1045
- text = await readFile2(path, "utf8");
1333
+ text = await readFile3(path, "utf8");
1046
1334
  } catch (err) {
1047
1335
  if (err.code === "ENOENT") return {};
1048
1336
  throw err;
@@ -1053,7 +1341,7 @@ async function loadProjectAgentboxDefaults(workspacePath) {
1053
1341
  const path = workspaceConfigFile(workspacePath);
1054
1342
  let text;
1055
1343
  try {
1056
- text = await readFile2(path, "utf8");
1344
+ text = await readFile3(path, "utf8");
1057
1345
  } catch (err) {
1058
1346
  if (err.code === "ENOENT") return {};
1059
1347
  throw err;
@@ -1143,7 +1431,7 @@ function writeLeaf(obj, branch, leaf, value) {
1143
1431
  b[leaf] = value;
1144
1432
  }
1145
1433
  function resolveDefaultCheckpoint(cfg, provider) {
1146
- const perProvider = provider === "daytona" ? cfg.box.defaultCheckpointDaytona : provider === "hetzner" ? cfg.box.defaultCheckpointHetzner : cfg.box.defaultCheckpointDocker;
1434
+ const perProvider = provider === "daytona" ? cfg.box.defaultCheckpointDaytona : provider === "hetzner" ? cfg.box.defaultCheckpointHetzner : provider === "vercel" ? cfg.box.defaultCheckpointVercel : cfg.box.defaultCheckpointDocker;
1147
1435
  if (perProvider && perProvider.length > 0) return perProvider;
1148
1436
  return cfg.box.defaultCheckpoint;
1149
1437
  }
@@ -1151,6 +1439,7 @@ function defaultCheckpointConfigKey(provider) {
1151
1439
  if (provider === "docker") return "box.defaultCheckpointDocker";
1152
1440
  if (provider === "daytona") return "box.defaultCheckpointDaytona";
1153
1441
  if (provider === "hetzner") return "box.defaultCheckpointHetzner";
1442
+ if (provider === "vercel") return "box.defaultCheckpointVercel";
1154
1443
  return "box.defaultCheckpoint";
1155
1444
  }
1156
1445
  async function setConfigValue(scope, key, value, cwd, opts = {}) {
@@ -1161,6 +1450,7 @@ async function setConfigValue(scope, key, value, cwd, opts = {}) {
1161
1450
  const path = await configPathFor(scope, cwd);
1162
1451
  const current = await readExistingDoc(path);
1163
1452
  setLeaf(current, key, coerced);
1453
+ stampSchema(current);
1164
1454
  parseUserConfig(stringifyYaml(current), path);
1165
1455
  await atomicWriteYaml(path, current);
1166
1456
  if (scope === "project") {
@@ -1177,6 +1467,7 @@ async function unsetConfigValue(scope, key, cwd) {
1177
1467
  const current = await readExistingDoc(path);
1178
1468
  const existed = unsetLeaf(current, key);
1179
1469
  if (!existed) return { path, existed: false };
1470
+ stampSchema(current);
1180
1471
  await atomicWriteYaml(path, current);
1181
1472
  if (scope === "project") {
1182
1473
  const root = (await findProjectRoot(cwd)).root;
@@ -1287,6 +1578,12 @@ async function readExistingDoc(path) {
1287
1578
  }
1288
1579
  return parseUserConfig(text, path);
1289
1580
  }
1581
+ var CURRENT_CONFIG_SCHEMA = 1;
1582
+ function stampSchema(doc) {
1583
+ if (typeof doc.schema !== "number") {
1584
+ doc.schema = CURRENT_CONFIG_SCHEMA;
1585
+ }
1586
+ }
1290
1587
  function setLeaf(doc, key, value) {
1291
1588
  const idx = key.indexOf(".");
1292
1589
  const branch = key.slice(0, idx);
@@ -1341,181 +1638,7 @@ async function touchProjectMeta(absPath) {
1341
1638
  // ../../packages/sandbox-docker/dist/index.js
1342
1639
  import { chmod, mkdir as mkdir32, readFile as readFile32 } from "fs/promises";
1343
1640
  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
1641
+ import { execa as execa4 } from "execa";
1519
1642
  import { spawnSync as spawnSync2 } from "child_process";
1520
1643
  import { stat as stat22 } from "fs/promises";
1521
1644
  import { homedir as homedir32 } from "os";
@@ -1526,7 +1649,7 @@ import { stat as stat32 } from "fs/promises";
1526
1649
  import { homedir as homedir4 } from "os";
1527
1650
  import { join as join5 } from "path";
1528
1651
  import { execa as execa6 } from "execa";
1529
- import { randomBytes } from "crypto";
1652
+ import { randomBytes as randomBytes2 } from "crypto";
1530
1653
  import { execa as execa7 } from "execa";
1531
1654
  import { existsSync } from "fs";
1532
1655
  import { readFile as readFile42 } from "fs/promises";
@@ -1534,41 +1657,213 @@ import { homedir as homedir5 } from "os";
1534
1657
  import { join as join6 } from "path";
1535
1658
  import { execa as execa8 } from "execa";
1536
1659
  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";
1660
+ import { mkdir as mkdir4, readdir as readdir22, rm as rm22, stat as stat4 } from "fs/promises";
1542
1661
  import { homedir as homedir6, platform } from "os";
1543
- import { join as join7, resolve as resolve22 } from "path";
1662
+ import { join as join7, resolve as resolve2 } from "path";
1544
1663
  import { mkdir as mkdir5, mkdtemp as mkdtemp2, readFile as readFile5, readdir as readdir32, rm as rm3, writeFile as writeFile22 } from "fs/promises";
1545
1664
  import { homedir as homedir7, tmpdir as tmpdir2 } from "os";
1546
1665
  import { basename as basename3, join as join8 } from "path";
1547
- import { execa as execa11 } from "execa";
1666
+ import { execa as execa10 } from "execa";
1548
1667
  import { stat as stat5 } from "fs/promises";
1668
+ import { execa as execa11 } from "execa";
1549
1669
  import { execa as execa12 } from "execa";
1550
- import { execa as execa13 } from "execa";
1551
1670
  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";
1671
+ import { randomBytes as randomBytes22 } from "crypto";
1672
+ import { existsSync as existsSync2, openSync } from "fs";
1673
+ import { mkdir as mkdir6, readFile as readFile6, unlink as unlink2, writeFile as writeFile32 } from "fs/promises";
1555
1674
  import { request as httpRequest } from "http";
1556
1675
  import { homedir as homedir8 } from "os";
1557
- import { dirname as dirname22, join as join9, resolve as resolve3 } from "path";
1676
+ import { dirname as dirname3, join as join9, resolve as resolve22 } from "path";
1558
1677
  import { setTimeout as delay2 } from "timers/promises";
1559
- import { fileURLToPath as fileURLToPath2 } from "url";
1678
+ import { fileURLToPath } from "url";
1560
1679
 
1561
1680
  // ../../packages/relay/dist/index.js
1562
- import { execa as execa2 } from "execa";
1681
+ import { createHash as createHash2, randomBytes } from "crypto";
1682
+ import { execa } from "execa";
1683
+ import { spawn as spawn4 } from "child_process";
1684
+ import {
1685
+ mkdir as mkdir2,
1686
+ readdir as readdir2,
1687
+ readFile as readFile23,
1688
+ rename as rename2,
1689
+ unlink,
1690
+ writeFile as writeFile2
1691
+ } from "fs/promises";
1692
+ import { join as join3 } from "path";
1563
1693
  var DEFAULT_RELAY_PORT = 8787;
1564
1694
  var RELAY_CONTAINER_NAME = "agentbox-relay";
1565
1695
  var RELAY_NETWORK_NAME = "agentbox-net";
1566
1696
  var RELAY_IMAGE_REF = "agentbox/relay:dev";
1567
1697
  var DEFAULT_HOST_ACTION_MAX_AGE_MS = 15 * 60 * 1e3;
1698
+ function hashRpcParams(params) {
1699
+ return createHash2("sha256").update(canonicalJson(params)).digest("hex");
1700
+ }
1701
+ function canonicalJson(v) {
1702
+ if (v === null) return "null";
1703
+ if (typeof v === "undefined") return "null";
1704
+ if (typeof v === "number") return Number.isFinite(v) ? String(v) : "null";
1705
+ if (typeof v === "boolean") return v ? "true" : "false";
1706
+ if (typeof v === "string") return JSON.stringify(v);
1707
+ if (Array.isArray(v)) return "[" + v.map(canonicalJson).join(",") + "]";
1708
+ if (typeof v === "object") {
1709
+ const entries = Object.entries(v).filter(([k]) => k !== "hostInitiated").filter(([, val]) => val !== void 0).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
1710
+ return "{" + entries.map(([k, val]) => JSON.stringify(k) + ":" + canonicalJson(val)).join(",") + "}";
1711
+ }
1712
+ return "null";
1713
+ }
1714
+ var GH_PR_OPS = [
1715
+ "create",
1716
+ "view",
1717
+ "list",
1718
+ "comment",
1719
+ "review",
1720
+ "merge",
1721
+ "checkout",
1722
+ "close",
1723
+ "reopen"
1724
+ ];
1725
+ function injectPrCreateHead(op, branch, args) {
1726
+ if (op !== "create") return args;
1727
+ if (!branch || branch === "HEAD") return args;
1728
+ if (hasHeadArg(args)) return args;
1729
+ return ["--head", branch, ...args];
1730
+ }
1731
+ function hasHeadArg(args) {
1732
+ return args.some((a) => a === "--head" || a.startsWith("--head=") || a.startsWith("-H"));
1733
+ }
1568
1734
  var MAX_BODY_BYTES = 1024 * 1024;
1735
+ var QUEUE_DIR = join3(STATE_DIR, "queue");
1736
+ async function loadQueueConfig() {
1737
+ const d = BUILT_IN_DEFAULTS.queue;
1738
+ let global = {};
1739
+ try {
1740
+ global = parseUserConfig(await readFile23(GLOBAL_CONFIG_FILE, "utf8"), GLOBAL_CONFIG_FILE);
1741
+ } catch {
1742
+ }
1743
+ const q = global.queue ?? {};
1744
+ return {
1745
+ enabled: q.enabled ?? d.enabled,
1746
+ maxConcurrent: q.maxConcurrent ?? d.maxConcurrent,
1747
+ maxWorking: q.maxWorking ?? d.maxWorking,
1748
+ idleGraceMs: (q.idleGraceSeconds ?? d.idleGraceSeconds) * 1e3
1749
+ };
1750
+ }
1751
+ async function writeJob(job) {
1752
+ await mkdir2(QUEUE_DIR, { recursive: true });
1753
+ const final = join3(QUEUE_DIR, `${job.id}.json`);
1754
+ const tmp = `${final}.tmp.${String(process.pid)}.${String(Date.now())}`;
1755
+ await writeFile2(tmp, JSON.stringify(job, null, 2) + "\n", "utf8");
1756
+ await rename2(tmp, final);
1757
+ }
1758
+ async function readJob(id) {
1759
+ try {
1760
+ const raw = await readFile23(join3(QUEUE_DIR, `${id}.json`), "utf8");
1761
+ return JSON.parse(raw);
1762
+ } catch (err) {
1763
+ if (err.code === "ENOENT") return null;
1764
+ throw err;
1765
+ }
1766
+ }
1767
+ async function deleteJob(id) {
1768
+ try {
1769
+ await unlink(join3(QUEUE_DIR, `${id}.json`));
1770
+ } catch (err) {
1771
+ if (err.code !== "ENOENT") throw err;
1772
+ }
1773
+ }
1774
+ async function loadQueue() {
1775
+ let entries;
1776
+ try {
1777
+ entries = await readdir2(QUEUE_DIR);
1778
+ } catch (err) {
1779
+ if (err.code === "ENOENT") return [];
1780
+ throw err;
1781
+ }
1782
+ const out = [];
1783
+ for (const name of entries) {
1784
+ if (!name.endsWith(".json")) continue;
1785
+ try {
1786
+ const raw = await readFile23(join3(QUEUE_DIR, name), "utf8");
1787
+ out.push(JSON.parse(raw));
1788
+ } catch {
1789
+ }
1790
+ }
1791
+ out.sort((a, b) => a.createdAt < b.createdAt ? -1 : a.createdAt > b.createdAt ? 1 : 0);
1792
+ return out;
1793
+ }
1794
+ var RUNNING_COUNT_CACHE_MS = 3e3;
1795
+ var runningCountCache = null;
1796
+ async function defaultCountRunningBoxes() {
1797
+ const now = Date.now();
1798
+ if (runningCountCache && runningCountCache.expiresAt > now) {
1799
+ return runningCountCache.value;
1800
+ }
1801
+ const value = await uncachedCountRunningBoxes();
1802
+ runningCountCache = { value, expiresAt: now + RUNNING_COUNT_CACHE_MS };
1803
+ return value;
1804
+ }
1805
+ async function uncachedCountRunningBoxes() {
1806
+ let boxes;
1807
+ try {
1808
+ boxes = (await readState(STATE_FILE)).boxes;
1809
+ } catch {
1810
+ return 0;
1811
+ }
1812
+ if (boxes.length === 0) return 0;
1813
+ let count = 0;
1814
+ const dockerBoxes = [];
1815
+ for (const b of boxes) {
1816
+ const provider = b.provider ?? "docker";
1817
+ if (provider === "docker") {
1818
+ dockerBoxes.push(b);
1819
+ } else {
1820
+ count += 1;
1821
+ }
1822
+ }
1823
+ if (dockerBoxes.length > 0) {
1824
+ const states = await Promise.all(dockerBoxes.map((b) => inspectDockerState(b.container)));
1825
+ for (const s of states) {
1826
+ if (s === "running") count += 1;
1827
+ }
1828
+ }
1829
+ return count;
1830
+ }
1831
+ function inspectDockerState(containerName) {
1832
+ return new Promise((resolveP) => {
1833
+ const child = spawn4("docker", ["inspect", "--format", "{{.State.Status}}", containerName], {
1834
+ stdio: ["ignore", "pipe", "pipe"]
1835
+ });
1836
+ let out = "";
1837
+ let settled = false;
1838
+ const finish = (state) => {
1839
+ if (settled) return;
1840
+ settled = true;
1841
+ resolveP(state);
1842
+ };
1843
+ const timer = setTimeout(() => {
1844
+ child.kill("SIGTERM");
1845
+ finish("other");
1846
+ }, 1e4);
1847
+ child.stdout?.on("data", (c) => {
1848
+ out += c.toString("utf8");
1849
+ });
1850
+ child.on("error", () => {
1851
+ clearTimeout(timer);
1852
+ finish("other");
1853
+ });
1854
+ child.on("close", () => {
1855
+ clearTimeout(timer);
1856
+ finish(out.trim() === "running" ? "running" : "other");
1857
+ });
1858
+ });
1859
+ }
1860
+ var QUEUE_LOGS_DIR = join3(STATE_DIR, "logs");
1861
+ function queueLogPath(id) {
1862
+ return join3(QUEUE_LOGS_DIR, `queue-${id}.log`);
1863
+ }
1569
1864
 
1570
1865
  // ../../packages/sandbox-docker/dist/index.js
1571
- import { execa as execa16 } from "execa";
1866
+ import { execa as execa15 } from "execa";
1572
1867
  import { readdir as readdir4, rm as rm4, stat as stat7 } from "fs/promises";
1573
1868
  import { join as join12 } from "path";
1574
1869
 
@@ -1624,14 +1919,17 @@ var AmbiguousBoxError = class extends Error {
1624
1919
  };
1625
1920
 
1626
1921
  // ../../packages/sandbox-docker/dist/index.js
1627
- import { execa as execa15 } from "execa";
1922
+ import { execa as execa14 } from "execa";
1628
1923
  import { join as join11 } from "path";
1629
1924
  import { homedir as homedir10 } from "os";
1630
1925
  import { join as join13 } from "path";
1926
+ import { execa as execa16 } from "execa";
1927
+ import { existsSync as existsSync3, mkdirSync, renameSync, statSync } from "fs";
1928
+ import { basename as basename32, dirname as dirname22, posix, resolve as resolve4 } from "path";
1631
1929
  import { execa as execa17 } from "execa";
1632
1930
  import { copyFile, mkdtemp as mkdtemp3, readdir as readdir5, readFile as readFile7, rm as rm5, stat as stat8, writeFile as writeFile4 } from "fs/promises";
1633
1931
  import { homedir as homedir11, tmpdir as tmpdir3 } from "os";
1634
- import { basename as basename32, join as join14, relative as relative2 } from "path";
1932
+ import { basename as basename4, join as join14, relative as relative2 } from "path";
1635
1933
  import { execa as execa18 } from "execa";
1636
1934
  function isHostPathHookCommand(command, hostHome) {
1637
1935
  if (typeof command !== "string" || command.length === 0) return false;
@@ -1758,16 +2056,16 @@ function rewritePluginPaths(value, hostPluginsPrefix) {
1758
2056
  }
1759
2057
  return value;
1760
2058
  }
1761
- function isPlainObject3(v) {
2059
+ function isPlainObject4(v) {
1762
2060
  return v !== null && typeof v === "object" && !Array.isArray(v);
1763
2061
  }
1764
2062
  function additiveMerge(hostRoot, boxRoot, hostPluginsPrefix, selectMap, withMap) {
1765
2063
  const hostMap = selectMap(hostRoot);
1766
2064
  const boxMap = selectMap(boxRoot);
1767
- if (!isPlainObject3(boxMap)) {
2065
+ if (!isPlainObject4(boxMap)) {
1768
2066
  return { data: hostRoot, changed: false, addedKeys: [] };
1769
2067
  }
1770
- const base = isPlainObject3(hostMap) ? { ...hostMap } : {};
2068
+ const base = isPlainObject4(hostMap) ? { ...hostMap } : {};
1771
2069
  const addedKeys = [];
1772
2070
  for (const [key, value] of Object.entries(boxMap)) {
1773
2071
  if (Object.prototype.hasOwnProperty.call(base, key)) continue;
@@ -1782,7 +2080,7 @@ function additiveMerge(hostRoot, boxRoot, hostPluginsPrefix, selectMap, withMap)
1782
2080
  function mergeKnownMarketplaces(hostJson, boxJson, opts) {
1783
2081
  const prefix = `${opts.hostHome}/.claude/plugins/`;
1784
2082
  return additiveMerge(
1785
- isPlainObject3(hostJson) ? hostJson : {},
2083
+ isPlainObject4(hostJson) ? hostJson : {},
1786
2084
  boxJson,
1787
2085
  prefix,
1788
2086
  (root) => root,
@@ -1791,24 +2089,24 @@ function mergeKnownMarketplaces(hostJson, boxJson, opts) {
1791
2089
  }
1792
2090
  function mergeInstalledPlugins(hostJson, boxJson, opts) {
1793
2091
  const prefix = `${opts.hostHome}/.claude/plugins/`;
1794
- const hostRoot = isPlainObject3(hostJson) ? hostJson : { plugins: {} };
2092
+ const hostRoot = isPlainObject4(hostJson) ? hostJson : { plugins: {} };
1795
2093
  return additiveMerge(
1796
2094
  hostRoot,
1797
2095
  boxJson,
1798
2096
  prefix,
1799
- (root) => isPlainObject3(root) ? root["plugins"] : void 0,
2097
+ (root) => isPlainObject4(root) ? root["plugins"] : void 0,
1800
2098
  (host, merged) => ({ ...host, plugins: merged })
1801
2099
  );
1802
2100
  }
1803
2101
  function referencedPluginVersionKeys(installedPluginsJson) {
1804
2102
  const keys = /* @__PURE__ */ new Set();
1805
- if (!isPlainObject3(installedPluginsJson)) return keys;
2103
+ if (!isPlainObject4(installedPluginsJson)) return keys;
1806
2104
  const plugins = installedPluginsJson["plugins"];
1807
- if (!isPlainObject3(plugins)) return keys;
2105
+ if (!isPlainObject4(plugins)) return keys;
1808
2106
  for (const entries of Object.values(plugins)) {
1809
2107
  if (!Array.isArray(entries)) continue;
1810
2108
  for (const entry of entries) {
1811
- if (!isPlainObject3(entry)) continue;
2109
+ if (!isPlainObject4(entry)) continue;
1812
2110
  const installPath = entry["installPath"];
1813
2111
  if (typeof installPath !== "string") continue;
1814
2112
  const segments = installPath.split("/").filter((s) => s.length > 0);
@@ -1819,7 +2117,7 @@ function referencedPluginVersionKeys(installedPluginsJson) {
1819
2117
  return keys;
1820
2118
  }
1821
2119
  async function dockerInfo() {
1822
- const result = await execa4("docker", ["info"], { reject: false });
2120
+ const result = await execa2("docker", ["info"], { reject: false });
1823
2121
  if (result.exitCode !== 0) {
1824
2122
  throw new Error(
1825
2123
  `docker info failed (exit ${String(result.exitCode)}). Is the Docker daemon running?
@@ -1866,6 +2164,9 @@ async function runBox(spec) {
1866
2164
  // on the macOS engines). Boxes use it to reach the host relay process.
1867
2165
  "--add-host=host.docker.internal:host-gateway"
1868
2166
  ];
2167
+ if (process.env.AGENTBOX === "1") {
2168
+ args.push("--user", "0");
2169
+ }
1869
2170
  const lim = spec.limits;
1870
2171
  if (lim) {
1871
2172
  if (lim.memoryBytes && lim.memoryBytes > 0) {
@@ -1892,11 +2193,11 @@ async function runBox(spec) {
1892
2193
  args.push("-e", `${k}=${val}`);
1893
2194
  }
1894
2195
  args.push(spec.image, "sleep", "infinity");
1895
- const { stdout } = await execa4("docker", args);
2196
+ const { stdout } = await execa2("docker", args);
1896
2197
  return stdout.trim();
1897
2198
  }
1898
2199
  async function dockerStorageDriver() {
1899
- const result = await execa4("docker", ["info", "--format", "{{.Driver}}"], { reject: false });
2200
+ const result = await execa2("docker", ["info", "--format", "{{.Driver}}"], { reject: false });
1900
2201
  if (result.exitCode !== 0) return "";
1901
2202
  return (result.stdout ?? "").trim();
1902
2203
  }
@@ -1905,7 +2206,7 @@ async function execInBox(container, cmd, opts = {}) {
1905
2206
  if (opts.detach) args.push("-d");
1906
2207
  if (opts.user) args.push("--user", opts.user);
1907
2208
  args.push(container, ...cmd);
1908
- const result = await execa4("docker", args, {
2209
+ const result = await execa2("docker", args, {
1909
2210
  reject: false,
1910
2211
  ...opts.timeoutMs ? { timeout: opts.timeoutMs } : {}
1911
2212
  });
@@ -1916,49 +2217,49 @@ async function execInBox(container, cmd, opts = {}) {
1916
2217
  };
1917
2218
  }
1918
2219
  async function removeContainer(container) {
1919
- await execa4("docker", ["rm", "-f", container], { reject: false });
2220
+ await execa2("docker", ["rm", "-f", container], { reject: false });
1920
2221
  }
1921
2222
  async function removeVolume(name) {
1922
- await execa4("docker", ["volume", "rm", name], { reject: false });
2223
+ await execa2("docker", ["volume", "rm", name], { reject: false });
1923
2224
  }
1924
2225
  async function removeImage(ref, opts = {}) {
1925
2226
  const args = ["image", "rm"];
1926
2227
  if (opts.force !== false) args.push("-f");
1927
2228
  args.push(ref);
1928
- const result = await execa4("docker", args, { reject: false });
2229
+ const result = await execa2("docker", args, { reject: false });
1929
2230
  return result.exitCode === 0;
1930
2231
  }
1931
2232
  async function containerExists(name) {
1932
- const result = await execa4("docker", ["container", "inspect", "--format", "{{.Id}}", name], {
2233
+ const result = await execa2("docker", ["container", "inspect", "--format", "{{.Id}}", name], {
1933
2234
  reject: false
1934
2235
  });
1935
2236
  return result.exitCode === 0;
1936
2237
  }
1937
2238
  async function volumeExists(name) {
1938
- const result = await execa4("docker", ["volume", "inspect", name], { reject: false });
2239
+ const result = await execa2("docker", ["volume", "inspect", name], { reject: false });
1939
2240
  return result.exitCode === 0;
1940
2241
  }
1941
2242
  async function ensureVolume(name) {
1942
2243
  if (await volumeExists(name)) return;
1943
- await execa4("docker", ["volume", "create", name]);
2244
+ await execa2("docker", ["volume", "create", name]);
1944
2245
  }
1945
2246
  async function removeNetwork(name) {
1946
- await execa4("docker", ["network", "rm", name], { reject: false });
2247
+ await execa2("docker", ["network", "rm", name], { reject: false });
1947
2248
  }
1948
2249
  async function pauseContainer(name) {
1949
- await execa4("docker", ["pause", name]);
2250
+ await execa2("docker", ["pause", name]);
1950
2251
  }
1951
2252
  async function unpauseContainer(name) {
1952
- await execa4("docker", ["unpause", name]);
2253
+ await execa2("docker", ["unpause", name]);
1953
2254
  }
1954
2255
  async function stopContainer(name) {
1955
- await execa4("docker", ["stop", name]);
2256
+ await execa2("docker", ["stop", name]);
1956
2257
  }
1957
2258
  async function startContainer(name) {
1958
- await execa4("docker", ["start", name]);
2259
+ await execa2("docker", ["start", name]);
1959
2260
  }
1960
2261
  async function inspectContainerStatus(name) {
1961
- const result = await execa4("docker", ["inspect", "--format", "{{.State.Status}}", name], {
2262
+ const result = await execa2("docker", ["inspect", "--format", "{{.State.Status}}", name], {
1962
2263
  reject: false
1963
2264
  });
1964
2265
  if (result.exitCode !== 0) return "missing";
@@ -1979,7 +2280,7 @@ async function inspectContainerStatus(name) {
1979
2280
  }
1980
2281
  }
1981
2282
  async function inspectContainer(name) {
1982
- const result = await execa4("docker", ["inspect", name], { reject: false });
2283
+ const result = await execa2("docker", ["inspect", name], { reject: false });
1983
2284
  if (result.exitCode !== 0) return null;
1984
2285
  try {
1985
2286
  const parsed = JSON.parse(result.stdout ?? "null");
@@ -1989,7 +2290,7 @@ async function inspectContainer(name) {
1989
2290
  }
1990
2291
  }
1991
2292
  async function inspectVolumeMountpoint(name) {
1992
- const result = await execa4("docker", ["volume", "inspect", "--format", "{{.Mountpoint}}", name], {
2293
+ const result = await execa2("docker", ["volume", "inspect", "--format", "{{.Mountpoint}}", name], {
1993
2294
  reject: false
1994
2295
  });
1995
2296
  if (result.exitCode !== 0) return null;
@@ -1997,7 +2298,7 @@ async function inspectVolumeMountpoint(name) {
1997
2298
  }
1998
2299
  var AGENTBOX_PREFIX = "agentbox-";
1999
2300
  async function listAgentboxContainers() {
2000
- const result = await execa4(
2301
+ const result = await execa2(
2001
2302
  "docker",
2002
2303
  ["ps", "-a", "--filter", `name=^${AGENTBOX_PREFIX}`, "--format", "{{.Names}}"],
2003
2304
  { reject: false }
@@ -2006,7 +2307,7 @@ async function listAgentboxContainers() {
2006
2307
  return (result.stdout ?? "").split("\n").map((s) => s.trim()).filter((s) => s.startsWith(AGENTBOX_PREFIX));
2007
2308
  }
2008
2309
  async function publishedHostPort(container, containerPort) {
2009
- const result = await execa4("docker", ["port", container, `${String(containerPort)}/tcp`], {
2310
+ const result = await execa2("docker", ["port", container, `${String(containerPort)}/tcp`], {
2010
2311
  reject: false
2011
2312
  });
2012
2313
  if (result.exitCode !== 0) return null;
@@ -2016,7 +2317,7 @@ async function publishedHostPort(container, containerPort) {
2016
2317
  return m ? Number(m[1]) : null;
2017
2318
  }
2018
2319
  async function listAgentboxVolumes() {
2019
- const result = await execa4(
2320
+ const result = await execa2(
2020
2321
  "docker",
2021
2322
  ["volume", "ls", "--filter", `name=^${AGENTBOX_PREFIX}`, "--format", "{{.Name}}"],
2022
2323
  { reject: false }
@@ -2385,9 +2686,126 @@ var ExportError = class extends Error {
2385
2686
  this.stderr = stderr;
2386
2687
  this.name = "ExportError";
2387
2688
  }
2388
- stdout;
2389
- stderr;
2390
- };
2689
+ stdout;
2690
+ stderr;
2691
+ };
2692
+ async function copyCarryPathsToBox(opts) {
2693
+ const log = opts.onLog ?? (() => {
2694
+ });
2695
+ let copied = 0;
2696
+ const errors = [];
2697
+ const applied = [];
2698
+ for (const [i, entry] of opts.entries.entries()) {
2699
+ const where = `carry[${String(i)}] "${entry.rawSrc}"`;
2700
+ if (entry.kind === "missing") {
2701
+ log(`${where}: skipped (missing on host, optional)`);
2702
+ continue;
2703
+ }
2704
+ try {
2705
+ await copyOneEntry(opts.container, entry);
2706
+ copied += 1;
2707
+ applied.push({ src: entry.absSrc, dest: entry.absDest, bytes: entry.bytes ?? 0 });
2708
+ } catch (err) {
2709
+ const msg = err instanceof Error ? err.message : String(err);
2710
+ errors.push(`${where}: ${msg}`);
2711
+ log(`${where}: failed: ${msg}`);
2712
+ }
2713
+ }
2714
+ return { copied, errors, applied };
2715
+ }
2716
+ var BOX_HOME = "/home/vscode";
2717
+ async function copyOneEntry(container, entry) {
2718
+ if (entry.kind === "missing") return;
2719
+ const boxDest = entry.absDest.startsWith("~/") ? `${BOX_HOME}/${entry.absDest.slice(2)}` : entry.absDest;
2720
+ const boxDestParent = boxDest.endsWith("/") ? boxDest.slice(0, -1) : boxDest;
2721
+ const parentDir = entry.kind === "dir" ? boxDestParent : dirnameUnix(boxDestParent);
2722
+ const mkdir8 = await execa22(
2723
+ "docker",
2724
+ ["exec", "--user", "0:0", container, "mkdir", "-p", parentDir],
2725
+ { reject: false }
2726
+ );
2727
+ if (mkdir8.exitCode !== 0) {
2728
+ throw new Error(`mkdir -p ${parentDir} failed: ${String(mkdir8.stderr).slice(0, 300)}`);
2729
+ }
2730
+ if (entry.kind === "file") {
2731
+ const cp = await execa22(
2732
+ "docker",
2733
+ ["cp", entry.absSrc, `${container}:${boxDest}`],
2734
+ { reject: false }
2735
+ );
2736
+ if (cp.exitCode !== 0) {
2737
+ throw new Error(`docker cp failed: ${String(cp.stderr).slice(0, 300)}`);
2738
+ }
2739
+ } else {
2740
+ const packed = await execa22(
2741
+ "tar",
2742
+ ["-C", entry.absSrc, "-cf", "-", "."],
2743
+ { encoding: "buffer", reject: false }
2744
+ );
2745
+ if (packed.exitCode !== 0) {
2746
+ throw new Error(`tar pack failed: ${String(packed.stderr).slice(0, 300)}`);
2747
+ }
2748
+ const extract = await execa22(
2749
+ "docker",
2750
+ [
2751
+ "exec",
2752
+ "-i",
2753
+ "--user",
2754
+ "0:0",
2755
+ container,
2756
+ "tar",
2757
+ "-xf",
2758
+ "-",
2759
+ "-C",
2760
+ boxDest,
2761
+ "--no-same-permissions",
2762
+ "--no-same-owner",
2763
+ "-m"
2764
+ ],
2765
+ { input: packed.stdout, reject: false }
2766
+ );
2767
+ if (extract.exitCode !== 0) {
2768
+ throw new Error(`tar extract failed: ${String(extract.stderr).slice(0, 300)}`);
2769
+ }
2770
+ }
2771
+ if (entry.mode !== void 0) {
2772
+ const modeStr = entry.mode.toString(8).padStart(4, "0");
2773
+ const chmod2 = await execa22(
2774
+ "docker",
2775
+ ["exec", "--user", "0:0", container, "chmod", "-R", modeStr, boxDest],
2776
+ { reject: false }
2777
+ );
2778
+ if (chmod2.exitCode !== 0) {
2779
+ throw new Error(`chmod failed: ${String(chmod2.stderr).slice(0, 300)}`);
2780
+ }
2781
+ }
2782
+ const uid = entry.user ?? 1e3;
2783
+ const chown = await execa22(
2784
+ "docker",
2785
+ ["exec", "--user", "0:0", container, "chown", "-R", `${String(uid)}:${String(uid)}`, boxDest],
2786
+ { reject: false }
2787
+ );
2788
+ if (chown.exitCode !== 0) {
2789
+ throw new Error(`chown failed: ${String(chown.stderr).slice(0, 300)}`);
2790
+ }
2791
+ if (boxDest.startsWith(BOX_HOME + "/") && dirnameUnix(boxDest) !== BOX_HOME) {
2792
+ const safeDest = boxDest.replace(/'/g, `'\\''`);
2793
+ const script = `set -e; parent="$(dirname '${safeDest}')"; while [ "$parent" != "${BOX_HOME}" ] && [ "$parent" != "/" ]; do chown ${String(uid)}:${String(uid)} "$parent"; parent="$(dirname "$parent")"; done`;
2794
+ const chownParents = await execa22(
2795
+ "docker",
2796
+ ["exec", "--user", "0:0", container, "bash", "-c", script],
2797
+ { reject: false }
2798
+ );
2799
+ if (chownParents.exitCode !== 0) {
2800
+ throw new Error(`chown parents failed: ${String(chownParents.stderr).slice(0, 300)}`);
2801
+ }
2802
+ }
2803
+ }
2804
+ function dirnameUnix(p) {
2805
+ const i = p.lastIndexOf("/");
2806
+ if (i <= 0) return "/";
2807
+ return p.slice(0, i);
2808
+ }
2391
2809
  var SHARED_CLAUDE_VOLUME = "agentbox-claude-config";
2392
2810
  var DEFAULT_CLAUDE_SESSION = "claude";
2393
2811
  var CONTAINER_CLAUDE_DIR = "/home/vscode/.claude";
@@ -2403,7 +2821,7 @@ function resolveClaudeVolume(opts) {
2403
2821
  }
2404
2822
  async function pathExists(p) {
2405
2823
  try {
2406
- await stat4(p);
2824
+ await stat3(p);
2407
2825
  return true;
2408
2826
  } catch {
2409
2827
  return false;
@@ -2427,10 +2845,10 @@ async function findBrokenSymlinks(root) {
2427
2845
  return;
2428
2846
  }
2429
2847
  for (const ent of entries) {
2430
- const full = join23(dir, ent.name);
2848
+ const full = join22(dir, ent.name);
2431
2849
  if (ent.isSymbolicLink()) {
2432
2850
  try {
2433
- await stat4(full);
2851
+ await stat3(full);
2434
2852
  } catch {
2435
2853
  broken.push(relative(root, full));
2436
2854
  }
@@ -2447,13 +2865,13 @@ async function ensureClaudeVolume(spec, opts) {
2447
2865
  await ensureVolume(spec.volume);
2448
2866
  const created = !existed;
2449
2867
  if (!opts.syncFromHost) return { created, synced: false };
2450
- const hostClaude = join23(homedir22(), ".claude");
2868
+ const hostClaude = join22(homedir2(), ".claude");
2451
2869
  if (!await pathExists(hostClaude)) return { created, synced: false };
2452
- const hostClaudeJson = join23(homedir22(), ".claude.json");
2870
+ const hostClaudeJson = join22(homedir2(), ".claude.json");
2453
2871
  const hasJson = await pathExists(hostClaudeJson);
2454
2872
  const seedClaudeJson = !await volumeHasClaudeJson(spec.volume, opts.image);
2455
- const hostHome = homedir22();
2456
- const hostAgents = join23(homedir22(), ".agents");
2873
+ const hostHome = homedir2();
2874
+ const hostAgents = join22(homedir2(), ".agents");
2457
2875
  const hasAgents = await pathExists(hostAgents);
2458
2876
  const args = [
2459
2877
  "run",
@@ -2471,15 +2889,15 @@ async function ensureClaudeVolume(spec, opts) {
2471
2889
  ];
2472
2890
  if (hasJson && seedClaudeJson) args.push("-v", `${hostClaudeJson}:/src-claude-json:ro`);
2473
2891
  if (hasAgents) args.push("-v", `${hostAgents}:/.agents:ro`);
2474
- const filterDir = await mkdtemp(join23(tmpdir(), "agentbox-claude-filter-"));
2892
+ const filterDir = await mkdtemp(join22(tmpdir(), "agentbox-claude-filter-"));
2475
2893
  let filteredHookCount = 0;
2476
2894
  let installMethodFixed = false;
2477
2895
  let aliasedProjectKey = false;
2478
2896
  let workspaceTrusted = false;
2479
2897
  try {
2480
2898
  const settingsResult = await maybeFilterTo(
2481
- join23(hostClaude, "settings.json"),
2482
- join23(filterDir, "settings.json"),
2899
+ join22(hostClaude, "settings.json"),
2900
+ join22(filterDir, "settings.json"),
2483
2901
  hostHome
2484
2902
  );
2485
2903
  filteredHookCount += settingsResult.removedHooks;
@@ -2487,7 +2905,7 @@ async function ensureClaudeVolume(spec, opts) {
2487
2905
  } else if (hasJson) {
2488
2906
  const jsonResult = await maybeFilterTo(
2489
2907
  hostClaudeJson,
2490
- join23(filterDir, "_claude.json"),
2908
+ join22(filterDir, "_claude.json"),
2491
2909
  hostHome,
2492
2910
  {
2493
2911
  setInstallMethodNative: true,
@@ -2501,7 +2919,7 @@ async function ensureClaudeVolume(spec, opts) {
2501
2919
  workspaceTrusted = jsonResult.workspaceTrusted;
2502
2920
  } else {
2503
2921
  await writeFile3(
2504
- join23(filterDir, "_claude.json"),
2922
+ join22(filterDir, "_claude.json"),
2505
2923
  JSON.stringify(
2506
2924
  {
2507
2925
  installMethod: "native",
@@ -2603,7 +3021,7 @@ async function maybeFilterTo(src, dest, hostHome, opts = {}) {
2603
3021
  };
2604
3022
  let parsed;
2605
3023
  try {
2606
- parsed = JSON.parse(await readFile23(src, "utf8"));
3024
+ parsed = JSON.parse(await readFile24(src, "utf8"));
2607
3025
  } catch {
2608
3026
  return zero;
2609
3027
  }
@@ -2664,14 +3082,14 @@ var PLUGIN_INSTALL_BACKOFF_MIN = Math.round(PLUGIN_INSTALL_BACKOFF_MS / 6e4);
2664
3082
  var NPM_CACHE_DIR = "/home/vscode/.claude/.agentbox-npm-cache";
2665
3083
  async function isFile(p) {
2666
3084
  try {
2667
- return (await stat4(p)).isFile();
3085
+ return (await stat3(p)).isFile();
2668
3086
  } catch {
2669
3087
  return false;
2670
3088
  }
2671
3089
  }
2672
3090
  async function isRecentFailMarker(p) {
2673
3091
  try {
2674
- const st = await stat4(p);
3092
+ const st = await stat3(p);
2675
3093
  return Date.now() - st.mtimeMs < PLUGIN_INSTALL_BACKOFF_MS;
2676
3094
  } catch {
2677
3095
  return false;
@@ -2679,14 +3097,14 @@ async function isRecentFailMarker(p) {
2679
3097
  }
2680
3098
  async function isDir(p) {
2681
3099
  try {
2682
- return (await stat4(p)).isDirectory();
3100
+ return (await stat3(p)).isDirectory();
2683
3101
  } catch {
2684
3102
  return false;
2685
3103
  }
2686
3104
  }
2687
3105
  async function readReferencedPluginKeys(installedPluginsJsonPath) {
2688
3106
  try {
2689
- const raw = await readFile23(installedPluginsJsonPath, "utf8");
3107
+ const raw = await readFile24(installedPluginsJsonPath, "utf8");
2690
3108
  return referencedPluginVersionKeys(JSON.parse(raw));
2691
3109
  } catch {
2692
3110
  return /* @__PURE__ */ new Set();
@@ -2694,7 +3112,7 @@ async function readReferencedPluginKeys(installedPluginsJsonPath) {
2694
3112
  }
2695
3113
  async function scanPluginCacheForRebuild(cacheRoot) {
2696
3114
  const referenced = await readReferencedPluginKeys(
2697
- join23(cacheRoot, "..", "installed_plugins.json")
3115
+ join22(cacheRoot, "..", "installed_plugins.json")
2698
3116
  );
2699
3117
  let marketplaces;
2700
3118
  try {
@@ -2704,7 +3122,7 @@ async function scanPluginCacheForRebuild(cacheRoot) {
2704
3122
  }
2705
3123
  for (const m of marketplaces) {
2706
3124
  if (!m.isDirectory()) continue;
2707
- const mPath = join23(cacheRoot, m.name);
3125
+ const mPath = join22(cacheRoot, m.name);
2708
3126
  let plugins;
2709
3127
  try {
2710
3128
  plugins = await readdir3(mPath, { withFileTypes: true });
@@ -2713,7 +3131,7 @@ async function scanPluginCacheForRebuild(cacheRoot) {
2713
3131
  }
2714
3132
  for (const p of plugins) {
2715
3133
  if (!p.isDirectory()) continue;
2716
- const pPath = join23(mPath, p.name);
3134
+ const pPath = join22(mPath, p.name);
2717
3135
  let versions;
2718
3136
  try {
2719
3137
  versions = await readdir3(pPath, { withFileTypes: true });
@@ -2723,10 +3141,10 @@ async function scanPluginCacheForRebuild(cacheRoot) {
2723
3141
  for (const v of versions) {
2724
3142
  if (!v.isDirectory()) continue;
2725
3143
  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;
3144
+ const vPath = join22(pPath, v.name);
3145
+ if (!await isFile(join22(vPath, "package.json"))) continue;
3146
+ if (await isFile(join22(vPath, PLUGIN_INSTALLED_MARKER))) continue;
3147
+ if (await isRecentFailMarker(join22(vPath, PLUGIN_FAILED_MARKER))) continue;
2730
3148
  return true;
2731
3149
  }
2732
3150
  }
@@ -3018,67 +3436,31 @@ async function waitForTmuxPaneContent(container, sessionName, timeoutMs = 2e4) {
3018
3436
  await delay(400);
3019
3437
  }
3020
3438
  }
3021
- function buildTmuxSessionArgs(sessionName) {
3022
- const s = sessionName;
3439
+ function tmuxConfigSubcommands(sessionName) {
3023
3440
  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"
3441
+ ["set", "-g", "prefix", "C-a"],
3442
+ ["set", "-g", "prefix2", "C-b"],
3443
+ ["bind-key", "C-a", "send-prefix"],
3444
+ ["bind-key", "C-b", "send-prefix", "-2"],
3445
+ ["bind-key", "d", "detach-client"],
3446
+ ["set", "-g", "extended-keys", "on"],
3447
+ ["set", "-as", "terminal-features", ",*:extkeys"],
3448
+ ["set", "-t", sessionName, "status", "off"]
3080
3449
  ];
3081
3450
  }
3451
+ function buildTmuxSessionArgs(sessionName) {
3452
+ const out = [];
3453
+ for (const sub of tmuxConfigSubcommands(sessionName)) {
3454
+ out.push(";", ...sub);
3455
+ }
3456
+ return out;
3457
+ }
3458
+ function buildTmuxConfigShellSnippet(sessionName) {
3459
+ return tmuxConfigSubcommands(sessionName).map((sub) => `tmux ${sub.map(shellSingleQuoteIfNeeded).join(" ")}`).join("; ");
3460
+ }
3461
+ function shellSingleQuoteIfNeeded(s) {
3462
+ return /^[A-Za-z0-9_:.\/=+-]+$/.test(s) ? s : "'" + s.replace(/'/g, "'\\''") + "'";
3463
+ }
3082
3464
  function buildShellArgv(container) {
3083
3465
  const term = process.env["TERM"] ?? "xterm-256color";
3084
3466
  return ["exec", "-it", "-e", `TERM=${term}`, "--user", CONTAINER_USER, container, "bash", "-l"];
@@ -3197,14 +3579,14 @@ async function listChildDirs(dir) {
3197
3579
  }
3198
3580
  async function readJsonFile(path) {
3199
3581
  try {
3200
- return JSON.parse(await readFile23(path, "utf8"));
3582
+ return JSON.parse(await readFile24(path, "utf8"));
3201
3583
  } catch {
3202
3584
  return void 0;
3203
3585
  }
3204
3586
  }
3205
3587
  async function pullClaudeExtras(spec, opts) {
3206
- const hostHome = homedir22();
3207
- const hostClaude = join23(hostHome, ".claude");
3588
+ const hostHome = homedir2();
3589
+ const hostClaude = join22(hostHome, ".claude");
3208
3590
  const inventoryScript = [
3209
3591
  "for cat in skills agents commands; do",
3210
3592
  ' [ -d "/src/$cat" ] || continue;',
@@ -3266,7 +3648,7 @@ async function pullClaudeExtras(spec, opts) {
3266
3648
  const newItems = [];
3267
3649
  const applyPaths = [];
3268
3650
  for (const cat of PULL_DIR_CATEGORIES) {
3269
- const hostNames = await listChildDirs(join23(hostClaude, cat));
3651
+ const hostNames = await listChildDirs(join22(hostClaude, cat));
3270
3652
  const excludes = cat === "skills" ? SKILL_EXCLUDE_PREFIXES : [];
3271
3653
  for (const name of pickNewItems(boxDirs[cat] ?? [], hostNames, excludes)) {
3272
3654
  newItems.push({ category: cat, name });
@@ -3274,8 +3656,8 @@ async function pullClaudeExtras(spec, opts) {
3274
3656
  }
3275
3657
  }
3276
3658
  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))) {
3659
+ for (const m of await listChildDirs(join22(hostClaude, "plugins", "cache"))) {
3660
+ for (const p of await listChildDirs(join22(hostClaude, "plugins", "cache", m))) {
3279
3661
  hostPluginKeys.push(`${m}/${p}`);
3280
3662
  }
3281
3663
  }
@@ -3283,8 +3665,8 @@ async function pullClaudeExtras(spec, opts) {
3283
3665
  newItems.push({ category: "plugins", name: key });
3284
3666
  applyPaths.push({ src: `/src/plugins/cache/${key}`, dest: `/dst/plugins/cache/${key}` });
3285
3667
  }
3286
- const hostInstalled = await readJsonFile(join23(hostClaude, "plugins", "installed_plugins.json"));
3287
- const hostMarkets = await readJsonFile(join23(hostClaude, "plugins", "known_marketplaces.json"));
3668
+ const hostInstalled = await readJsonFile(join22(hostClaude, "plugins", "installed_plugins.json"));
3669
+ const hostMarkets = await readJsonFile(join22(hostClaude, "plugins", "known_marketplaces.json"));
3288
3670
  const mergedInstalled = mergeInstalledPlugins(hostInstalled, boxJson["installed_plugins"], {
3289
3671
  hostHome
3290
3672
  });
@@ -3327,17 +3709,17 @@ async function pullClaudeExtras(spec, opts) {
3327
3709
  }
3328
3710
  }
3329
3711
  if (mergedMarkets.changed || mergedInstalled.changed) {
3330
- await mkdir22(join23(hostClaude, "plugins"), { recursive: true });
3712
+ await mkdir22(join22(hostClaude, "plugins"), { recursive: true });
3331
3713
  if (mergedMarkets.changed) {
3332
3714
  await writeFile3(
3333
- join23(hostClaude, "plugins", "known_marketplaces.json"),
3715
+ join22(hostClaude, "plugins", "known_marketplaces.json"),
3334
3716
  `${JSON.stringify(mergedMarkets.data, null, 2)}
3335
3717
  `
3336
3718
  );
3337
3719
  }
3338
3720
  if (mergedInstalled.changed) {
3339
3721
  await writeFile3(
3340
- join23(hostClaude, "plugins", "installed_plugins.json"),
3722
+ join22(hostClaude, "plugins", "installed_plugins.json"),
3341
3723
  `${JSON.stringify(mergedInstalled.data, null, 2)}
3342
3724
  `
3343
3725
  );
@@ -3345,7 +3727,23 @@ async function pullClaudeExtras(spec, opts) {
3345
3727
  }
3346
3728
  return { newItems, mergedRegistries };
3347
3729
  }
3348
- var CREDENTIALS_BACKUP_FILE = join32(STATE_DIR2, "claude-credentials.json");
3730
+ var CREDENTIALS_BACKUP_FILE = join32(STATE_DIR, "claude-credentials.json");
3731
+ var CODEX_CREDENTIALS_BACKUP_FILE = join32(STATE_DIR, "codex-credentials.json");
3732
+ var OPENCODE_CREDENTIALS_BACKUP_FILE = join32(STATE_DIR, "opencode-credentials.json");
3733
+ function isRealAgentCredential(agent, text) {
3734
+ let parsed;
3735
+ try {
3736
+ parsed = JSON.parse(text);
3737
+ } catch {
3738
+ return false;
3739
+ }
3740
+ if (typeof parsed !== "object" || parsed === null) return false;
3741
+ if (agent === "claude") {
3742
+ const rt = parsed.claudeAiOauth?.refreshToken;
3743
+ return typeof rt === "string" && rt.length > 0;
3744
+ }
3745
+ return Object.keys(parsed).length > 0;
3746
+ }
3349
3747
  async function hostBackupHasCredentials(path = CREDENTIALS_BACKUP_FILE) {
3350
3748
  try {
3351
3749
  const parsed = JSON.parse(await readFile32(path, "utf8"));
@@ -3377,8 +3775,8 @@ echo "EXTRACTED=$EXTRACTED SEEDED=$SEEDED VOLREAL=$VOL_REAL"
3377
3775
  `;
3378
3776
  async function syncClaudeCredentials(spec, opts) {
3379
3777
  try {
3380
- await mkdir32(STATE_DIR2, { recursive: true });
3381
- const { stdout } = await execa42("docker", [
3778
+ await mkdir32(STATE_DIR, { recursive: true });
3779
+ const { stdout } = await execa4("docker", [
3382
3780
  "run",
3383
3781
  "--rm",
3384
3782
  "--user",
@@ -3386,7 +3784,7 @@ async function syncClaudeCredentials(spec, opts) {
3386
3784
  "-v",
3387
3785
  `${spec.volume}:/dst`,
3388
3786
  "-v",
3389
- `${STATE_DIR2}:/host-state`,
3787
+ `${STATE_DIR}:/host-state`,
3390
3788
  "-e",
3391
3789
  `ISOLATE=${opts.isolate ? "yes" : "no"}`,
3392
3790
  opts.image,
@@ -3529,9 +3927,10 @@ ${(install.stdout ?? "").toString().slice(-600)}`
3529
3927
  }
3530
3928
  return { installed: true };
3531
3929
  }
3930
+ var CODEX_AGENTBOX_FLAGS = ["--enable", "hooks", "--dangerously-bypass-hook-trust"];
3532
3931
  async function startCodexSession(opts) {
3533
3932
  const sessionName = opts.sessionName ?? DEFAULT_CODEX_SESSION;
3534
- const cmd = ["codex", ...opts.codexArgs].map(shQuote2).join(" ");
3933
+ const cmd = ["codex", ...CODEX_AGENTBOX_FLAGS, ...opts.codexArgs].map(shQuote2).join(" ");
3535
3934
  const term = process.env["TERM"] ?? "xterm-256color";
3536
3935
  const envFlags = ["-e", `TERM=${term}`];
3537
3936
  for (const k of CODEX_FORWARDED_ENV_KEYS) {
@@ -3726,6 +4125,8 @@ var SHARED_OPENCODE_VOLUME = "agentbox-opencode-config";
3726
4125
  var DEFAULT_OPENCODE_SESSION = "opencode";
3727
4126
  var CONTAINER_OPENCODE_DIR = "/home/vscode/.local/share/opencode";
3728
4127
  var CONTAINER_OPENCODE_CONFIG_DIR = "/home/vscode/.local/share/opencode/config";
4128
+ var CONTAINER_OPENCODE_STATE_HOME = "/home/vscode/.local/share/opencode/.state";
4129
+ var IN_BOX_OPENCODE_PLUGIN_PATH = "/usr/local/share/agentbox/opencode-agentbox-plugin.js";
3729
4130
  function resolveOpencodeVolume(opts) {
3730
4131
  if (opts.isolate) {
3731
4132
  return { volume: `${SHARED_OPENCODE_VOLUME}-${opts.boxId}` };
@@ -3751,13 +4152,16 @@ async function ensureOpencodeVolume(spec, opts) {
3751
4152
  const created = !existed;
3752
4153
  const hostData = join5(homedir4(), ".local", "share", "opencode");
3753
4154
  const hostConfig = join5(homedir4(), ".config", "opencode");
4155
+ const hostState = join5(homedir4(), ".local", "state", "opencode");
3754
4156
  const hasData = await pathExists3(hostData);
3755
4157
  const hasConfig = await pathExists3(hostConfig);
3756
- const willSync = opts.syncFromHost && (hasData || hasConfig);
4158
+ const hasState = await pathExists3(hostState);
4159
+ const willSync = opts.syncFromHost && (hasData || hasConfig || hasState);
3757
4160
  if (willSync) {
3758
4161
  const args = ["run", "--rm", "--user", "0", "-v", `${spec.volume}:/dst`];
3759
4162
  if (hasData) args.push("-v", `${hostData}:/src-data:ro`);
3760
4163
  if (hasConfig) args.push("-v", `${hostConfig}:/src-config:ro`);
4164
+ if (hasState) args.push("-v", `${hostState}:/src-state:ro`);
3761
4165
  const steps = [];
3762
4166
  if (hasData) {
3763
4167
  steps.push(
@@ -3767,6 +4171,11 @@ async function ensureOpencodeVolume(spec, opts) {
3767
4171
  if (hasConfig) {
3768
4172
  steps.push("mkdir -p /dst/config && rsync -a /src-config/ /dst/config/");
3769
4173
  }
4174
+ if (hasState) {
4175
+ steps.push(
4176
+ "mkdir -p /dst/.state/opencode && rsync -a --update --exclude=locks /src-state/ /dst/.state/opencode/"
4177
+ );
4178
+ }
3770
4179
  steps.push("chown -R 1000:1000 /dst");
3771
4180
  args.push(opts.image, "sh", "-c", steps.join(" && "));
3772
4181
  await execa6("docker", args);
@@ -3790,6 +4199,25 @@ async function ensureOpencodeVolume(spec, opts) {
3790
4199
  );
3791
4200
  return { created, synced: false };
3792
4201
  }
4202
+ async function seedOpencodePlugin(volume, image) {
4203
+ try {
4204
+ const { stdout } = await execa6("docker", [
4205
+ "run",
4206
+ "--rm",
4207
+ "--user",
4208
+ "0",
4209
+ "-v",
4210
+ `${volume}:/dst`,
4211
+ image,
4212
+ "sh",
4213
+ "-c",
4214
+ `{ [ -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`
4215
+ ]);
4216
+ return { seeded: stdout.includes("SEEDED") };
4217
+ } catch {
4218
+ return { seeded: false };
4219
+ }
4220
+ }
3793
4221
  var OPENCODE_FORWARDED_ENV_KEYS = [
3794
4222
  "ANTHROPIC_API_KEY",
3795
4223
  "OPENAI_API_KEY",
@@ -3800,7 +4228,10 @@ var OPENCODE_FORWARDED_ENV_KEYS = [
3800
4228
  "GROQ_API_KEY"
3801
4229
  ];
3802
4230
  function buildOpencodeMounts(spec, hostEnv) {
3803
- const env = { OPENCODE_CONFIG_DIR: CONTAINER_OPENCODE_CONFIG_DIR };
4231
+ const env = {
4232
+ OPENCODE_CONFIG_DIR: CONTAINER_OPENCODE_CONFIG_DIR,
4233
+ XDG_STATE_HOME: CONTAINER_OPENCODE_STATE_HOME
4234
+ };
3804
4235
  for (const k of OPENCODE_FORWARDED_ENV_KEYS) {
3805
4236
  const v = hostEnv[k];
3806
4237
  if (typeof v === "string" && v.length > 0) env[k] = v;
@@ -3915,6 +4346,8 @@ function buildOpencodeLoginRunArgv(opts) {
3915
4346
  "DISPLAY=",
3916
4347
  "-e",
3917
4348
  `OPENCODE_CONFIG_DIR=${CONTAINER_OPENCODE_CONFIG_DIR}`,
4349
+ "-e",
4350
+ `XDG_STATE_HOME=${CONTAINER_OPENCODE_STATE_HOME}`,
3918
4351
  "-v",
3919
4352
  `${opts.volume}:${CONTAINER_OPENCODE_DIR}`,
3920
4353
  "--user",
@@ -4105,7 +4538,7 @@ async function launchVncDaemon(container, timeoutMs = 5e3) {
4105
4538
  }
4106
4539
  var VNC_PASSWORD_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
4107
4540
  function generateVncPassword() {
4108
- const bytes = randomBytes(8);
4541
+ const bytes = randomBytes2(8);
4109
4542
  let out = "";
4110
4543
  for (let i = 0; i < 8; i++) {
4111
4544
  out += VNC_PASSWORD_ALPHABET[bytes[i] % VNC_PASSWORD_ALPHABET.length];
@@ -4124,6 +4557,10 @@ function buildVncUrls(record, engine) {
4124
4557
  if (record.vncHostPort) {
4125
4558
  urls.loopbackUrl = `http://127.0.0.1:${String(record.vncHostPort)}/vnc.html?${qs}`;
4126
4559
  }
4560
+ if (record.portlessVncAlias) {
4561
+ const base = record.portlessVncUrl ?? `https://${record.portlessVncAlias}.localhost`;
4562
+ urls.portlessUrl = `${base}/vnc.html?${qs}`;
4563
+ }
4127
4564
  return urls;
4128
4565
  }
4129
4566
  var WEB_CONTAINER_PORT = 80;
@@ -4206,23 +4643,11 @@ async function seedWorkspace(opts) {
4206
4643
  for (const r of opts.repos) {
4207
4644
  const main = r.repo.hostMainRepo;
4208
4645
  const wt = r.gitWorktreePath;
4646
+ const baseRef = r.repo.kind === "root" ? opts.fromBranch ?? "HEAD" : "HEAD";
4647
+ const addArgs = r.reuseBranch ? ["worktree", "add", wt, r.branch] : ["worktree", "add", "-b", r.branch, wt, baseRef];
4209
4648
  const add = await execa7(
4210
4649
  "docker",
4211
- [
4212
- "exec",
4213
- "--user",
4214
- "vscode",
4215
- opts.container,
4216
- "git",
4217
- "-C",
4218
- main,
4219
- "worktree",
4220
- "add",
4221
- "-b",
4222
- r.branch,
4223
- wt,
4224
- "HEAD"
4225
- ],
4650
+ ["exec", "--user", "vscode", opts.container, "git", "-C", main, ...addArgs],
4226
4651
  { reject: false }
4227
4652
  );
4228
4653
  if (add.exitCode !== 0) {
@@ -4515,79 +4940,6 @@ async function isProxyRunning() {
4515
4940
  return false;
4516
4941
  }
4517
4942
  }
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
4943
  var EXCLUDE_DIRS = /* @__PURE__ */ new Set([
4592
4944
  "node_modules",
4593
4945
  ".next",
@@ -4633,12 +4985,12 @@ async function findExcludedDirs(root, excluded = EXCLUDE_DIRS) {
4633
4985
  return matches;
4634
4986
  }
4635
4987
  async function createSnapshot(opts) {
4636
- const source = resolve22(opts.source);
4637
- const destination = resolve22(opts.destination);
4988
+ const source = resolve2(opts.source);
4989
+ const destination = resolve2(opts.destination);
4638
4990
  const excluded = opts.excluded ?? EXCLUDE_DIRS;
4639
4991
  await mkdir4(SNAPSHOTS_ROOT, { recursive: true });
4640
4992
  const cpArgs = platform() === "darwin" ? ["-cR"] : ["-R"];
4641
- await execa10("cp", [...cpArgs, `${source}/`, destination]);
4993
+ await execa9("cp", [...cpArgs, `${source}/`, destination]);
4642
4994
  const toPrune = await findExcludedDirs(destination, excluded);
4643
4995
  await Promise.all(toPrune.map((p) => rm22(p, { recursive: true, force: true })));
4644
4996
  return { destination, prunedPaths: toPrune };
@@ -4659,7 +5011,7 @@ async function readManifest(dir) {
4659
5011
  try {
4660
5012
  const raw = await readFile5(join8(dir, "manifest.json"), "utf8");
4661
5013
  const m = JSON.parse(raw);
4662
- if (m.schema !== 2) return null;
5014
+ if (m.schema !== 2 && m.schema !== 3) return null;
4663
5015
  return m;
4664
5016
  } catch {
4665
5017
  return null;
@@ -4747,7 +5099,7 @@ async function runCleanup(container, log) {
4747
5099
  }
4748
5100
  }
4749
5101
  async function inspectImageConfig(imageRef) {
4750
- const r = await execa11("docker", ["image", "inspect", imageRef], { reject: false });
5102
+ const r = await execa10("docker", ["image", "inspect", imageRef], { reject: false });
4751
5103
  if (r.exitCode !== 0) {
4752
5104
  throw new CheckpointError(`docker image inspect ${imageRef} failed`, r.stdout, r.stderr);
4753
5105
  }
@@ -4823,14 +5175,14 @@ async function createCheckpoint(opts) {
4823
5175
  await runCleanup(box.container, log);
4824
5176
  if (type === "layered") {
4825
5177
  log(`docker commit ${box.container} -> ${tag} (layered)`);
4826
- const r = await execa11("docker", ["commit", box.container, tag], { reject: false });
5178
+ const r = await execa10("docker", ["commit", box.container, tag], { reject: false });
4827
5179
  if (r.exitCode !== 0) {
4828
5180
  throw new CheckpointError(`docker commit failed for ${box.container}`, r.stdout, r.stderr);
4829
5181
  }
4830
5182
  } else {
4831
5183
  log(`docker commit ${box.container} -> <intermediate> (flattened path)`);
4832
5184
  const intermediate = `${tag}-intermediate`;
4833
- const commit = await execa11("docker", ["commit", box.container, intermediate], {
5185
+ const commit = await execa10("docker", ["commit", box.container, intermediate], {
4834
5186
  reject: false
4835
5187
  });
4836
5188
  if (commit.exitCode !== 0) {
@@ -4843,8 +5195,15 @@ async function createCheckpoint(opts) {
4843
5195
  }
4844
5196
  }
4845
5197
  const base = (box.gitWorktrees ?? []).some((w) => w.kind === "root") ? "worktree" : "workspace";
5198
+ const prepared = readPreparedDockerState();
5199
+ let baseFingerprint = prepared?.base?.contextSha256;
5200
+ if (!baseFingerprint) {
5201
+ const fp = await computeDockerContextFingerprint();
5202
+ baseFingerprint = fp?.contextSha256;
5203
+ }
5204
+ const stamp = readCliStamp();
4846
5205
  const manifest = {
4847
- schema: 2,
5206
+ schema: 3,
4848
5207
  name,
4849
5208
  type,
4850
5209
  image: tag,
@@ -4854,6 +5213,9 @@ async function createCheckpoint(opts) {
4854
5213
  sourceBoxId: box.id,
4855
5214
  sourceBoxName: box.name,
4856
5215
  worktrees: box.gitWorktrees,
5216
+ baseProvider: "docker",
5217
+ baseFingerprint,
5218
+ cliVersion: stamp.cliVersion,
4857
5219
  createdAt: (/* @__PURE__ */ new Date()).toISOString()
4858
5220
  };
4859
5221
  await writeFile22(join8(dir, "manifest.json"), JSON.stringify(manifest, null, 2) + "\n", "utf8");
@@ -4865,7 +5227,7 @@ async function createCheckpoint(opts) {
4865
5227
  }
4866
5228
  async function flattenImage(sourceTag, destTag, log) {
4867
5229
  const tmpName = `agentbox-flatten-${Date.now().toString(36)}`;
4868
- const create = await execa11(
5230
+ const create = await execa10(
4869
5231
  "docker",
4870
5232
  ["create", "--name", tmpName, sourceTag, "sleep", "0"],
4871
5233
  { reject: false }
@@ -4877,7 +5239,7 @@ async function flattenImage(sourceTag, destTag, log) {
4877
5239
  try {
4878
5240
  const rootfsPath = join8(scratch, "rootfs.tar");
4879
5241
  log(`exporting rootfs of ${sourceTag} to ${rootfsPath}`);
4880
- const exp = await execa11("docker", ["export", "-o", rootfsPath, tmpName], { reject: false });
5242
+ const exp = await execa10("docker", ["export", "-o", rootfsPath, tmpName], { reject: false });
4881
5243
  if (exp.exitCode !== 0) {
4882
5244
  throw new CheckpointError(`docker export failed`, exp.stdout, exp.stderr);
4883
5245
  }
@@ -4890,7 +5252,7 @@ async function flattenImage(sourceTag, destTag, log) {
4890
5252
  ];
4891
5253
  await writeFile22(join8(scratch, "Dockerfile"), lines.join("\n") + "\n", "utf8");
4892
5254
  log(`building flattened ${destTag} from rootfs.tar (FROM scratch)`);
4893
- const build = await execa11(
5255
+ const build = await execa10(
4894
5256
  "docker",
4895
5257
  ["build", "-t", destTag, "-f", join8(scratch, "Dockerfile"), scratch],
4896
5258
  { reject: false }
@@ -4899,7 +5261,7 @@ async function flattenImage(sourceTag, destTag, log) {
4899
5261
  throw new CheckpointError(`flatten docker build failed`, build.stdout, build.stderr);
4900
5262
  }
4901
5263
  } finally {
4902
- await execa11("docker", ["rm", "-f", tmpName], { reject: false });
5264
+ await execa10("docker", ["rm", "-f", tmpName], { reject: false });
4903
5265
  await rm3(scratch, { recursive: true, force: true });
4904
5266
  }
4905
5267
  }
@@ -4943,7 +5305,7 @@ async function pathExists4(p) {
4943
5305
  }
4944
5306
  async function writeBoxEnvFile(container, env) {
4945
5307
  const body = formatBoxEnvBody(env);
4946
- const result = await execa12(
5308
+ const result = await execa11(
4947
5309
  "docker",
4948
5310
  ["exec", "--user", "root", "-i", container, "sh", "-c", "umask 022 && cat > /etc/agentbox/box.env"],
4949
5311
  { input: body, reject: false }
@@ -4967,7 +5329,7 @@ function shellSingleQuote(s) {
4967
5329
  return `'${s.replace(/'/g, `'\\''`)}'`;
4968
5330
  }
4969
5331
  async function ensureHomeOwnedByVscode(container) {
4970
- await execa13(
5332
+ await execa12(
4971
5333
  "docker",
4972
5334
  [
4973
5335
  "exec",
@@ -5004,25 +5366,72 @@ async function ensureRelay(opts = {}) {
5004
5366
  await removeContainer(RELAY_CONTAINER_NAME);
5005
5367
  log(`removed legacy relay container ${RELAY_CONTAINER_NAME}`);
5006
5368
  }
5007
- if (await pingHealthz(500)) {
5008
- return ENDPOINT;
5009
- }
5010
- const existingPid = await readPidFile();
5011
- if (existingPid !== null && await processAlive(existingPid)) {
5012
- for (let i = 0; i < 10; i++) {
5013
- if (await pingHealthz(300)) return ENDPOINT;
5014
- await delay2(200);
5369
+ const health = await fetchHealthz(500);
5370
+ if (health !== null) {
5371
+ if (health.cliEntry !== false) {
5372
+ return ENDPOINT;
5373
+ }
5374
+ log("relay is alive but lacks AGENTBOX_CLI_ENTRY (cp/download/checkpoint would fail) \u2014 reclaiming");
5375
+ await reclaimRelay(health.pid, log);
5376
+ } else {
5377
+ const existingPid = await readPidFile();
5378
+ if (existingPid !== null && await processAlive(existingPid)) {
5379
+ for (let i = 0; i < 10; i++) {
5380
+ if (await pingHealthz(300)) return ENDPOINT;
5381
+ await delay2(200);
5382
+ }
5383
+ log(`relay pid ${String(existingPid)} alive but /healthz unresponsive \u2014 proceeding anyway`);
5384
+ return ENDPOINT;
5385
+ }
5386
+ if (existingPid !== null) {
5387
+ await unlink2(PID_FILE).catch(() => {
5388
+ });
5015
5389
  }
5016
- log(`relay pid ${String(existingPid)} alive but /healthz unresponsive \u2014 proceeding anyway`);
5017
- return ENDPOINT;
5018
- }
5019
- if (existingPid !== null) {
5020
- await unlink(PID_FILE).catch(() => {
5021
- });
5022
5390
  }
5023
5391
  const relayBin = resolveRelayBin();
5024
- const logFd = openSync(LOG_FILE, "a");
5025
5392
  const cliEntry = resolveCliEntry();
5393
+ if (cliEntry === null) {
5394
+ throw new Error(
5395
+ "cannot start the host relay: agentbox CLI entry not found (is the build complete / dist present?). Set AGENTBOX_CLI_ENTRY to override."
5396
+ );
5397
+ }
5398
+ return spawnRelay(relayBin, cliEntry, log);
5399
+ }
5400
+ async function reclaimRelay(reportedPid, log) {
5401
+ const pidFromFile = await readPidFile();
5402
+ const seen = /* @__PURE__ */ new Set();
5403
+ for (const pid of [reportedPid, pidFromFile]) {
5404
+ if (typeof pid !== "number" || pid <= 0 || seen.has(pid)) continue;
5405
+ seen.add(pid);
5406
+ if (!await processAlive(pid)) continue;
5407
+ log(`stopping crippled relay pid ${String(pid)}`);
5408
+ await killPid(pid);
5409
+ }
5410
+ await unlink2(PID_FILE).catch(() => {
5411
+ });
5412
+ if (await pingHealthz(300)) {
5413
+ throw new Error(
5414
+ `a relay without AGENTBOX_CLI_ENTRY is still listening on :${String(PORT)} and could not be stopped (reported pid ${String(reportedPid ?? "unknown")}); kill it manually and retry`
5415
+ );
5416
+ }
5417
+ }
5418
+ async function killPid(pid) {
5419
+ try {
5420
+ process.kill(pid, "SIGTERM");
5421
+ } catch {
5422
+ return;
5423
+ }
5424
+ for (let i = 0; i < 20; i++) {
5425
+ if (!await processAlive(pid)) return;
5426
+ await delay2(100);
5427
+ }
5428
+ try {
5429
+ process.kill(pid, "SIGKILL");
5430
+ } catch {
5431
+ }
5432
+ }
5433
+ async function spawnRelay(relayBin, cliEntry, log) {
5434
+ const logFd = openSync(LOG_FILE, "a");
5026
5435
  const child = spawn(
5027
5436
  process.execPath,
5028
5437
  [relayBin, "serve", "--port", String(PORT), "--host", "0.0.0.0"],
@@ -5031,7 +5440,7 @@ async function ensureRelay(opts = {}) {
5031
5440
  stdio: ["ignore", logFd, logFd],
5032
5441
  env: {
5033
5442
  ...process.env,
5034
- ...cliEntry ? { AGENTBOX_CLI_ENTRY: cliEntry } : {}
5443
+ AGENTBOX_CLI_ENTRY: cliEntry
5035
5444
  }
5036
5445
  }
5037
5446
  );
@@ -5053,16 +5462,16 @@ async function ensureRelay(opts = {}) {
5053
5462
  }
5054
5463
  function resolveRelayBin() {
5055
5464
  const override = process.env.AGENTBOX_RELAY_BIN;
5056
- if (override && existsSync3(override)) return override;
5057
- const here2 = dirname22(fileURLToPath2(import.meta.url));
5465
+ if (override && existsSync2(override)) return override;
5466
+ const here = dirname3(fileURLToPath(import.meta.url));
5058
5467
  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")
5468
+ resolve22(here, "..", "runtime", "relay", "bin.cjs"),
5469
+ resolve22(here, "..", "..", "relay", "dist", "bin.cjs"),
5470
+ resolve22(here, "..", "..", "..", "@agentbox", "relay", "dist", "bin.cjs"),
5471
+ resolve22(here, "..", "..", "node_modules", "@agentbox", "relay", "dist", "bin.cjs")
5063
5472
  ];
5064
5473
  for (const c of candidates) {
5065
- if (existsSync3(c)) return c;
5474
+ if (existsSync2(c)) return c;
5066
5475
  }
5067
5476
  throw new Error(
5068
5477
  `could not locate @agentbox/relay bin; tried:
@@ -5071,17 +5480,17 @@ function resolveRelayBin() {
5071
5480
  }
5072
5481
  function resolveCliEntry() {
5073
5482
  const override = process.env.AGENTBOX_CLI_ENTRY;
5074
- if (override && existsSync3(override)) return override;
5075
- const here2 = dirname22(fileURLToPath2(import.meta.url));
5483
+ if (override && existsSync2(override)) return override;
5484
+ const here = dirname3(fileURLToPath(import.meta.url));
5076
5485
  const candidates = [
5077
5486
  // Bundled CLI (dev + published): this module IS bundled into the CLI
5078
5487
  // 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")
5488
+ resolve22(here, "index.js"),
5489
+ resolve22(here, "..", "..", "..", "apps", "cli", "dist", "index.js"),
5490
+ resolve22(here, "..", "..", "..", "..", "dist", "index.js")
5082
5491
  ];
5083
5492
  for (const c of candidates) {
5084
- if (existsSync3(c)) return c;
5493
+ if (existsSync2(c)) return c;
5085
5494
  }
5086
5495
  return null;
5087
5496
  }
@@ -5091,7 +5500,7 @@ async function stopRelay() {
5091
5500
  return { stopped: false, pid: null };
5092
5501
  }
5093
5502
  if (!await processAlive(pid)) {
5094
- await unlink(PID_FILE).catch(() => {
5503
+ await unlink2(PID_FILE).catch(() => {
5095
5504
  });
5096
5505
  return { stopped: false, pid };
5097
5506
  }
@@ -5109,7 +5518,7 @@ async function stopRelay() {
5109
5518
  } catch {
5110
5519
  }
5111
5520
  }
5112
- await unlink(PID_FILE).catch(() => {
5521
+ await unlink2(PID_FILE).catch(() => {
5113
5522
  });
5114
5523
  return { stopped: true, pid };
5115
5524
  }
@@ -5163,7 +5572,13 @@ function fetchHealthz(timeoutMs) {
5163
5572
  try {
5164
5573
  const parsed = JSON.parse(Buffer.concat(chunks).toString("utf8"));
5165
5574
  if (typeof parsed.ok === "boolean" && typeof parsed.boxes === "number" && typeof parsed.events === "number") {
5166
- resolveP({ ok: parsed.ok, boxes: parsed.boxes, events: parsed.events });
5575
+ resolveP({
5576
+ ok: parsed.ok,
5577
+ boxes: parsed.boxes,
5578
+ events: parsed.events,
5579
+ pid: typeof parsed.pid === "number" ? parsed.pid : void 0,
5580
+ cliEntry: typeof parsed.cliEntry === "boolean" ? parsed.cliEntry : void 0
5581
+ });
5167
5582
  } else {
5168
5583
  resolveP(null);
5169
5584
  }
@@ -5200,7 +5615,7 @@ async function processAlive(pid) {
5200
5615
  }
5201
5616
  }
5202
5617
  function generateRelayToken() {
5203
- return randomBytes2(32).toString("hex");
5618
+ return randomBytes22(32).toString("hex");
5204
5619
  }
5205
5620
  async function registerBoxWithRelay(args) {
5206
5621
  const worktrees = (args.worktrees ?? []).map((w) => ({
@@ -5249,6 +5664,20 @@ async function clearRelayNotice(boxId, id) {
5249
5664
  } catch {
5250
5665
  }
5251
5666
  }
5667
+ async function mintHostInitiatedToken(boxId, method, paramsHash, ttlMs) {
5668
+ try {
5669
+ const body = await adminPostForJson("/admin/host-initiated/mint", {
5670
+ boxId,
5671
+ method,
5672
+ paramsHash,
5673
+ ...typeof ttlMs === "number" ? { ttlMs } : {}
5674
+ });
5675
+ const token = body?.token;
5676
+ return typeof token === "string" && token.length > 0 ? token : null;
5677
+ } catch {
5678
+ return null;
5679
+ }
5680
+ }
5252
5681
  async function adminPost(path, body) {
5253
5682
  const json = JSON.stringify(body);
5254
5683
  await new Promise((resolveP, rejectP) => {
@@ -5501,7 +5930,7 @@ function generateBoxId() {
5501
5930
  return randomBytes3(4).toString("hex");
5502
5931
  }
5503
5932
  function sanitizeBasename(workspacePath) {
5504
- const raw = basename2(resolve4(workspacePath));
5933
+ const raw = basename2(resolve3(workspacePath));
5505
5934
  return raw.toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/-+/g, "-").replace(/^[-._]+|[-._]+$/g, "").slice(0, 30).replace(/[-._]+$/, "");
5506
5935
  }
5507
5936
  function defaultBoxName(workspacePath, id) {
@@ -5532,7 +5961,7 @@ async function buildIdentityMounts() {
5532
5961
  async function createBox(opts) {
5533
5962
  const log = opts.onLog ?? (() => {
5534
5963
  });
5535
- const workspace = resolve4(opts.workspacePath);
5964
+ const workspace = resolve3(opts.workspacePath);
5536
5965
  if (!await pathExists5(workspace)) {
5537
5966
  throw new Error(`workspace does not exist: ${workspace}`);
5538
5967
  }
@@ -5567,6 +5996,27 @@ async function createBox(opts) {
5567
5996
  log(
5568
5997
  `starting from checkpoint ${opts.checkpointRef} (${head.manifest.type}, ${String(chain.length)} layer(s), image ${head.manifest.image})`
5569
5998
  );
5999
+ const ckptFingerprint = head.manifest.baseFingerprint;
6000
+ const ckptCliVersion = head.manifest.cliVersion ?? "unknown";
6001
+ if (head.manifest.schema === 2) {
6002
+ log(
6003
+ `WARNING: checkpoint '${opts.checkpointRef}' was captured before checkpoint versioning landed.
6004
+ Its base-image layers may be older than your current base image. If the box is missing
6005
+ expected updates, remove the checkpoint with \`agentbox checkpoint rm ${opts.checkpointRef}\` and recreate it.`
6006
+ );
6007
+ } else {
6008
+ const prepared = readPreparedDockerState();
6009
+ const currentFingerprint = prepared?.base?.contextSha256 ?? (await computeDockerContextFingerprint())?.contextSha256;
6010
+ if (ckptFingerprint && currentFingerprint && ckptFingerprint !== currentFingerprint) {
6011
+ log(
6012
+ `WARNING: checkpoint '${opts.checkpointRef}' was captured against an older base image.
6013
+ captured: cli ${ckptCliVersion}, fingerprint ${ckptFingerprint.slice(0, 12)}
6014
+ current : cli ${prepared?.base?.cliVersion ?? "unknown"}, fingerprint ${currentFingerprint.slice(0, 12)}
6015
+ The restored box will keep the old base layers and will NOT include base-image updates.
6016
+ To pick up updates: \`agentbox checkpoint rm ${opts.checkpointRef}\` and recreate from a fresh box.`
6017
+ );
6018
+ }
6019
+ }
5570
6020
  }
5571
6021
  const imageRef = checkpointImage ?? opts.image ?? DEFAULT_BOX_IMAGE;
5572
6022
  const ensureRef = checkpointImage ? opts.image ?? DEFAULT_BOX_IMAGE : imageRef;
@@ -5607,9 +6057,33 @@ async function createBox(opts) {
5607
6057
  );
5608
6058
  }
5609
6059
  for (const r of repos) {
6060
+ const containerPath = r.kind === "root" ? "/workspace" : `/workspace/${r.relPathFromWorkspace}`;
6061
+ const reuseBranch = r.kind === "root" && opts.useBranch !== void 0;
6062
+ if (reuseBranch) {
6063
+ const branch2 = opts.useBranch;
6064
+ const gitWorktreePath2 = gitWorktreePathFor(branch2);
6065
+ repoCarryOvers.push({
6066
+ repo: r,
6067
+ containerPath,
6068
+ gitWorktreePath: gitWorktreePath2,
6069
+ branch: branch2,
6070
+ stashSha: null,
6071
+ untrackedNul: "",
6072
+ hostSource: r.hostMainRepo,
6073
+ reuseBranch: true
6074
+ });
6075
+ gitWorktreeRecords.push({
6076
+ kind: r.kind,
6077
+ hostMainRepo: r.hostMainRepo,
6078
+ containerPath,
6079
+ gitWorktreePath: gitWorktreePath2,
6080
+ branch: branch2,
6081
+ relPathFromWorkspace: r.relPathFromWorkspace
6082
+ });
6083
+ continue;
6084
+ }
5610
6085
  const branchBase = r.kind === "root" ? `agentbox/${name}` : `agentbox/${name}--${r.relPathFromWorkspace.replace(/[^A-Za-z0-9._-]+/g, "_")}`;
5611
6086
  const branch = await pickFreshBranch(r.hostMainRepo, branchBase);
5612
- const containerPath = r.kind === "root" ? "/workspace" : `/workspace/${r.relPathFromWorkspace}`;
5613
6087
  const gitWorktreePath = gitWorktreePathFor(branch);
5614
6088
  const carry = await collectRepoCarryOver(r, branch, containerPath, gitWorktreePath);
5615
6089
  repoCarryOvers.push(carry);
@@ -5716,6 +6190,8 @@ async function createBox(opts) {
5716
6190
  if (opencodeEnsured.synced) log(`synced ${opencodeSpec.volume} from ~/.config + ~/.local/share opencode`);
5717
6191
  else if (opencodeEnsured.created) log(`created empty volume ${opencodeSpec.volume} (no host opencode)`);
5718
6192
  else log(`reusing volume ${opencodeSpec.volume}`);
6193
+ const opencodePlugin = await seedOpencodePlugin(opencodeSpec.volume, ensureRef);
6194
+ if (opencodePlugin.seeded) log(`seeded agentbox-state plugin into ${opencodeSpec.volume}`);
5719
6195
  opencodeMounts = buildOpencodeMounts(opencodeSpec, process.env);
5720
6196
  opencodeConfigVolume = opencodeSpec.volume;
5721
6197
  }
@@ -5771,10 +6247,15 @@ async function createBox(opts) {
5771
6247
  }
5772
6248
  }
5773
6249
  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
6250
+ // The in-box ctl client always talks to its own in-box relay/forwarder
6251
+ // on AGENTBOX_BOX_RELAY_PORT (default 8788). For docker boxes the
6252
+ // forwarder transparently proxies to the host relay at
6253
+ // host.docker.internal:8787 (the matching `--add-host` is set in
6254
+ // runBox). This keeps :8787 inside the box free for a nested
6255
+ // agentbox to claim its own host relay.
6256
+ AGENTBOX_RELAY_URL: `http://127.0.0.1:8788`,
6257
+ AGENTBOX_RELAY_TOKEN: relayToken,
6258
+ AGENTBOX_HOST_RELAY_URL: `http://host.docker.internal:8787`
5778
6259
  } : {};
5779
6260
  const vncEnabled = opts.vnc?.enabled !== false;
5780
6261
  const vncPassword = vncEnabled ? generateVncPassword() : void 0;
@@ -5839,12 +6320,26 @@ async function createBox(opts) {
5839
6320
  if (!checkpointImage) {
5840
6321
  if (repoCarryOvers.length > 0) {
5841
6322
  try {
5842
- await seedWorkspace({ container: containerName, repos: repoCarryOvers, onLog: log });
6323
+ await seedWorkspace({
6324
+ container: containerName,
6325
+ repos: repoCarryOvers,
6326
+ fromBranch: opts.fromBranch,
6327
+ onLog: log
6328
+ });
5843
6329
  log("seeded /workspace from in-container git worktree(s)");
5844
6330
  } catch (err) {
5845
- log(
5846
- `seedWorkspace failed; leaving ${containerName} running so you can inspect it`
5847
- );
6331
+ if (opts.useBranch !== void 0) {
6332
+ log(`seedWorkspace failed for --use-branch ${opts.useBranch}; cleaning up the box`);
6333
+ await execa13("docker", ["rm", "-f", containerName], { reject: false });
6334
+ for (const w of gitWorktreeRecords) {
6335
+ await removeInBoxWorktree({
6336
+ hostMainRepo: w.hostMainRepo,
6337
+ gitWorktreePath: w.gitWorktreePath
6338
+ });
6339
+ }
6340
+ } else {
6341
+ log(`seedWorkspace failed; leaving ${containerName} running so you can inspect it`);
6342
+ }
5848
6343
  throw err;
5849
6344
  }
5850
6345
  } else {
@@ -5878,7 +6373,7 @@ async function createBox(opts) {
5878
6373
  }
5879
6374
  if (opts.withPlaywright) {
5880
6375
  log("installing @playwright/cli@latest (--with-playwright)");
5881
- const result = await execa14(
6376
+ const result = await execa13(
5882
6377
  "docker",
5883
6378
  [
5884
6379
  "exec",
@@ -5923,6 +6418,20 @@ async function createBox(opts) {
5923
6418
  log(`copied ${String(copied)}/${String(opts.envFilesToImport.length)} selected env/config file(s)`);
5924
6419
  }
5925
6420
  }
6421
+ let carrySummary;
6422
+ if (opts.carry && opts.carry.length > 0) {
6423
+ log(`carry: copying ${String(opts.carry.length)} host path(s) into the box`);
6424
+ const result = await copyCarryPathsToBox({
6425
+ container: containerName,
6426
+ entries: opts.carry,
6427
+ onLog: log
6428
+ });
6429
+ log(`carry: copied ${String(result.copied)}/${String(opts.carry.length)} entry/entries`);
6430
+ for (const err of result.errors) log(`carry: ${err}`);
6431
+ if (result.applied.length > 0) {
6432
+ carrySummary = { count: result.applied.length, entries: result.applied };
6433
+ }
6434
+ }
5926
6435
  let vncHostPort = null;
5927
6436
  if (vncEnabled) {
5928
6437
  const vnc = await launchVncDaemon(containerName);
@@ -5939,7 +6448,9 @@ async function createBox(opts) {
5939
6448
  }
5940
6449
  let portlessAliasName;
5941
6450
  let portlessUrl;
5942
- if (opts.portless === true && webHostPort) {
6451
+ let portlessVncAliasName;
6452
+ let portlessVncUrl;
6453
+ if (opts.portless === true && (webHostPort || vncEnabled && vncHostPort)) {
5943
6454
  try {
5944
6455
  const engine = await detectEngine();
5945
6456
  if (engine === "orbstack") {
@@ -5948,15 +6459,29 @@ async function createBox(opts) {
5948
6459
  const portless = await detectPortless();
5949
6460
  if (!portless.installed) {
5950
6461
  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) {
6462
+ } else {
6463
+ if (webHostPort) {
6464
+ if (await portlessAlias(name, webHostPort)) {
6465
+ portlessAliasName = name;
6466
+ portlessUrl = await portlessGetUrl(name);
6467
+ log(`portless alias ${portlessUrl} -> 127.0.0.1:${String(webHostPort)}`);
6468
+ } else {
6469
+ log("portless alias failed (best-effort) \u2014 box still reachable on the loopback URL");
6470
+ }
6471
+ }
6472
+ if (vncEnabled && vncHostPort) {
6473
+ const vncAlias = `vnc-${name}`;
6474
+ if (await portlessAlias(vncAlias, vncHostPort)) {
6475
+ portlessVncAliasName = vncAlias;
6476
+ portlessVncUrl = await portlessGetUrl(vncAlias);
6477
+ log(`portless alias ${portlessVncUrl} -> 127.0.0.1:${String(vncHostPort)}`);
6478
+ } else {
6479
+ log("portless vnc alias failed (best-effort) \u2014 VNC still reachable on the loopback URL");
6480
+ }
6481
+ }
6482
+ if (!portless.proxyRunning && (portlessAliasName || portlessVncAliasName)) {
5956
6483
  log(`portless proxy not running \u2014 start it with \`${portlessStartHint()}\``);
5957
6484
  }
5958
- } else {
5959
- log("portless alias failed (best-effort) \u2014 box still reachable on the loopback URL");
5960
6485
  }
5961
6486
  }
5962
6487
  } catch (err) {
@@ -5980,6 +6505,7 @@ async function createBox(opts) {
5980
6505
  gitWorktrees: gitWorktreeRecords.length > 0 ? gitWorktreeRecords : void 0,
5981
6506
  withPlaywright: opts.withPlaywright ? true : void 0,
5982
6507
  withEnv: opts.withEnv ? true : void 0,
6508
+ carry: carrySummary,
5983
6509
  vncEnabled: vncEnabled ? true : void 0,
5984
6510
  vncContainerPort: vncEnabled ? VNC_CONTAINER_PORT : void 0,
5985
6511
  vncHostPort: vncHostPort ?? void 0,
@@ -5988,6 +6514,8 @@ async function createBox(opts) {
5988
6514
  webHostPort: webHostPort ?? void 0,
5989
6515
  portlessAlias: portlessAliasName,
5990
6516
  portlessUrl,
6517
+ portlessVncAlias: portlessVncAliasName,
6518
+ portlessVncUrl,
5991
6519
  dockerVolume,
5992
6520
  dockerCacheShared: dockerCacheShared || void 0,
5993
6521
  projectRoot: opts.projectRoot,
@@ -6046,7 +6574,7 @@ function parseShellSessionList(stdout) {
6046
6574
  return out;
6047
6575
  }
6048
6576
  async function listShellSessions(container, user) {
6049
- const res = await execa15(
6577
+ const res = await execa14(
6050
6578
  "docker",
6051
6579
  [
6052
6580
  "exec",
@@ -6070,7 +6598,7 @@ async function startShellSession(opts) {
6070
6598
  const login = opts.login !== false;
6071
6599
  const term = process.env["TERM"] ?? "xterm-256color";
6072
6600
  const cmd = login ? "bash -l" : "bash";
6073
- const result = await execa15(
6601
+ const result = await execa14(
6074
6602
  "docker",
6075
6603
  [
6076
6604
  "exec",
@@ -6119,7 +6647,7 @@ function buildShellSessionAttachArgv(container, sessionName, user) {
6119
6647
  }
6120
6648
  async function shellSessionInfo(container, sessionName, user) {
6121
6649
  const name = sessionName ?? DEFAULT_SHELL_SESSION;
6122
- const has = await execa15(
6650
+ const has = await execa14(
6123
6651
  "docker",
6124
6652
  ["exec", "--user", user ?? CONTAINER_USER, container, "tmux", "has-session", "-t", name],
6125
6653
  { reject: false }
@@ -6127,7 +6655,7 @@ async function shellSessionInfo(container, sessionName, user) {
6127
6655
  return { running: has.exitCode === 0, sessionName: name };
6128
6656
  }
6129
6657
  async function killShellSession(container, sessionName, user) {
6130
- const res = await execa15(
6658
+ const res = await execa14(
6131
6659
  "docker",
6132
6660
  [
6133
6661
  "exec",
@@ -6149,7 +6677,7 @@ async function getBoxEndpoints(record, engine, persisted) {
6149
6677
  const endpoints = [];
6150
6678
  if (record.vncEnabled && record.vncPassword) {
6151
6679
  const vncUrls = buildVncUrls(record, engine);
6152
- const url = vncUrls.orbUrl ?? vncUrls.loopbackUrl;
6680
+ const url = engine === "orbstack" ? vncUrls.orbUrl ?? vncUrls.loopbackUrl : vncUrls.portlessUrl ?? vncUrls.loopbackUrl;
6153
6681
  endpoints.push({
6154
6682
  kind: "vnc",
6155
6683
  name: "vnc",
@@ -6216,18 +6744,30 @@ async function listBoxes() {
6216
6744
  const portlessWebUrl = b.portlessAlias !== void 0 ? b.portlessUrl ?? `https://${b.portlessAlias}.localhost` : void 0;
6217
6745
  const cachedWebUrl = webPort > 0 ? b.cloud?.previewUrls?.[webPort] : void 0;
6218
6746
  const webUrl = portlessWebUrl ?? cachedWebUrl;
6747
+ const portlessVncBase = b.portlessVncAlias !== void 0 ? b.portlessVncUrl ?? `https://${b.portlessVncAlias}.localhost` : void 0;
6748
+ const vncUrl = portlessVncBase && b.vncPassword ? `${portlessVncBase}/vnc.html?autoconnect=1&password=${encodeURIComponent(b.vncPassword)}` : void 0;
6749
+ const cloudEndpoints = [];
6750
+ if (webUrl) {
6751
+ cloudEndpoints.push({
6752
+ kind: "web",
6753
+ name: "web",
6754
+ containerPort: webPort,
6755
+ url: webUrl,
6756
+ reachable: true
6757
+ });
6758
+ }
6759
+ if (b.vncEnabled && b.vncPassword) {
6760
+ cloudEndpoints.push({
6761
+ kind: "vnc",
6762
+ name: "vnc",
6763
+ containerPort: b.vncContainerPort ?? 6080,
6764
+ ...vncUrl ? { url: vncUrl, reachable: true } : { reachable: false }
6765
+ });
6766
+ }
6219
6767
  const endpoints2 = {
6220
6768
  domain: webUrl ? safeHost(webUrl) : "",
6221
6769
  domainIsOrb: false,
6222
- endpoints: webUrl ? [
6223
- {
6224
- kind: "web",
6225
- name: "web",
6226
- containerPort: webPort,
6227
- url: webUrl,
6228
- reachable: true
6229
- }
6230
- ] : []
6770
+ endpoints: cloudEndpoints
6231
6771
  };
6232
6772
  return {
6233
6773
  ...b,
@@ -6351,19 +6891,31 @@ async function startBox(idOrName) {
6351
6891
  box.webHostPort = freshWebPort;
6352
6892
  await recordBox(box);
6353
6893
  }
6354
- if (box.portlessAlias && box.webHostPort) {
6355
- try {
6356
- const portless = await detectPortless();
6357
- if (portless.installed) {
6894
+ }
6895
+ if (box.portlessAlias && box.webHostPort || box.portlessVncAlias && box.vncHostPort) {
6896
+ try {
6897
+ const portless = await detectPortless();
6898
+ if (portless.installed) {
6899
+ let dirty = false;
6900
+ if (box.portlessAlias && box.webHostPort) {
6358
6901
  await portlessAlias(box.portlessAlias, box.webHostPort);
6359
6902
  const url = await portlessGetUrl(box.portlessAlias);
6360
6903
  if (url !== box.portlessUrl) {
6361
6904
  box.portlessUrl = url;
6362
- await recordBox(box);
6905
+ dirty = true;
6363
6906
  }
6364
6907
  }
6365
- } catch {
6908
+ if (box.portlessVncAlias && box.vncHostPort) {
6909
+ await portlessAlias(box.portlessVncAlias, box.vncHostPort);
6910
+ const url = await portlessGetUrl(box.portlessVncAlias);
6911
+ if (url !== box.portlessVncUrl) {
6912
+ box.portlessVncUrl = url;
6913
+ dirty = true;
6914
+ }
6915
+ }
6916
+ if (dirty) await recordBox(box);
6366
6917
  }
6918
+ } catch {
6367
6919
  }
6368
6920
  }
6369
6921
  if (box.relayToken) {
@@ -6395,7 +6947,7 @@ async function getBoxHostPaths(idOrName) {
6395
6947
  }
6396
6948
  async function dirSizeBytes(path) {
6397
6949
  try {
6398
- const result = await execa16("du", ["-sk", path], { reject: false });
6950
+ const result = await execa15("du", ["-sk", path], { reject: false });
6399
6951
  if (result.exitCode !== 0) return null;
6400
6952
  const sizeKb = Number.parseInt((result.stdout ?? "").split(/\s+/)[0] ?? "", 10);
6401
6953
  if (Number.isNaN(sizeKb)) return null;
@@ -6463,6 +7015,12 @@ async function destroyBox(idOrName, opts = {}) {
6463
7015
  } catch {
6464
7016
  }
6465
7017
  }
7018
+ if (box.portlessVncAlias) {
7019
+ try {
7020
+ await portlessUnalias(box.portlessVncAlias);
7021
+ } catch {
7022
+ }
7023
+ }
6466
7024
  const ownsWorktrees = !box.checkpointImage;
6467
7025
  if (ownsWorktrees) {
6468
7026
  for (const w of box.gitWorktrees ?? []) {
@@ -6537,7 +7095,7 @@ async function listBoxDirs() {
6537
7095
  }
6538
7096
  }
6539
7097
  async function listCheckpointImageTags() {
6540
- const r = await execa16(
7098
+ const r = await execa15(
6541
7099
  "docker",
6542
7100
  ["image", "ls", "--format", "{{.Repository}}:{{.Tag}}", `${CHECKPOINT_IMAGE_PREFIX}*`],
6543
7101
  { reject: false }
@@ -6645,7 +7203,7 @@ async function pruneBoxes(opts = {}) {
6645
7203
  } catch {
6646
7204
  }
6647
7205
  try {
6648
- await execa16("docker", ["image", "rm", RELAY_IMAGE_REF], { reject: false });
7206
+ await execa15("docker", ["image", "rm", RELAY_IMAGE_REF], { reject: false });
6649
7207
  } catch {
6650
7208
  }
6651
7209
  try {
@@ -6707,7 +7265,7 @@ function splitPair(raw) {
6707
7265
  return [parts[0].trim(), parts[1].trim()];
6708
7266
  }
6709
7267
  async function duBytes(path) {
6710
- const result = await execa17("du", ["-sk", path], { reject: false });
7268
+ const result = await execa16("du", ["-sk", path], { reject: false });
6711
7269
  if (result.exitCode !== 0) return null;
6712
7270
  const kb = Number.parseInt((result.stdout ?? "").split(/\s+/)[0] ?? "", 10);
6713
7271
  return Number.isNaN(kb) ? null : kb * 1024;
@@ -6720,7 +7278,7 @@ async function volumeSizeBytes(name) {
6720
7278
  const sz = await duBytes(live);
6721
7279
  if (sz !== null) return sz;
6722
7280
  }
6723
- const df = await execa17(
7281
+ const df = await execa16(
6724
7282
  "docker",
6725
7283
  ["system", "df", "-v", "--format", "{{json .Volumes}}"],
6726
7284
  { reject: false }
@@ -6741,7 +7299,7 @@ async function volumeSizeBytes(name) {
6741
7299
  return null;
6742
7300
  }
6743
7301
  async function imageBytes(tag) {
6744
- const r = await execa17("docker", ["image", "inspect", tag, "--format", "{{.Size}}"], {
7302
+ const r = await execa16("docker", ["image", "inspect", tag, "--format", "{{.Size}}"], {
6745
7303
  reject: false
6746
7304
  });
6747
7305
  if (r.exitCode !== 0) return null;
@@ -6752,7 +7310,7 @@ async function projectCheckpointImageBytes(projectRoot, name) {
6752
7310
  return imageBytes(checkpointImageTag(projectRoot, name));
6753
7311
  }
6754
7312
  async function allCheckpointImagesBytes() {
6755
- const r = await execa17(
7313
+ const r = await execa16(
6756
7314
  "docker",
6757
7315
  [
6758
7316
  "image",
@@ -6804,7 +7362,7 @@ function reconcileLimits(persisted, dockerJson) {
6804
7362
  };
6805
7363
  }
6806
7364
  async function containerWritableBytes(container) {
6807
- const r = await execa17(
7365
+ const r = await execa16(
6808
7366
  "docker",
6809
7367
  ["ps", "-a", "--filter", `name=^${container}$`, "--format", "{{.Size}}", "--size"],
6810
7368
  { reject: false }
@@ -6851,7 +7409,7 @@ async function boxResourceStats(record) {
6851
7409
  if (await inspectContainerStatus(record.container) !== "running") {
6852
7410
  return base;
6853
7411
  }
6854
- const proc = await execa17(
7412
+ const proc = await execa16(
6855
7413
  "docker",
6856
7414
  ["stats", "--no-stream", "--format", "{{json .}}", record.container],
6857
7415
  { reject: false }
@@ -6886,6 +7444,125 @@ async function boxResourceStats(record) {
6886
7444
  blockWriteBytes: blkPair ? parseDockerSize(blkPair[1]) : null
6887
7445
  };
6888
7446
  }
7447
+ function posixDirname(p) {
7448
+ return posix.dirname(p) || "/";
7449
+ }
7450
+ function asText(s) {
7451
+ if (s === void 0) return "";
7452
+ if (typeof s === "string") return s;
7453
+ return Buffer.from(s).toString("utf8");
7454
+ }
7455
+ async function uploadToBox(box, hostSrc, boxDst) {
7456
+ const srcAbs = resolve4(hostSrc);
7457
+ if (!existsSync3(srcAbs)) throw new Error(`source not found: ${hostSrc}`);
7458
+ const srcBasename = basename32(srcAbs);
7459
+ const srcParent = dirname22(srcAbs);
7460
+ let boxParent;
7461
+ let finalName;
7462
+ if (boxDst.endsWith("/")) {
7463
+ boxParent = boxDst.replace(/\/+$/, "") || "/";
7464
+ finalName = srcBasename;
7465
+ } else {
7466
+ const isDir2 = await execa17(
7467
+ "docker",
7468
+ ["exec", box.container, "test", "-d", boxDst],
7469
+ { reject: false }
7470
+ );
7471
+ if (isDir2.exitCode === 0) {
7472
+ boxParent = boxDst.replace(/\/+$/, "") || "/";
7473
+ finalName = srcBasename;
7474
+ } else {
7475
+ boxParent = posixDirname(boxDst);
7476
+ finalName = posix.basename(boxDst);
7477
+ }
7478
+ }
7479
+ const finalPath = boxParent === "/" ? `/${finalName}` : `${boxParent}/${finalName}`;
7480
+ const mk = await execa17(
7481
+ "docker",
7482
+ ["exec", "--user", "root", box.container, "mkdir", "-p", boxParent],
7483
+ { reject: false }
7484
+ );
7485
+ if (mk.exitCode !== 0) {
7486
+ throw new Error(`mkdir -p ${boxParent} in box failed: ${asText(mk.stderr).slice(0, 300)}`);
7487
+ }
7488
+ const packed = await execa17("tar", ["-C", srcParent, "-cf", "-", srcBasename], {
7489
+ encoding: "buffer",
7490
+ reject: false,
7491
+ env: { ...process.env, COPYFILE_DISABLE: "1" }
7492
+ });
7493
+ if (packed.exitCode !== 0) {
7494
+ throw new Error(`tar pack failed: ${asText(packed.stderr).slice(0, 300)}`);
7495
+ }
7496
+ const extract = await execa17(
7497
+ "docker",
7498
+ ["exec", "-i", "--user", "root", box.container, "tar", "-xf", "-", "-C", boxParent],
7499
+ { input: packed.stdout, reject: false }
7500
+ );
7501
+ if (extract.exitCode !== 0) {
7502
+ throw new Error(`tar extract in box failed: ${asText(extract.stderr).slice(0, 300)}`);
7503
+ }
7504
+ if (finalName !== srcBasename) {
7505
+ const initial = boxParent === "/" ? `/${srcBasename}` : `${boxParent}/${srcBasename}`;
7506
+ const mv = await execa17(
7507
+ "docker",
7508
+ ["exec", "--user", "root", box.container, "mv", initial, finalPath],
7509
+ { reject: false }
7510
+ );
7511
+ if (mv.exitCode !== 0) {
7512
+ throw new Error(
7513
+ `rename ${initial} -> ${finalPath} in box failed: ${asText(mv.stderr).slice(0, 300)}`
7514
+ );
7515
+ }
7516
+ }
7517
+ const chown = await execa17(
7518
+ "docker",
7519
+ ["exec", "--user", "root", box.container, "chown", "-R", "1000:1000", finalPath],
7520
+ { reject: false }
7521
+ );
7522
+ if (chown.exitCode !== 0) {
7523
+ return {
7524
+ finalPath,
7525
+ warn: `chown ${finalPath} to vscode (uid 1000) failed; ownership inside the box may be root.`
7526
+ };
7527
+ }
7528
+ return { finalPath };
7529
+ }
7530
+ async function downloadFromBox(box, boxSrc, hostDst) {
7531
+ const srcBasename = posix.basename(boxSrc);
7532
+ const srcParent = posixDirname(boxSrc);
7533
+ const dstAbs = resolve4(hostDst);
7534
+ let hostParent;
7535
+ let finalName;
7536
+ const dstExists = existsSync3(dstAbs);
7537
+ if (hostDst.endsWith("/") || dstExists && statSync(dstAbs).isDirectory()) {
7538
+ hostParent = dstAbs;
7539
+ finalName = srcBasename;
7540
+ } else {
7541
+ hostParent = dirname22(dstAbs);
7542
+ finalName = basename32(dstAbs);
7543
+ }
7544
+ mkdirSync(hostParent, { recursive: true });
7545
+ const finalPath = posix.join(hostParent, finalName);
7546
+ const packed = await execa17(
7547
+ "docker",
7548
+ ["exec", box.container, "tar", "-C", srcParent, "-cf", "-", srcBasename],
7549
+ { encoding: "buffer", reject: false }
7550
+ );
7551
+ if (packed.exitCode !== 0) {
7552
+ throw new Error(`tar pack in box failed: ${asText(packed.stderr).slice(0, 300)}`);
7553
+ }
7554
+ const extract = await execa17("tar", ["-xf", "-", "-C", hostParent], {
7555
+ input: packed.stdout,
7556
+ reject: false
7557
+ });
7558
+ if (extract.exitCode !== 0) {
7559
+ throw new Error(`tar extract on host failed: ${asText(extract.stderr).slice(0, 300)}`);
7560
+ }
7561
+ if (finalName !== srcBasename) {
7562
+ renameSync(posix.join(hostParent, srcBasename), finalPath);
7563
+ }
7564
+ return { finalPath };
7565
+ }
6889
7566
  var dockerProvider = {
6890
7567
  name: "docker",
6891
7568
  async create(req) {
@@ -6895,6 +7572,8 @@ var dockerProvider = {
6895
7572
  name: req.name,
6896
7573
  useSnapshot: po.useSnapshot ?? false,
6897
7574
  checkpointRef: req.checkpointRef,
7575
+ fromBranch: req.fromBranch,
7576
+ useBranch: req.useBranch,
6898
7577
  image: req.image,
6899
7578
  onLog: req.onLog,
6900
7579
  claudeConfig: po.claudeConfig,
@@ -6904,6 +7583,7 @@ var dockerProvider = {
6904
7583
  withPlaywright: req.withPlaywright,
6905
7584
  withEnv: req.withEnv,
6906
7585
  envFilesToImport: req.envFilesToImport,
7586
+ carry: req.carry,
6907
7587
  vnc: req.vnc,
6908
7588
  docker: po.sharedCache !== void 0 ? { sharedCache: po.sharedCache } : void 0,
6909
7589
  portless: po.portless,
@@ -6954,6 +7634,14 @@ var dockerProvider = {
6954
7634
  const r = await execInBox(box.container, argv, opts?.user ? { user: opts.user } : {});
6955
7635
  return { exitCode: r.exitCode, stdout: r.stdout, stderr: r.stderr };
6956
7636
  },
7637
+ async uploadPath(box, hostSrc, boxDst) {
7638
+ const r = await uploadToBox(box, hostSrc, boxDst);
7639
+ return { finalPath: r.finalPath };
7640
+ },
7641
+ async downloadPath(box, boxSrc, hostDst) {
7642
+ const r = await downloadFromBox(box, boxSrc, hostDst);
7643
+ return { finalPath: r.finalPath };
7644
+ },
6957
7645
  async resolveUrl(box, opts) {
6958
7646
  if (box.webContainerPort === void 0) {
6959
7647
  throw new Error(
@@ -6976,13 +7664,33 @@ var dockerProvider = {
6976
7664
  },
6977
7665
  async prepare(opts) {
6978
7666
  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 {};
7667
+ const fingerprint = await computeDockerContextFingerprint();
7668
+ const prepared = readPreparedDockerState();
7669
+ if (!opts.force) {
7670
+ const exists = await imageExists(ref);
7671
+ if (exists && fingerprint && preparedMatches(prepared, fingerprint.contextSha256)) {
7672
+ opts.onLog?.(
7673
+ `docker image ${ref} up to date (fingerprint ${fingerprint.contextSha256.slice(0, 12)}) \u2014 skipping (use --force to rebuild)`
7674
+ );
7675
+ return {};
7676
+ }
7677
+ if (exists && !fingerprint) {
7678
+ opts.onLog?.(
7679
+ `docker image ${ref} present but build context could not be fingerprinted \u2014 skipping (use --force to rebuild)`
7680
+ );
7681
+ return {};
7682
+ }
6982
7683
  }
6983
7684
  opts.onLog?.(`building docker image ${ref}\u2026`);
6984
7685
  await buildImage({ ref, onProgress: opts.onLog });
6985
- opts.onLog?.(`docker image ${ref} built`);
7686
+ if (fingerprint) {
7687
+ writePreparedDockerState({ imageRef: ref, contextSha256: fingerprint.contextSha256 });
7688
+ opts.onLog?.(
7689
+ `docker image ${ref} built; recorded fingerprint ${fingerprint.contextSha256.slice(0, 12)}`
7690
+ );
7691
+ } else {
7692
+ opts.onLog?.(`docker image ${ref} built (fingerprint unavailable, prepared state not written)`);
7693
+ }
6986
7694
  return {};
6987
7695
  }
6988
7696
  };
@@ -7028,7 +7736,7 @@ function emptyResult(warnings = []) {
7028
7736
  }, warnings };
7029
7737
  }
7030
7738
  async function tarballFromDir(stageDir, agent) {
7031
- const tarballPath = join14(tmpdir3(), `agentbox-${agent}-${basename32(stageDir)}.tar.gz`);
7739
+ const tarballPath = join14(tmpdir3(), `agentbox-${agent}-${basename4(stageDir)}.tar.gz`);
7032
7740
  await execa18("tar", ["-czf", tarballPath, "-C", stageDir, "."], {
7033
7741
  env: { ...process.env, COPYFILE_DISABLE: "1" }
7034
7742
  });
@@ -7222,6 +7930,9 @@ async function stageCodexStaticForUpload(opts = {}) {
7222
7930
  }
7223
7931
  }
7224
7932
  async function stageCodexCredentialsForUpload(opts = {}) {
7933
+ if (await pathExists7(CODEX_CREDENTIALS_BACKUP_FILE)) {
7934
+ return stageSingleFileTarball("codex-creds", CODEX_CREDENTIALS_BACKUP_FILE, "auth.json");
7935
+ }
7225
7936
  const hostHome = opts.hostHome ?? homedir11();
7226
7937
  const hostAuth = join14(hostHome, ".codex", "auth.json");
7227
7938
  if (!await pathExists7(hostAuth)) return emptyResult([CODEX_KEYCHAIN_WARNING]);
@@ -7286,11 +7997,20 @@ async function stageOpencodeStaticForUpload(opts = {}) {
7286
7997
  }
7287
7998
  }
7288
7999
  async function stageOpencodeCredentialsForUpload(opts = {}) {
8000
+ if (await pathExists7(OPENCODE_CREDENTIALS_BACKUP_FILE)) {
8001
+ return stageSingleFileTarball("opencode-creds", OPENCODE_CREDENTIALS_BACKUP_FILE, "auth.json");
8002
+ }
7289
8003
  const hostHome = opts.hostHome ?? homedir11();
7290
8004
  const hostAuth = join14(hostHome, ".local", "share", "opencode", "auth.json");
7291
8005
  if (!await pathExists7(hostAuth)) return emptyResult();
7292
8006
  return stageSingleFileTarball("opencode-creds", hostAuth, "auth.json");
7293
8007
  }
8008
+ async function stageOpencodeStateForUpload(opts = {}) {
8009
+ const hostHome = opts.hostHome ?? homedir11();
8010
+ const hostModel = join14(hostHome, ".local", "state", "opencode", "model.json");
8011
+ if (!await pathExists7(hostModel)) return emptyResult();
8012
+ return stageSingleFileTarball("opencode-state", hostModel, "model.json");
8013
+ }
7294
8014
  function browserSessionActive(stdout, exitCode) {
7295
8015
  return exitCode === 0 && !/no active sessions/i.test(stdout);
7296
8016
  }
@@ -7330,25 +8050,25 @@ export {
7330
8050
  listProjectsConfigured,
7331
8051
  pruneOrphanProjectConfigs,
7332
8052
  bumpProjectGcCounter,
8053
+ BOX_STATUS_EVENT,
7333
8054
  renderStatusTable,
7334
8055
  renderTaskTable,
7335
8056
  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,
8057
+ loadCarrySection,
7348
8058
  DEFAULT_RELAY_PORT,
7349
8059
  RELAY_CONTAINER_NAME,
7350
8060
  RELAY_NETWORK_NAME,
7351
8061
  RELAY_IMAGE_REF,
8062
+ hashRpcParams,
8063
+ GH_PR_OPS,
8064
+ injectPrCreateHead,
8065
+ loadQueueConfig,
8066
+ writeJob,
8067
+ readJob,
8068
+ deleteJob,
8069
+ loadQueue,
8070
+ defaultCountRunningBoxes,
8071
+ queueLogPath,
7352
8072
  resolveAgentLauncher,
7353
8073
  BoxNotFoundError,
7354
8074
  AmbiguousBoxError,
@@ -7390,6 +8110,7 @@ export {
7390
8110
  buildDashboardAttachArgv,
7391
8111
  waitForTmuxPaneContent,
7392
8112
  buildTmuxSessionArgs,
8113
+ buildTmuxConfigShellSnippet,
7393
8114
  buildShellArgv,
7394
8115
  buildClaudeLoginRunArgv,
7395
8116
  runInteractiveClaudeLogin,
@@ -7399,6 +8120,9 @@ export {
7399
8120
  claudeSessionInfo,
7400
8121
  pullClaudeExtras,
7401
8122
  CREDENTIALS_BACKUP_FILE,
8123
+ CODEX_CREDENTIALS_BACKUP_FILE,
8124
+ OPENCODE_CREDENTIALS_BACKUP_FILE,
8125
+ isRealAgentCredential,
7402
8126
  hostBackupHasCredentials,
7403
8127
  parseSyncResult,
7404
8128
  syncClaudeCredentials,
@@ -7422,6 +8146,7 @@ export {
7422
8146
  DEFAULT_OPENCODE_SESSION,
7423
8147
  resolveOpencodeVolume,
7424
8148
  ensureOpencodeVolume,
8149
+ seedOpencodePlugin,
7425
8150
  OPENCODE_FORWARDED_ENV_KEYS,
7426
8151
  buildOpencodeMounts,
7427
8152
  OpencodeSessionError,
@@ -7459,10 +8184,6 @@ export {
7459
8184
  installPortless,
7460
8185
  startPortlessProxy,
7461
8186
  resolvePortlessHostStateDir,
7462
- DEFAULT_BOX_IMAGE,
7463
- imageExists,
7464
- imageInfo,
7465
- ensureImage,
7466
8187
  EXCLUDE_DIRS,
7467
8188
  SNAPSHOTS_ROOT,
7468
8189
  snapshotPathFor,
@@ -7485,6 +8206,7 @@ export {
7485
8206
  forgetBoxFromRelay,
7486
8207
  setRelayNotice,
7487
8208
  clearRelayNotice,
8209
+ mintHostInitiatedToken,
7488
8210
  rehydrateRelayRegistry,
7489
8211
  IDE_FLAVORS,
7490
8212
  ideProfile,
@@ -7533,6 +8255,8 @@ export {
7533
8255
  allCheckpointImagesBytes,
7534
8256
  agentboxHomeBytes,
7535
8257
  boxResourceStats,
8258
+ uploadToBox,
8259
+ downloadFromBox,
7536
8260
  dockerProvider,
7537
8261
  stageClaudeStaticForUpload,
7538
8262
  stageClaudeCredentialsForUpload,
@@ -7540,7 +8264,8 @@ export {
7540
8264
  stageCodexCredentialsForUpload,
7541
8265
  stageOpencodeStaticForUpload,
7542
8266
  stageOpencodeCredentialsForUpload,
8267
+ stageOpencodeStateForUpload,
7543
8268
  browserSessionActive,
7544
8269
  ensureBoxBrowser
7545
8270
  };
7546
- //# sourceMappingURL=chunk-NAVL4R34.js.map
8271
+ //# sourceMappingURL=chunk-NCJP5MTN.js.map