windows-development-sandbox 1.9.0 → 1.10.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 (52) hide show
  1. package/config.example.json +2 -1
  2. package/dist/cli/commands/launch/launch.js +2 -0
  3. package/dist/cli/commands/vpn.js +1 -0
  4. package/dist/cli/commands/web.d.ts +5 -0
  5. package/dist/cli/commands/web.js +38 -0
  6. package/dist/core/config/defaults.js +1 -1
  7. package/dist/core/schema/config.d.ts +1 -0
  8. package/dist/core/schema/vpn.js +1 -1
  9. package/dist/core/schema/web.d.ts +1 -0
  10. package/dist/core/schema/web.js +4 -1
  11. package/dist/guest/boot/boot.js +3 -1
  12. package/dist/guest/governor/decide.d.ts +11 -1
  13. package/dist/guest/governor/decide.js +30 -1
  14. package/dist/guest/governor/governor.d.ts +3 -0
  15. package/dist/guest/governor/governor.js +18 -2
  16. package/dist/guest/network/firewall.d.ts +1 -0
  17. package/dist/guest/network/firewall.js +65 -23
  18. package/dist/guest/runtime/process.js +53 -5
  19. package/dist/guest/setup/claude-mem.js +12 -4
  20. package/dist/guest/toolchains/languages/node.js +29 -3
  21. package/dist/host/life/host-memory.d.ts +1 -0
  22. package/dist/host/life/host-memory.js +4 -1
  23. package/dist/host/life/watchdog.d.ts +2 -0
  24. package/dist/host/life/watchdog.js +19 -3
  25. package/dist/runtime/agent.js +14 -10
  26. package/dist/runtime/daemon.js +861 -711
  27. package/dist/runtime/db.js +14 -10
  28. package/dist/runtime/main.js +159 -41
  29. package/dist/runtime/netproxy.js +4 -3
  30. package/dist/runtime/view.js +4 -3
  31. package/dist/selftest/guest-boot.test.js +47 -1
  32. package/dist/selftest/heavy-load.test.js +27 -1
  33. package/dist/selftest/network.test.js +20 -1
  34. package/dist/selftest/support/fixtures.d.ts +1 -0
  35. package/dist/shared/io/copy-tree.d.ts +7 -0
  36. package/dist/shared/io/copy-tree.js +45 -0
  37. package/dist/shared/io/index.d.ts +2 -0
  38. package/dist/shared/io/index.js +2 -0
  39. package/dist/shared/io/robocopy-result.d.ts +6 -0
  40. package/dist/shared/io/robocopy-result.js +23 -0
  41. package/dist/shared/memory/policy.d.ts +5 -0
  42. package/dist/shared/memory/policy.js +8 -0
  43. package/dist/shared/network/firewall-rules.d.ts +8 -0
  44. package/dist/shared/network/firewall-rules.js +28 -0
  45. package/native/csharp/HcsHost/AssemblyInfo.cs +3 -3
  46. package/native/csharp/HostTools/AssemblyInfo.cs +3 -3
  47. package/native/csharp/HostTools/MemoryStatus.cs +128 -107
  48. package/native/scripts/memory-status.ps1 +10 -2
  49. package/package.json +1 -1
  50. package/sandbox/csharp/MemGov.cs +466 -398
  51. package/sandbox/scripts/new-shortcuts.ps1 +3 -1
  52. package/schemas/config.schema.json +6 -2
@@ -26538,16 +26538,17 @@ var ResumeSchema = external_exports.strictObject({
26538
26538
  // src/core/schema/web.ts
26539
26539
  var WebSchema = external_exports.strictObject({
26540
26540
  enabled: external_exports.boolean().describe(
26541
- "(Beta) Allow the web UI: your sandbox sessions in a browser on this PC. Off by default, and nothing starts it by itself yet: sandbox web start works either way and runs it until sandbox web stop (or a restart of the PC)."
26541
+ "(Beta) The web UI: your sandbox sessions in a browser on this PC (http://127.0.0.1:<port>, this PC only). On: every sandbox start also starts it in the background (it keeps running until sandbox web stop or a restart of the PC). Off: only sandbox web start runs it. Open it signed in with sandbox web open."
26542
26542
  ),
26543
- port: external_exports.number().int().min(1024).max(65535).describe("Port of the web UI on 127.0.0.1 (this PC only). Pick another when it is taken.")
26543
+ port: external_exports.number().int().min(1024).max(65535).describe("Port of the web UI on 127.0.0.1 (this PC only). Pick another when it is taken."),
26544
+ openOnLaunch: external_exports.boolean().describe("(Beta) With the web UI on: open it in your browser, already signed in, every time a sandbox starts.")
26544
26545
  });
26545
26546
 
26546
26547
  // src/core/schema/vpn.ts
26547
26548
  var VPN_PROVIDERS = ["nordvpn", "openvpn-file", "wireguard-file"];
26548
26549
  var VpnSchema = external_exports.strictObject({
26549
26550
  enabled: external_exports.boolean().describe(
26550
- "Send the sandbox's traffic through a VPN. Only the sandbox: your PC's own connection is untouched. Needs a saved login first (sandbox vpn login). The agent in the sandbox can read that login and can turn the tunnel off (it is the administrator there)."
26551
+ "(Alpha: not yet tried on a real PC) Send the sandbox's traffic through a VPN. Only the sandbox: your PC's own connection is untouched. Needs a saved login first (sandbox vpn login). The agent in the sandbox can read that login and can turn the tunnel off (it is the administrator there)."
26551
26552
  ),
26552
26553
  provider: external_exports.enum(VPN_PROVIDERS).describe(
26553
26554
  "nordvpn: a recommended NordVPN server is picked at every launch. openvpn-file: your own .ovpn file (configFile). wireguard-file: your own WireGuard .conf (configFile), e.g. from your company."
@@ -27439,19 +27440,22 @@ import fs10 from "node:fs";
27439
27440
  import os3 from "node:os";
27440
27441
  import path12 from "node:path";
27441
27442
  import { spawnSync as spawnSync2 } from "node:child_process";
27442
- function exec(file2, args, options = {}) {
27443
- const result = spawnSync2(file2, args, { stdio: "inherit", windowsHide: true, ...options });
27444
- if (result.error) throw result.error;
27445
- if (result.status !== 0)
27446
- throw new Error(path12.basename(file2) + " " + args.slice(0, 3).join(" ") + " failed with exit code " + result.status);
27447
- }
27448
27443
  async function execAsync(file2, args, options = {}) {
27449
27444
  const code = await spawnAsync(file2, args, { stdio: "inherit", ...options });
27450
27445
  if (code !== 0)
27451
27446
  throw new Error(path12.basename(file2) + " " + args.slice(0, 3).join(" ") + " failed with exit code " + code);
27452
27447
  }
27453
27448
  function runScript(name, args = []) {
27454
- exec(SYSTEM.powershell(), scriptArgs(name, args));
27449
+ const result = spawnSync2(SYSTEM.powershell(), scriptArgs(name, args), {
27450
+ stdio: ["inherit", "inherit", "pipe"],
27451
+ encoding: "utf8",
27452
+ windowsHide: true
27453
+ });
27454
+ if (result.error) throw result.error;
27455
+ const errors = String(result.stderr ?? "").trim();
27456
+ if (errors) process.stderr.write(errors + "\n");
27457
+ if (result.status !== 0)
27458
+ throw new Error(name + " failed with exit code " + result.status + (errors ? ": " + errors.slice(0, 400) : ""));
27455
27459
  }
27456
27460
  function runScriptWithJson(name, param, data, args = []) {
27457
27461
  const dir = fs10.mkdtempSync(path12.join(os3.tmpdir(), "sbx-"));
@@ -26595,16 +26595,17 @@ var ResumeSchema = external_exports.strictObject({
26595
26595
  // src/core/schema/web.ts
26596
26596
  var WebSchema = external_exports.strictObject({
26597
26597
  enabled: external_exports.boolean().describe(
26598
- "(Beta) Allow the web UI: your sandbox sessions in a browser on this PC. Off by default, and nothing starts it by itself yet: sandbox web start works either way and runs it until sandbox web stop (or a restart of the PC)."
26598
+ "(Beta) The web UI: your sandbox sessions in a browser on this PC (http://127.0.0.1:<port>, this PC only). On: every sandbox start also starts it in the background (it keeps running until sandbox web stop or a restart of the PC). Off: only sandbox web start runs it. Open it signed in with sandbox web open."
26599
26599
  ),
26600
- port: external_exports.number().int().min(1024).max(65535).describe("Port of the web UI on 127.0.0.1 (this PC only). Pick another when it is taken.")
26600
+ port: external_exports.number().int().min(1024).max(65535).describe("Port of the web UI on 127.0.0.1 (this PC only). Pick another when it is taken."),
26601
+ openOnLaunch: external_exports.boolean().describe("(Beta) With the web UI on: open it in your browser, already signed in, every time a sandbox starts.")
26601
26602
  });
26602
26603
 
26603
26604
  // src/core/schema/vpn.ts
26604
26605
  var VPN_PROVIDERS = ["nordvpn", "openvpn-file", "wireguard-file"];
26605
26606
  var VpnSchema = external_exports.strictObject({
26606
26607
  enabled: external_exports.boolean().describe(
26607
- "Send the sandbox's traffic through a VPN. Only the sandbox: your PC's own connection is untouched. Needs a saved login first (sandbox vpn login). The agent in the sandbox can read that login and can turn the tunnel off (it is the administrator there)."
26608
+ "(Alpha: not yet tried on a real PC) Send the sandbox's traffic through a VPN. Only the sandbox: your PC's own connection is untouched. Needs a saved login first (sandbox vpn login). The agent in the sandbox can read that login and can turn the tunnel off (it is the administrator there)."
26608
26609
  ),
26609
26610
  provider: external_exports.enum(VPN_PROVIDERS).describe(
26610
26611
  "nordvpn: a recommended NordVPN server is picked at every launch. openvpn-file: your own .ovpn file (configFile). wireguard-file: your own WireGuard .conf (configFile), e.g. from your company."
@@ -27014,6 +27015,21 @@ function safeRemove(target, root) {
27014
27015
  fs3.rmSync(target, { recursive: true, force: true, maxRetries: 2 });
27015
27016
  }
27016
27017
 
27018
+ // src/shared/io/robocopy-result.ts
27019
+ function robocopyFailed(code) {
27020
+ return code === null || code >= 8;
27021
+ }
27022
+ function robocopyMeaning(code) {
27023
+ if (code === null) return "robocopy did not finish";
27024
+ if (code >= 16) return "robocopy stopped with a fatal error (bad path or no access)";
27025
+ if (code >= 8) return code & 1 ? "some files were copied and some could not be" : "files could not be copied";
27026
+ return code === 0 ? "nothing to copy" : "copied";
27027
+ }
27028
+ function robocopyErrors(output3, max = 3) {
27029
+ const lines = output3.split(/\r?\n/).map((l) => l.trim()).filter((l) => /\bERROR\b/.test(l) || /^The (process|system) cannot/i.test(l));
27030
+ return lines.slice(0, max).map((l) => l.length > 220 ? l.slice(0, 217) + "..." : l);
27031
+ }
27032
+
27017
27033
  // src/shared/channel/run-dir.ts
27018
27034
  function safeId(id) {
27019
27035
  return IdSchema.parse(id);
@@ -27596,7 +27612,16 @@ function execOutput(file3, args, options = {}) {
27596
27612
  return result.status === 0 ? String(result.stdout ?? "").trim() : "";
27597
27613
  }
27598
27614
  function runScript(name, args = []) {
27599
- exec(SYSTEM.powershell(), scriptArgs(name, args));
27615
+ const result = spawnSync(SYSTEM.powershell(), scriptArgs(name, args), {
27616
+ stdio: ["inherit", "inherit", "pipe"],
27617
+ encoding: "utf8",
27618
+ windowsHide: true
27619
+ });
27620
+ if (result.error) throw result.error;
27621
+ const errors = String(result.stderr ?? "").trim();
27622
+ if (errors) process.stderr.write(errors + "\n");
27623
+ if (result.status !== 0)
27624
+ throw new Error(name + " failed with exit code " + result.status + (errors ? ": " + errors.slice(0, 400) : ""));
27600
27625
  }
27601
27626
  function scriptOutput(name, args = [], options = {}) {
27602
27627
  return execOutput(SYSTEM.powershell(), scriptArgs(name, args), options);
@@ -27622,14 +27647,40 @@ function robocopy(from, to, files = []) {
27622
27647
  fs12.copyFileSync(path15.join(from, f), path15.join(to, f));
27623
27648
  return;
27624
27649
  }
27625
- const what = files.length ? files : ["/E", "/MT:16"];
27626
- const result = spawnSync(
27627
- SYSTEM.robocopy(),
27628
- [from, to, ...what, "/NFL", "/NDL", "/NJH", "/NJS", "/NP", "/R:1", "/W:1", "/XJ"],
27629
- { stdio: "ignore", windowsHide: true }
27650
+ const what = files.length ? files : ["/E"];
27651
+ const passes = [
27652
+ ["/MT:16", "/R:1", "/W:1"],
27653
+ ["/MT:1", "/R:4", "/W:3"]
27654
+ ];
27655
+ let code = null;
27656
+ let output3 = "";
27657
+ for (const pass of passes) {
27658
+ const result = spawnSync(
27659
+ SYSTEM.robocopy(),
27660
+ [
27661
+ from,
27662
+ to,
27663
+ ...what,
27664
+ ...files.length ? [] : [pass[0]],
27665
+ pass[1],
27666
+ pass[2],
27667
+ "/NFL",
27668
+ "/NDL",
27669
+ "/NJH",
27670
+ "/NJS",
27671
+ "/NP",
27672
+ "/XJ"
27673
+ ],
27674
+ { encoding: "utf8", windowsHide: true }
27675
+ );
27676
+ code = result.status;
27677
+ output3 = String(result.stdout ?? "") + String(result.stderr ?? "");
27678
+ if (!robocopyFailed(code)) return;
27679
+ }
27680
+ const why = robocopyErrors(output3);
27681
+ throw new Error(
27682
+ "robocopy " + from + " -> " + to + " failed (exit " + code + ": " + robocopyMeaning(code) + ")" + (why.length ? ": " + why.join(" | ") : "")
27630
27683
  );
27631
- if ((result.status ?? 16) >= 8)
27632
- throw new Error("robocopy " + from + " -> " + to + " failed (exit " + result.status + ")");
27633
27684
  }
27634
27685
  function copyNode(from, to) {
27635
27686
  const work = stagingFolder(to);
@@ -29990,6 +30041,10 @@ var ensureNvm = async () => {
29990
30041
  log("Downloading nvm-windows (" + asset.url.split("/").slice(-2, -1)[0] + ") ...");
29991
30042
  await downloader.archive(asset.url, NVM_HOME, { sha256: asset.sha256, locate: "nvm.exe", fileName: asset.name });
29992
30043
  };
30044
+ var hostNodeFolder = (version2) => {
30045
+ const dir = path45.join(hostToolsDir, "node");
30046
+ return session.hostTools.nodeVersion?.replace(/^v/, "") === version2 && exists(path45.join(dir, "node.exe")) ? dir : null;
30047
+ };
29993
30048
  var installNodeVersion = async (version2) => {
29994
30049
  const dir = path45.join(NVM_HOME, "v" + version2);
29995
30050
  const fromHost = path45.join(HOST_NVM, "v" + version2);
@@ -29999,6 +30054,12 @@ var installNodeVersion = async (version2) => {
29999
30054
  copyNode(fromHost, dir);
30000
30055
  return;
30001
30056
  }
30057
+ const hostNode = hostNodeFolder(version2);
30058
+ if (hostNode) {
30059
+ log("Copying Node " + version2 + " from this PC (without global packages) ...");
30060
+ copyNode(hostNode, dir);
30061
+ return;
30062
+ }
30002
30063
  log("Downloading Node " + version2 + " (~30 MB once) ...");
30003
30064
  mkdirp(NVM_TEMP);
30004
30065
  let out;
@@ -30007,8 +30068,13 @@ var installNodeVersion = async (version2) => {
30007
30068
  } finally {
30008
30069
  safeRemove(NVM_TEMP, NVM_HOME);
30009
30070
  }
30010
- if (!exists(path45.join(dir, "node.exe")))
30011
- throw new Error("nvm install " + version2 + " failed: " + out.trim().split("\n").slice(-2).join(" "));
30071
+ if (!exists(path45.join(dir, "node.exe"))) {
30072
+ const tail2 = out.trim().split(/\r?\n/).filter(Boolean).slice(-3).join(" ");
30073
+ const why = /complete/i.test(out) ? "nvm said it finished, but its move into the data folder did not happen" : "nvm could not download or unpack it: check the internet connection and the free disk space";
30074
+ throw new Error(
30075
+ "nvm install " + version2 + " did not leave " + path45.join(dir, "node.exe") + " (" + why + ")" + (tail2 ? ". nvm printed: " + tail2 : "")
30076
+ );
30077
+ }
30012
30078
  };
30013
30079
  var activate = async (version2) => {
30014
30080
  await nvm(["use", version2]);
@@ -33042,9 +33108,31 @@ function buildFirewallCommands(exemptIps) {
33042
33108
  }
33043
33109
  return commands;
33044
33110
  }
33111
+ var ATTACH_RULE = "AgentSandbox-Attach";
33112
+ function hasShortName(file3) {
33113
+ return file3.split(/[/\\]/).some((part) => /~\d/.test(part));
33114
+ }
33115
+ function attachRuleCommands(ptyHostExe) {
33116
+ return [
33117
+ ["advfirewall", "firewall", "delete", "rule", "name=" + ATTACH_RULE],
33118
+ [
33119
+ "advfirewall",
33120
+ "firewall",
33121
+ "add",
33122
+ "rule",
33123
+ "name=" + ATTACH_RULE,
33124
+ "dir=in",
33125
+ "action=allow",
33126
+ "enable=yes",
33127
+ "profile=any",
33128
+ "protocol=tcp",
33129
+ "remoteip=LocalSubnet",
33130
+ "program=" + ptyHostExe
33131
+ ]
33132
+ ];
33133
+ }
33045
33134
 
33046
33135
  // src/guest/network/firewall.ts
33047
- var ATTACH_RULE = "AgentSandbox-Attach";
33048
33136
  var netsh = (args) => {
33049
33137
  const r = spawnSync5(SYSTEM.netsh(), args, { encoding: "utf8", windowsHide: true });
33050
33138
  return { status: r.status, out: (r.stdout ?? "") + (r.stderr ?? "") };
@@ -33078,23 +33166,38 @@ var waitForInternet = async (seconds) => {
33078
33166
  }
33079
33167
  return false;
33080
33168
  };
33081
- var attachRule = (ptyHostExe) => [
33082
- ["advfirewall", "firewall", "delete", "rule", "name=" + ATTACH_RULE],
33083
- [
33084
- "advfirewall",
33085
- "firewall",
33086
- "add",
33087
- "rule",
33088
- "name=" + ATTACH_RULE,
33089
- "dir=in",
33090
- "action=allow",
33091
- "enable=yes",
33092
- "profile=any",
33093
- "protocol=tcp",
33094
- "remoteip=LocalSubnet",
33095
- "program=" + ptyHostExe
33096
- ]
33097
- ];
33169
+ var problems = [];
33170
+ function networkProblems() {
33171
+ return problems;
33172
+ }
33173
+ var longPath = (file3) => {
33174
+ if (!hasShortName(file3)) return file3;
33175
+ try {
33176
+ return fs48.realpathSync.native(file3);
33177
+ } catch {
33178
+ return file3;
33179
+ }
33180
+ };
33181
+ async function applyAttachRule(ptyHostExe) {
33182
+ if (!session.attach.enabled) return;
33183
+ if (!ptyHostExe) {
33184
+ problems.push("attach: the session host was not built, so the firewall rule for it was not created");
33185
+ warn(problems[problems.length - 1]);
33186
+ return;
33187
+ }
33188
+ let last = "";
33189
+ for (let attempt = 1; attempt <= 3; attempt++) {
33190
+ try {
33191
+ run2(attachRuleCommands(longPath(ptyHostExe)));
33192
+ return;
33193
+ } catch (e) {
33194
+ last = errMsg(e);
33195
+ if (attempt < 3) await new Promise((r) => setTimeout(r, 1e3));
33196
+ }
33197
+ }
33198
+ problems.push("attach: the firewall rule for the session host could not be created (" + last + ")");
33199
+ warn("PROTECTION MISSING: " + problems[problems.length - 1]);
33200
+ }
33098
33201
  function run2(commands) {
33099
33202
  for (const c of commands) {
33100
33203
  const r = netsh(c);
@@ -33110,10 +33213,11 @@ async function applyNetworkPolicy(ptyHostExe) {
33110
33213
  const block = session.network.blockPrivateRanges;
33111
33214
  const exempt = block ? discoverExemptIps() : [];
33112
33215
  const lanRules = block ? buildFirewallCommands(exempt) : [];
33216
+ const forwardRules = forwardRule();
33113
33217
  const commands = [
33114
33218
  ...lanRules,
33115
- ...ptyHostExe && session.attach.enabled ? attachRule(ptyHostExe) : [],
33116
- ...forwardRule()
33219
+ ...ptyHostExe && session.attach.enabled ? attachRuleCommands(ptyHostExe) : [],
33220
+ ...forwardRules
33117
33221
  ];
33118
33222
  if (isWin && !dryRun) {
33119
33223
  try {
@@ -33139,7 +33243,20 @@ async function applyNetworkPolicy(ptyHostExe) {
33139
33243
  }
33140
33244
  if (commands.length === 0) return "open";
33141
33245
  const online = block ? await waitForInternet(30) : true;
33142
- run2(commands);
33246
+ const failures = [];
33247
+ try {
33248
+ run2(lanRules);
33249
+ } catch (e) {
33250
+ failures.push(errMsg(e));
33251
+ }
33252
+ await applyAttachRule(ptyHostExe);
33253
+ try {
33254
+ run2(forwardRules);
33255
+ } catch (e) {
33256
+ problems.push("forwardPorts: the firewall rule for the relays could not be created (" + errMsg(e) + ")");
33257
+ warn("PROTECTION MISSING: " + problems[problems.length - 1]);
33258
+ }
33259
+ if (failures.length) throw new Error(failures[0]);
33143
33260
  if (!block) return "open";
33144
33261
  if (online && !await waitForInternet(15)) {
33145
33262
  run2(lanRules.filter((c) => c[2] === "delete"));
@@ -33584,10 +33701,10 @@ function directiveOf(line) {
33584
33701
  return { name: parts2[0].toLowerCase(), args: parts2.slice(1) };
33585
33702
  }
33586
33703
  function inspectOvpn(text) {
33587
- const problems = [];
33704
+ const problems2 = [];
33588
33705
  const found = [];
33589
33706
  const refuse = (message) => {
33590
- if (problems.length < 20 && !problems.includes(message)) problems.push(message);
33707
+ if (problems2.length < 20 && !problems2.includes(message)) problems2.push(message);
33591
33708
  };
33592
33709
  if (Buffer.byteLength(text, "utf8") > MAX_OVPN_BYTES) return { ok: false, problems: ["the file is too big"] };
33593
33710
  if (text.includes("\0")) return { ok: false, problems: ["the file is not text"] };
@@ -33635,7 +33752,7 @@ function inspectOvpn(text) {
33635
33752
  if (block) refuse("a <" + block + "> section is never closed");
33636
33753
  if (remotes.length === 0) refuse("it names no remote server");
33637
33754
  if (remotes.length > 64) refuse("it names too many servers");
33638
- return problems.length ? { ok: false, problems } : { ok: true, remotes };
33755
+ return problems2.length ? { ok: false, problems: problems2 } : { ok: true, remotes };
33639
33756
  }
33640
33757
  function assertSafeOvpn(text, what) {
33641
33758
  const report = inspectOvpn(text);
@@ -33672,9 +33789,9 @@ var inRange = (text, low, high) => {
33672
33789
  };
33673
33790
  var splitList = (text) => text.split(",").map((s) => s.trim()).filter(Boolean);
33674
33791
  function inspectWireguard(text) {
33675
- const problems = [];
33792
+ const problems2 = [];
33676
33793
  const refuse = (message) => {
33677
- if (problems.length < 20 && !problems.includes(message)) problems.push(message);
33794
+ if (problems2.length < 20 && !problems2.includes(message)) problems2.push(message);
33678
33795
  };
33679
33796
  if (Buffer.byteLength(text, "utf8") > MAX_WIREGUARD_BYTES) return { ok: false, problems: ["the file is too big"] };
33680
33797
  if (text.includes("\0")) return { ok: false, problems: ["the file is not text"] };
@@ -33716,7 +33833,7 @@ function inspectWireguard(text) {
33716
33833
  if (interfaces.length !== 1) refuse("it needs exactly one [Interface] section");
33717
33834
  if (peers.length === 0) refuse("it has no [Peer] section");
33718
33835
  if (peers.length > MAX_PEERS) refuse("it has too many [Peer] sections");
33719
- if (problems.length) return { ok: false, problems };
33836
+ if (problems2.length) return { ok: false, problems: problems2 };
33720
33837
  const face = interfaces[0];
33721
33838
  const privateKey = one(face, "privatekey");
33722
33839
  if (!privateKey) refuse("[Interface] has no PrivateKey");
@@ -33763,7 +33880,7 @@ function inspectWireguard(text) {
33763
33880
  ...keepalive !== void 0 ? { keepalive } : {}
33764
33881
  });
33765
33882
  });
33766
- if (problems.length || !privateKey) return { ok: false, problems };
33883
+ if (problems2.length || !privateKey) return { ok: false, problems: problems2 };
33767
33884
  return {
33768
33885
  ok: true,
33769
33886
  config: {
@@ -34883,6 +35000,7 @@ var Boot = class {
34883
35000
  databases: session.databases ?? [],
34884
35001
  apps: session.apps
34885
35002
  });
35003
+ for (const p of networkProblems()) tools.summary.push("PROTECTION MISSING: " + p);
34886
35004
  if (!dryRun) tools.prepend.push(writeDbCommand());
34887
35005
  Object.assign(tools.vars, proxyEnv());
34888
35006
  await soft("Network: allow only some hosts", async () => {
@@ -26591,16 +26591,17 @@ var ResumeSchema = external_exports.strictObject({
26591
26591
  // src/core/schema/web.ts
26592
26592
  var WebSchema = external_exports.strictObject({
26593
26593
  enabled: external_exports.boolean().describe(
26594
- "(Beta) Allow the web UI: your sandbox sessions in a browser on this PC. Off by default, and nothing starts it by itself yet: sandbox web start works either way and runs it until sandbox web stop (or a restart of the PC)."
26594
+ "(Beta) The web UI: your sandbox sessions in a browser on this PC (http://127.0.0.1:<port>, this PC only). On: every sandbox start also starts it in the background (it keeps running until sandbox web stop or a restart of the PC). Off: only sandbox web start runs it. Open it signed in with sandbox web open."
26595
26595
  ),
26596
- port: external_exports.number().int().min(1024).max(65535).describe("Port of the web UI on 127.0.0.1 (this PC only). Pick another when it is taken.")
26596
+ port: external_exports.number().int().min(1024).max(65535).describe("Port of the web UI on 127.0.0.1 (this PC only). Pick another when it is taken."),
26597
+ openOnLaunch: external_exports.boolean().describe("(Beta) With the web UI on: open it in your browser, already signed in, every time a sandbox starts.")
26597
26598
  });
26598
26599
 
26599
26600
  // src/core/schema/vpn.ts
26600
26601
  var VPN_PROVIDERS = ["nordvpn", "openvpn-file", "wireguard-file"];
26601
26602
  var VpnSchema = external_exports.strictObject({
26602
26603
  enabled: external_exports.boolean().describe(
26603
- "Send the sandbox's traffic through a VPN. Only the sandbox: your PC's own connection is untouched. Needs a saved login first (sandbox vpn login). The agent in the sandbox can read that login and can turn the tunnel off (it is the administrator there)."
26604
+ "(Alpha: not yet tried on a real PC) Send the sandbox's traffic through a VPN. Only the sandbox: your PC's own connection is untouched. Needs a saved login first (sandbox vpn login). The agent in the sandbox can read that login and can turn the tunnel off (it is the administrator there)."
26604
26605
  ),
26605
26606
  provider: external_exports.enum(VPN_PROVIDERS).describe(
26606
26607
  "nordvpn: a recommended NordVPN server is picked at every launch. openvpn-file: your own .ovpn file (configFile). wireguard-file: your own WireGuard .conf (configFile), e.g. from your company."
@@ -26942,16 +26942,17 @@ var ResumeSchema = external_exports.strictObject({
26942
26942
  // src/core/schema/web.ts
26943
26943
  var WebSchema = external_exports.strictObject({
26944
26944
  enabled: external_exports.boolean().describe(
26945
- "(Beta) Allow the web UI: your sandbox sessions in a browser on this PC. Off by default, and nothing starts it by itself yet: sandbox web start works either way and runs it until sandbox web stop (or a restart of the PC)."
26945
+ "(Beta) The web UI: your sandbox sessions in a browser on this PC (http://127.0.0.1:<port>, this PC only). On: every sandbox start also starts it in the background (it keeps running until sandbox web stop or a restart of the PC). Off: only sandbox web start runs it. Open it signed in with sandbox web open."
26946
26946
  ),
26947
- port: external_exports.number().int().min(1024).max(65535).describe("Port of the web UI on 127.0.0.1 (this PC only). Pick another when it is taken.")
26947
+ port: external_exports.number().int().min(1024).max(65535).describe("Port of the web UI on 127.0.0.1 (this PC only). Pick another when it is taken."),
26948
+ openOnLaunch: external_exports.boolean().describe("(Beta) With the web UI on: open it in your browser, already signed in, every time a sandbox starts.")
26948
26949
  });
26949
26950
 
26950
26951
  // src/core/schema/vpn.ts
26951
26952
  var VPN_PROVIDERS = ["nordvpn", "openvpn-file", "wireguard-file"];
26952
26953
  var VpnSchema = external_exports.strictObject({
26953
26954
  enabled: external_exports.boolean().describe(
26954
- "Send the sandbox's traffic through a VPN. Only the sandbox: your PC's own connection is untouched. Needs a saved login first (sandbox vpn login). The agent in the sandbox can read that login and can turn the tunnel off (it is the administrator there)."
26955
+ "(Alpha: not yet tried on a real PC) Send the sandbox's traffic through a VPN. Only the sandbox: your PC's own connection is untouched. Needs a saved login first (sandbox vpn login). The agent in the sandbox can read that login and can turn the tunnel off (it is the administrator there)."
26955
26956
  ),
26956
26957
  provider: external_exports.enum(VPN_PROVIDERS).describe(
26957
26958
  "nordvpn: a recommended NordVPN server is picked at every launch. openvpn-file: your own .ovpn file (configFile). wireguard-file: your own WireGuard .conf (configFile), e.g. from your company."
@@ -2,7 +2,10 @@
2
2
  // what it copies in, and what it refuses to.
3
3
  import fs from 'node:fs';
4
4
  import path from 'node:path';
5
- import { readJsonFile } from '../shared/io/index.js';
5
+ import { spawnSync } from 'node:child_process';
6
+ import { copyTreeTolerant, readJsonFile, robocopyErrors, robocopyFailed, robocopyMeaning } from '../shared/io/index.js';
7
+ import { scriptArgs } from '../shared/scripts.js';
8
+ import { SYSTEM } from '../shared/system.js';
6
9
  export const guestBootSuite = {
7
10
  name: 'guest-boot',
8
11
  title: 'guest boot in dry-run mode (provisioning, Claude state, git policy)',
@@ -92,5 +95,48 @@ export const guestBootSuite = {
92
95
  const genNoAgent = await f.launch({ noAgent: true });
93
96
  const noAgent = await f.runGuest(genNoAgent.launchPath, t.at('data5'), f.workA);
94
97
  t.check(noAgent.status === 0 && noAgent.out.includes('no agent requested'), '--no-agent provisions without an agent', noAgent.out);
98
+ t.section('robocopy results, vanished files, shortcuts');
99
+ t.check([0, 1, 2, 3, 7].every((c) => !robocopyFailed(c)) &&
100
+ [8, 9, 16].every((c) => robocopyFailed(c)) &&
101
+ robocopyFailed(null), 'robocopy: exit codes below 8 are success, 8 and up (9 = some copied, some failed) are failures');
102
+ t.includes(robocopyMeaning(9), 'some files were copied and some could not be', 'robocopy: exit 9 is explained');
103
+ t.equal(robocopyErrors('x\r\n2026/10/11 ERROR 32 (0x00000020) Copying File C:\\a\\b.dll\r\nThe process cannot access the file because it is being used by another process.\r\n', 5), [
104
+ '2026/10/11 ERROR 32 (0x00000020) Copying File C:\\a\\b.dll',
105
+ 'The process cannot access the file because it is being used by another process.',
106
+ ], 'robocopy: the failing file and the reason are picked out of its output');
107
+ const spool = t.dir('copy-tree', 'live');
108
+ t.write(path.join(spool, 'a.json'), '1');
109
+ t.write(path.join(spool, 'b.json'), '2');
110
+ t.write(path.join(spool, 'db', 'chroma.sqlite3'), 'db');
111
+ t.write(path.join(spool, 'db', 'chroma.sqlite3-journal'), 'j');
112
+ t.write(path.join(spool, 'x.lock'), 'l');
113
+ const copied = copyTreeTolerant(spool, t.at('copy-tree', 'out'), (name) => /\.lock$|-journal$/.test(name));
114
+ t.check(copied.copied === 3 && !fs.existsSync(t.at('copy-tree', 'out', 'db', 'chroma.sqlite3-journal')), 'copy tree: files are copied, a -journal and a lock file are left out');
115
+ // The first file of the listing deletes the second before it is reached: the second has vanished, not failed.
116
+ const racing = t.dir('copy-tree', 'racing');
117
+ t.write(path.join(racing, 'a.json'), '1');
118
+ t.write(path.join(racing, 'b.json'), '2');
119
+ const result = copyTreeTolerant(racing, t.at('copy-tree', 'racing-out'), (name) => {
120
+ if (name === 'a.json')
121
+ fs.rmSync(path.join(racing, 'b.json'), { force: true });
122
+ return false;
123
+ });
124
+ t.check(result.copied === 1 && result.vanished === 1, 'copy tree: a file that vanishes during the copy is skipped, not an error');
125
+ t.equal(copyTreeTolerant(t.at('copy-tree', 'nothing'), t.at('copy-tree', 'nothing-out')).vanished, 1, 'copy tree: a missing folder is tolerated');
126
+ if (process.platform === 'win32') {
127
+ // Windows PowerShell 5.1 hands a JSON array over as one object: two or more shortcuts used to fail.
128
+ const out = t.dir('shortcuts');
129
+ const list = t.writeJson(path.join(out, 'in.json'), [
130
+ { link: path.join(out, 'one', 'A.lnk'), target: path.join(process.env.WINDIR ?? 'C:\\Windows', 'notepad.exe') },
131
+ { link: path.join(out, 'two', 'B.lnk'), target: path.join(process.env.WINDIR ?? 'C:\\Windows', 'notepad.exe') },
132
+ ]);
133
+ const made = spawnSync(SYSTEM.powershell(), scriptArgs('new-shortcuts.ps1', ['-ListFile', list]), {
134
+ encoding: 'utf8',
135
+ windowsHide: true,
136
+ });
137
+ t.check(made.status === 0 &&
138
+ fs.existsSync(path.join(out, 'one', 'A.lnk')) &&
139
+ fs.existsSync(path.join(out, 'two', 'B.lnk')), 'new-shortcuts.ps1 makes every shortcut of a list of two', String(made.stderr));
140
+ }
95
141
  },
96
142
  };
@@ -8,7 +8,7 @@ import { DEFAULTS } from '../core/config/index.js';
8
8
  import { isWin } from '../core/util/index.js';
9
9
  import { SYSTEM } from '../shared/system.js';
10
10
  import { GuestRequestSchema, PressureStatusSchema } from '../shared/channel/index.js';
11
- import { GUEST_THRESHOLDS, HOST_THRESHOLDS, cargoJobs, effectiveCpuLimit, gentleCpuLimit, } from '../shared/memory/index.js';
11
+ import { GUEST_THRESHOLDS, HOST_THRESHOLDS, cargoJobs, effectiveCpuLimit, gentleCpuLimit, maxCompilers, } from '../shared/memory/index.js';
12
12
  import { agentMemoryLimitMb, decideMemory, memoryNote, resolveMemoryMb } from '../host/launch/index.js';
13
13
  import { vgpuWarning, GENTLE_NOTE } from '../host/launch/risk-notes.js';
14
14
  import { bugcheckCode, cpuPercentBetween, explainStop, findDumps, findingLines, freezeLine, hostPressure, lowReason, pageFileRisk, parseHostMemory, crashReportText, watchdogStep, WATCHDOG_START, } from '../host/life/index.js';
@@ -149,6 +149,22 @@ export const heavyLoadSuite = {
149
149
  g.actions.length === 3 &&
150
150
  g.actions.every((a) => a.kind === 'priority' && a.to === 'below' && a.pid !== 3), 'compilers start below normal even with plenty of memory (the agent does not)');
151
151
  t.check(decide(INITIAL_STATE, sample(14, 50, procs), 0, gentleT, 'gentle').level === 'throttled', '14 % free is pressure');
152
+ t.section('the compiler cap (vGPU on)');
153
+ t.check(maxCompilers(16, 'gentle') === 4 && maxCompilers(4, 'gentle') === 2 && maxCompilers(16, 'normal') === null, 'at most max(2, cores / 4) with vGPU, none otherwise');
154
+ const many = [
155
+ proc(10, 'rustc.exe', 800),
156
+ proc(11, 'rustc.exe', 900),
157
+ proc(12, 'clippy-driver.exe', 700),
158
+ proc(13, 'link.exe', 100),
159
+ proc(3, 'claude.exe', 500),
160
+ proc(14, 'node.exe', 400),
161
+ ];
162
+ const capped = decide(INITIAL_STATE, sample(60, 40, many), 0, gentleT, 'gentle', 2);
163
+ const holds = capped.actions.filter((a) => a.kind === 'hold').map((a) => a.pid);
164
+ t.check(holds.length === 2 && holds.includes(12) && holds.includes(13), 'the newest compilers beyond the cap wait; the oldest keep running', holds.join(','));
165
+ t.check(!holds.includes(3) && !holds.includes(14), 'agents and build tools are never held');
166
+ t.check(decide(INITIAL_STATE, sample(60, 40, many), 0, gentleT, 'gentle', null).actions.every((a) => a.kind !== 'hold'), 'no cap, no holds');
167
+ t.check(decide(INITIAL_STATE, sample(60, 40, many.slice(0, 2)), 0, gentleT, 'gentle', 2).actions.every((a) => a.kind !== 'hold'), 'under the cap nothing waits');
152
168
  t.section('the governor over a port (fake Windows)');
153
169
  const calls = [];
154
170
  const port = {
@@ -209,6 +225,16 @@ export const heavyLoadSuite = {
209
225
  }
210
226
  t.check(sent && cpu.low, 'CPU above 95 % for 60 s: asks the sandbox to slow down');
211
227
  t.check(!watchdogStep(WATCHDOG_START, input(pc(), 0, 97)).state.low, 'a short CPU peak is nothing');
228
+ let disk = WATCHDOG_START;
229
+ let diskSent = false;
230
+ for (let i = 0; i < 5; i++) {
231
+ const st = watchdogStep(disk, input(pc({ diskQueue: 40 }), i * 10000));
232
+ disk = st.state;
233
+ diskSent = diskSent || st.send;
234
+ }
235
+ t.check(diskSent && disk.low, 'a disk queue of 32 or more for 40 s: asks the sandbox to slow down');
236
+ t.check(!watchdogStep(WATCHDOG_START, input(pc({ diskQueue: 40 }), 0)).state.low, 'a short disk peak is nothing');
237
+ t.check(!watchdogStep(WATCHDOG_START, input(pc(), 0)).state.low, 'no disk counter: memory and CPU only');
212
238
  t.check(cpuPercentBetween([{ times: { user: 0, nice: 0, sys: 0, idle: 0, irq: 0 } }], [{ times: { user: 90, nice: 0, sys: 0, idle: 10, irq: 0 } }]) === 90, 'CPU use from two readings');
213
239
  t.check(GuestRequestSchema.safeParse({ kind: 'pressure', id: 'p1', reason: 'low', seconds: 90, createdAt: 'now' })
214
240
  .success &&
@@ -1,5 +1,5 @@
1
1
  import { isSandboxAddress } from '../host/channel/index.js';
2
- import { buildFirewallCommands, cidrToRange, ipv4DnsServers, parseDefaultGateways, parseDnsServers, subtractIps, } from '../shared/network/index.js';
2
+ import { attachRuleCommands, hasShortName, buildFirewallCommands, cidrToRange, ipv4DnsServers, parseDefaultGateways, parseDnsServers, subtractIps, } from '../shared/network/index.js';
3
3
  import { expandAllowList, hostAllowed, withBlockedHosts } from '../shared/network/index.js';
4
4
  export const networkSuite = {
5
5
  name: 'network',
@@ -11,6 +11,25 @@ export const networkSuite = {
11
11
  const remaining = subtractIps([[start172, end172]], ['172.28.0.1']);
12
12
  t.check(remaining.every(([start, end]) => gateway < start || gateway > end), 'the private block minus the gateway no longer contains the gateway');
13
13
  t.excludes(buildFirewallCommands(['8.8.8.8']).join(), '8.8.8.8', 'public addresses are not part of the private-range rules');
14
+ t.section('the attach rule');
15
+ const exe = 'C:\\Users\\Me\\AppData\\Local\\Temp\\agent-sandbox\\ptyhost-abc.exe';
16
+ const attach = attachRuleCommands(exe);
17
+ t.equal(attach[0], ['advfirewall', 'firewall', 'delete', 'rule', 'name=AgentSandbox-Attach'], 'attach: the old rule is deleted first');
18
+ t.equal(attach[1].slice(2), [
19
+ 'add',
20
+ 'rule',
21
+ 'name=AgentSandbox-Attach',
22
+ 'dir=in',
23
+ 'action=allow',
24
+ 'enable=yes',
25
+ 'profile=any',
26
+ 'protocol=tcp',
27
+ 'remoteip=LocalSubnet',
28
+ 'program=' + exe,
29
+ ], 'attach: inbound TCP to ptyhost only, from the local subnet only');
30
+ t.check(hasShortName('C:\\Users\\WDAGUT~1\\AppData\\Local\\Temp\\agent-sandbox\\ptyhost.exe'), 'an 8.3 short path is recognised (netsh cannot resolve it: "The application name could not be resolved")');
31
+ t.check(!hasShortName('C:\\Users\\WDAGUtilityAccount\\AppData\\Local\\Temp\\ptyhost.exe'), 'a long path is left alone');
32
+ t.check(!hasShortName('C:\\Users\\a~b\\x.exe'), 'a tilde not followed by a digit is not a short name');
14
33
  t.section('reading Windows output');
15
34
  t.equal(parseDefaultGateways(' 0.0.0.0 0.0.0.0 172.28.80.1 172.28.85.200 5\n'), ['172.28.80.1'], 'the default gateway is read from the route table');
16
35
  t.equal(parseDnsServers(' DNS Servers . . . : 172.28.80.1\n 8.8.8.8\n NetBIOS : x\n'), ['172.28.80.1', '8.8.8.8'], 'DNS servers are read from ipconfig output');
@@ -257,6 +257,7 @@ export declare function createFixtures(t: Harness): {
257
257
  web: {
258
258
  enabled: boolean;
259
259
  port: number;
260
+ openOnLaunch: boolean;
260
261
  };
261
262
  agentCommands: {
262
263
  enabled: boolean;
@@ -0,0 +1,7 @@
1
+ export interface CopyTreeResult {
2
+ readonly copied: number;
3
+ /** Files (or folders) that were gone by the time they were reached. */
4
+ readonly vanished: number;
5
+ }
6
+ /** Copies `from` into `to`; `skip(name)` leaves out files by name. Any error except "no such file" is thrown. */
7
+ export declare function copyTreeTolerant(from: string, to: string, skip?: (name: string) => boolean): CopyTreeResult;
@@ -0,0 +1,45 @@
1
+ // Copies a folder that something else is still writing to: a file that disappears between the listing and the copy
2
+ // (a spool file the agent's hook just consumed, a SQLite journal that was committed) is skipped, not an error.
3
+ import fs from 'node:fs';
4
+ import path from 'node:path';
5
+ const isGone = (e) => e?.code === 'ENOENT';
6
+ /** Copies `from` into `to`; `skip(name)` leaves out files by name. Any error except "no such file" is thrown. */
7
+ export function copyTreeTolerant(from, to, skip = () => false) {
8
+ let copied = 0;
9
+ let vanished = 0;
10
+ const walk = (src, dest) => {
11
+ fs.mkdirSync(dest, { recursive: true });
12
+ let entries;
13
+ try {
14
+ entries = fs.readdirSync(src, { withFileTypes: true });
15
+ }
16
+ catch (e) {
17
+ if (!isGone(e))
18
+ throw e;
19
+ vanished++;
20
+ return;
21
+ }
22
+ for (const entry of entries) {
23
+ if (skip(entry.name))
24
+ continue;
25
+ const source = path.join(src, entry.name);
26
+ const target = path.join(dest, entry.name);
27
+ try {
28
+ if (entry.isDirectory())
29
+ walk(source, target);
30
+ else if (entry.isFile()) {
31
+ fs.copyFileSync(source, target);
32
+ copied++;
33
+ }
34
+ }
35
+ catch (e) {
36
+ if (!isGone(e))
37
+ throw e;
38
+ vanished++;
39
+ fs.rmSync(target, { force: true });
40
+ }
41
+ }
42
+ };
43
+ walk(from, to);
44
+ return { copied, vanished };
45
+ }