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
@@ -1,3 +1,5 @@
1
1
  export * from './json.js';
2
2
  export * from './json-file.js';
3
3
  export * from './safe-remove.js';
4
+ export * from './robocopy-result.js';
5
+ export * from './copy-tree.js';
@@ -1,3 +1,5 @@
1
1
  export * from './json.js';
2
2
  export * from './json-file.js';
3
3
  export * from './safe-remove.js';
4
+ export * from './robocopy-result.js';
5
+ export * from './copy-tree.js';
@@ -0,0 +1,6 @@
1
+ /** Exit code 8 and up means something could not be copied; below that every file is in place. */
2
+ export declare function robocopyFailed(code: number | null): boolean;
3
+ /** The exit code in plain words (it is a bit field: 1 copied, 2 extras, 4 mismatches, 8 failures, 16 fatal). */
4
+ export declare function robocopyMeaning(code: number | null): string;
5
+ /** The first few ERROR lines of robocopy's output (what went wrong and on which file), one line each. */
6
+ export declare function robocopyErrors(output: string, max?: number): string[];
@@ -0,0 +1,23 @@
1
+ // Reading robocopy's exit code and output (pure; the guest and the tests share it).
2
+ /** Exit code 8 and up means something could not be copied; below that every file is in place. */
3
+ export function robocopyFailed(code) {
4
+ return code === null || code >= 8;
5
+ }
6
+ /** The exit code in plain words (it is a bit field: 1 copied, 2 extras, 4 mismatches, 8 failures, 16 fatal). */
7
+ export function robocopyMeaning(code) {
8
+ if (code === null)
9
+ return 'robocopy did not finish';
10
+ if (code >= 16)
11
+ return 'robocopy stopped with a fatal error (bad path or no access)';
12
+ if (code >= 8)
13
+ return code & 1 ? 'some files were copied and some could not be' : 'files could not be copied';
14
+ return code === 0 ? 'nothing to copy' : 'copied';
15
+ }
16
+ /** The first few ERROR lines of robocopy's output (what went wrong and on which file), one line each. */
17
+ export function robocopyErrors(output, max = 3) {
18
+ const lines = output
19
+ .split(/\r?\n/)
20
+ .map((l) => l.trim())
21
+ .filter((l) => /\bERROR\b/.test(l) || /^The (process|system) cannot/i.test(l));
22
+ return lines.slice(0, max).map((l) => (l.length > 220 ? l.slice(0, 217) + '...' : l));
23
+ }
@@ -23,6 +23,9 @@ export interface HostThresholds {
23
23
  /** Total CPU above this (percent) for cpuSeconds. */
24
24
  readonly cpuPercent: number;
25
25
  readonly cpuSeconds: number;
26
+ /** The disks' queue length (all together) at or above this for diskSeconds: builds writing through mapped folders. */
27
+ readonly diskQueue: number;
28
+ readonly diskSeconds: number;
26
29
  }
27
30
  export declare const HOST_THRESHOLDS: Readonly<Record<LoadProfile, HostThresholds>>;
28
31
  /** The CPU cap (percent of the PC) the gentle profile uses: 15 points lower, never below 50. "none" = 100. */
@@ -31,3 +34,5 @@ export declare function gentleCpuLimit(cpuLimit: 'none' | number): number;
31
34
  export declare function effectiveCpuLimit(cpuLimit: 'none' | number, profile: LoadProfile): number | null;
32
35
  /** Cargo jobs that fit the memory (about 4 GB per rustc) and the CPUs, fewer when several agent sessions build. */
33
36
  export declare function cargoJobs(cpus: number, memoryGb: number, sessions: number): number;
37
+ /** The most heavy compilers (rustc, clippy-driver, link ...) that run at once in the agents' job: only with vGPU on. */
38
+ export declare function maxCompilers(cpus: number, profile: LoadProfile): number | null;
@@ -25,6 +25,8 @@ export const HOST_THRESHOLDS = {
25
25
  calmSamples: 3,
26
26
  cpuPercent: 95,
27
27
  cpuSeconds: 60,
28
+ diskQueue: 32,
29
+ diskSeconds: 40,
28
30
  },
29
31
  gentle: {
30
32
  availableMinMb: 6144,
@@ -33,6 +35,8 @@ export const HOST_THRESHOLDS = {
33
35
  calmSamples: 3,
34
36
  cpuPercent: 90,
35
37
  cpuSeconds: 45,
38
+ diskQueue: 16,
39
+ diskSeconds: 30,
36
40
  },
37
41
  };
38
42
  /** The CPU cap (percent of the PC) the gentle profile uses: 15 points lower, never below 50. "none" = 100. */
@@ -53,3 +57,7 @@ export function cargoJobs(cpus, memoryGb, sessions) {
53
57
  // All the agents' builds share the sandbox: about 8 jobs in total however many sessions there are.
54
58
  return Math.max(1, Math.min(jobs, Math.floor(8 / Math.max(1, sessions))));
55
59
  }
60
+ /** The most heavy compilers (rustc, clippy-driver, link ...) that run at once in the agents' job: only with vGPU on. */
61
+ export function maxCompilers(cpus, profile) {
62
+ return profile === 'gentle' ? Math.max(2, Math.floor(cpus / 4)) : null;
63
+ }
@@ -22,3 +22,11 @@ export declare function parseDnsServers(ipconfigAll: string): string[];
22
22
  * keeps working.
23
23
  */
24
24
  export declare function buildFirewallCommands(exemptIps: string[]): string[][];
25
+ export declare const ATTACH_RULE = "AgentSandbox-Attach";
26
+ /**
27
+ * True when a Windows path has an 8.3 short name in it (`C:\Users\WDAGUT~1\...`). netsh refuses `program=` paths
28
+ * like that ("The application name could not be resolved"), so they are expanded first.
29
+ */
30
+ export declare function hasShortName(file: string): boolean;
31
+ /** Inbound rule for ptyhost only, and only from the sandbox's own subnet (where the host's Hyper-V adapter is). */
32
+ export declare function attachRuleCommands(ptyHostExe: string): string[][];
@@ -135,3 +135,31 @@ export function buildFirewallCommands(exemptIps) {
135
135
  }
136
136
  return commands;
137
137
  }
138
+ export const ATTACH_RULE = 'AgentSandbox-Attach';
139
+ /**
140
+ * True when a Windows path has an 8.3 short name in it (`C:\Users\WDAGUT~1\...`). netsh refuses `program=` paths
141
+ * like that ("The application name could not be resolved"), so they are expanded first.
142
+ */
143
+ export function hasShortName(file) {
144
+ return file.split(/[/\\]/).some((part) => /~\d/.test(part));
145
+ }
146
+ /** Inbound rule for ptyhost only, and only from the sandbox's own subnet (where the host's Hyper-V adapter is). */
147
+ export function attachRuleCommands(ptyHostExe) {
148
+ return [
149
+ ['advfirewall', 'firewall', 'delete', 'rule', 'name=' + ATTACH_RULE],
150
+ [
151
+ 'advfirewall',
152
+ 'firewall',
153
+ 'add',
154
+ 'rule',
155
+ 'name=' + ATTACH_RULE,
156
+ 'dir=in',
157
+ 'action=allow',
158
+ 'enable=yes',
159
+ 'profile=any',
160
+ 'protocol=tcp',
161
+ 'remoteip=LocalSubnet',
162
+ 'program=' + ptyHostExe,
163
+ ],
164
+ ];
165
+ }
@@ -8,7 +8,7 @@ using System.Runtime.InteropServices;
8
8
  [assembly: AssemblyCompany("windows-development-sandbox (open source)")]
9
9
  [assembly: AssemblyProduct("Windows Development Sandbox")]
10
10
  [assembly: AssemblyCopyright("MIT License, windows-development-sandbox contributors")]
11
- [assembly: AssemblyVersion("1.9.0.0")]
12
- [assembly: AssemblyFileVersion("1.9.0.0")]
13
- [assembly: AssemblyInformationalVersion("1.9.0")]
11
+ [assembly: AssemblyVersion("1.10.0.0")]
12
+ [assembly: AssemblyFileVersion("1.10.0.0")]
13
+ [assembly: AssemblyInformationalVersion("1.10.0")]
14
14
  [assembly: ComVisible(false)]
@@ -8,7 +8,7 @@ using System.Runtime.InteropServices;
8
8
  [assembly: AssemblyCompany("windows-development-sandbox (open source)")]
9
9
  [assembly: AssemblyProduct("Windows Development Sandbox")]
10
10
  [assembly: AssemblyCopyright("MIT License, windows-development-sandbox contributors")]
11
- [assembly: AssemblyVersion("1.9.0.0")]
12
- [assembly: AssemblyFileVersion("1.9.0.0")]
13
- [assembly: AssemblyInformationalVersion("1.9.0")]
11
+ [assembly: AssemblyVersion("1.10.0.0")]
12
+ [assembly: AssemblyFileVersion("1.10.0.0")]
13
+ [assembly: AssemblyInformationalVersion("1.10.0")]
14
14
  [assembly: ComVisible(false)]
@@ -1,107 +1,128 @@
1
- // This PC's memory right now (HostTools memory-status): physical RAM and the commit limit (RAM plus paging files) with
2
- // what is left of each. The commit limit is what a sandbox's memory is charged against, so it matters as much as RAM.
3
- // C# 5.
4
- using System;
5
- using System.ComponentModel;
6
- using System.Globalization;
7
- using System.Runtime.InteropServices;
8
- using Microsoft.Win32;
9
-
10
- namespace AgentSandbox
11
- {
12
- public static class HostMemory
13
- {
14
- private const double BytesPerMb = 1024.0 * 1024.0;
15
-
16
- // MEMORYSTATUSEX.
17
- [StructLayout(LayoutKind.Sequential)]
18
- private struct MemoryStatus
19
- {
20
- public uint Length;
21
- public uint MemoryLoad;
22
- public ulong TotalPhys;
23
- public ulong AvailPhys;
24
- public ulong TotalPageFile;
25
- public ulong AvailPageFile;
26
- public ulong TotalVirtual;
27
- public ulong AvailVirtual;
28
- public ulong AvailExtendedVirtual;
29
- }
30
-
31
- private static class NativeMethods
32
- {
33
- [DllImport("kernel32.dll", SetLastError = true)]
34
- public static extern bool GlobalMemoryStatusEx(ref MemoryStatus status);
35
- }
36
-
37
- /// <summary>{"totalMb":..,"availableMb":..,"commitLimitMb":..,"commitAvailableMb":..}; throws Win32Exception on failure.</summary>
38
- public static string Json()
39
- {
40
- MemoryStatus status = new MemoryStatus();
41
- status.Length = (uint)Marshal.SizeOf(typeof(MemoryStatus));
42
- if (!NativeMethods.GlobalMemoryStatusEx(ref status))
43
- {
44
- throw new Win32Exception(Marshal.GetLastWin32Error());
45
- }
46
- return "{\"totalMb\":" + Mb(status.TotalPhys) +
47
- ",\"availableMb\":" + Mb(status.AvailPhys) +
48
- ",\"commitLimitMb\":" + Mb(status.TotalPageFile) +
49
- ",\"commitAvailableMb\":" + Mb(status.AvailPageFile) +
50
- "," + PageFileJson() + "}";
51
- }
52
-
53
- /// <summary>
54
- /// How this PC's paging file is set up, from the registry (read-only): "auto" (Windows manages it), "fixed"
55
- /// (with the total of the maximum sizes) or "none". An unreadable setting is reported as "auto".
56
- /// </summary>
57
- private static string PageFileJson()
58
- {
59
- string kind = "auto";
60
- long maxMb = 0;
61
- try
62
- {
63
- using (RegistryKey key = Registry.LocalMachine.OpenSubKey(@"SYSTEM\CurrentControlSet\Control\Session Manager\Memory Management"))
64
- {
65
- string[] files = key == null ? null : key.GetValue("PagingFiles") as string[];
66
- if (files != null)
67
- {
68
- kind = "none";
69
- foreach (string entry in files)
70
- {
71
- string[] parts = entry.Trim().Split(' ');
72
- if (entry.Trim().Length == 0)
73
- {
74
- continue;
75
- }
76
- long initial = 0;
77
- long max = 0;
78
- if (parts.Length >= 3)
79
- {
80
- long.TryParse(parts[parts.Length - 2], NumberStyles.Integer, CultureInfo.InvariantCulture, out initial);
81
- long.TryParse(parts[parts.Length - 1], NumberStyles.Integer, CultureInfo.InvariantCulture, out max);
82
- }
83
- if (entry.StartsWith("?:") || (initial == 0 && max == 0))
84
- {
85
- kind = "auto";
86
- maxMb = 0;
87
- break;
88
- }
89
- kind = "fixed";
90
- maxMb += max;
91
- }
92
- }
93
- }
94
- }
95
- catch (Exception)
96
- {
97
- // Not readable: the caller treats "auto" as "no warning".
98
- }
99
- return "\"pageFile\":\"" + kind + "\",\"pageFileMaxMb\":" + maxMb.ToString(CultureInfo.InvariantCulture);
100
- }
101
-
102
- private static string Mb(ulong bytes)
103
- {
104
- return Math.Floor(bytes / BytesPerMb).ToString("0", CultureInfo.InvariantCulture);
105
- }
106
- }
107
- }
1
+ // This PC's memory right now (HostTools memory-status): physical RAM and the commit limit (RAM plus paging files) with
2
+ // what is left of each. The commit limit is what a sandbox's memory is charged against, so it matters as much as RAM.
3
+ // C# 5.
4
+ using System;
5
+ using System.ComponentModel;
6
+ using System.Diagnostics;
7
+ using System.Globalization;
8
+ using System.Runtime.InteropServices;
9
+ using Microsoft.Win32;
10
+
11
+ namespace AgentSandbox
12
+ {
13
+ public static class HostMemory
14
+ {
15
+ private const double BytesPerMb = 1024.0 * 1024.0;
16
+
17
+ // MEMORYSTATUSEX.
18
+ [StructLayout(LayoutKind.Sequential)]
19
+ private struct MemoryStatus
20
+ {
21
+ public uint Length;
22
+ public uint MemoryLoad;
23
+ public ulong TotalPhys;
24
+ public ulong AvailPhys;
25
+ public ulong TotalPageFile;
26
+ public ulong AvailPageFile;
27
+ public ulong TotalVirtual;
28
+ public ulong AvailVirtual;
29
+ public ulong AvailExtendedVirtual;
30
+ }
31
+
32
+ private static class NativeMethods
33
+ {
34
+ [DllImport("kernel32.dll", SetLastError = true)]
35
+ public static extern bool GlobalMemoryStatusEx(ref MemoryStatus status);
36
+ }
37
+
38
+ /// <summary>{"totalMb":..,"availableMb":..,"commitLimitMb":..,"commitAvailableMb":..}; throws Win32Exception on failure.</summary>
39
+ public static string Json()
40
+ {
41
+ MemoryStatus status = new MemoryStatus();
42
+ status.Length = (uint)Marshal.SizeOf(typeof(MemoryStatus));
43
+ if (!NativeMethods.GlobalMemoryStatusEx(ref status))
44
+ {
45
+ throw new Win32Exception(Marshal.GetLastWin32Error());
46
+ }
47
+ return "{\"totalMb\":" + Mb(status.TotalPhys) +
48
+ ",\"availableMb\":" + Mb(status.AvailPhys) +
49
+ ",\"commitLimitMb\":" + Mb(status.TotalPageFile) +
50
+ ",\"commitAvailableMb\":" + Mb(status.AvailPageFile) +
51
+ "," + PageFileJson() + DiskQueueJson() + "}";
52
+ }
53
+
54
+ /// <summary>
55
+ /// How this PC's paging file is set up, from the registry (read-only): "auto" (Windows manages it), "fixed"
56
+ /// (with the total of the maximum sizes) or "none". An unreadable setting is reported as "auto".
57
+ /// </summary>
58
+ private static string PageFileJson()
59
+ {
60
+ string kind = "auto";
61
+ long maxMb = 0;
62
+ try
63
+ {
64
+ using (RegistryKey key = Registry.LocalMachine.OpenSubKey(@"SYSTEM\CurrentControlSet\Control\Session Manager\Memory Management"))
65
+ {
66
+ string[] files = key == null ? null : key.GetValue("PagingFiles") as string[];
67
+ if (files != null)
68
+ {
69
+ kind = "none";
70
+ foreach (string entry in files)
71
+ {
72
+ string[] parts = entry.Trim().Split(' ');
73
+ if (entry.Trim().Length == 0)
74
+ {
75
+ continue;
76
+ }
77
+ long initial = 0;
78
+ long max = 0;
79
+ if (parts.Length >= 3)
80
+ {
81
+ long.TryParse(parts[parts.Length - 2], NumberStyles.Integer, CultureInfo.InvariantCulture, out initial);
82
+ long.TryParse(parts[parts.Length - 1], NumberStyles.Integer, CultureInfo.InvariantCulture, out max);
83
+ }
84
+ if (entry.StartsWith("?:") || (initial == 0 && max == 0))
85
+ {
86
+ kind = "auto";
87
+ maxMb = 0;
88
+ break;
89
+ }
90
+ kind = "fixed";
91
+ maxMb += max;
92
+ }
93
+ }
94
+ }
95
+ }
96
+ catch (Exception)
97
+ {
98
+ // Not readable: the caller treats "auto" as "no warning".
99
+ }
100
+ return "\"pageFile\":\"" + kind + "\",\"pageFileMaxMb\":" + maxMb.ToString(CultureInfo.InvariantCulture);
101
+ }
102
+
103
+ /// <summary>
104
+ /// The disks' queue length now (all disks together), as ,"diskQueue":n; empty when the counter is not available.
105
+ /// Heavy builds through mapped folders show up here before anything else.
106
+ /// </summary>
107
+ private static string DiskQueueJson()
108
+ {
109
+ try
110
+ {
111
+ using (PerformanceCounter counter = new PerformanceCounter("PhysicalDisk", "Current Disk Queue Length", "_Total", true))
112
+ {
113
+ return ",\"diskQueue\":" + Math.Round(counter.NextValue()).ToString("0", CultureInfo.InvariantCulture);
114
+ }
115
+ }
116
+ catch (Exception)
117
+ {
118
+ // No performance counters on this PC: the watchdog then looks at memory and CPU only.
119
+ return "";
120
+ }
121
+ }
122
+
123
+ private static string Mb(ulong bytes)
124
+ {
125
+ return Math.Floor(bytes / BytesPerMb).ToString("0", CultureInfo.InvariantCulture);
126
+ }
127
+ }
128
+ }
@@ -24,11 +24,19 @@ try {
24
24
  }
25
25
 
26
26
  # Win32_OperatingSystem counts in KB.
27
- ConvertTo-Json -Compress -InputObject ([ordered]@{
27
+ $result = [ordered]@{
28
28
  totalMb = [int64][math]::Floor($os.TotalVisibleMemorySize / 1024)
29
29
  availableMb = [int64][math]::Floor($os.FreePhysicalMemory / 1024)
30
30
  commitLimitMb = [int64][math]::Floor($os.TotalVirtualMemorySize / 1024)
31
31
  commitAvailableMb = [int64][math]::Floor($os.FreeVirtualMemory / 1024)
32
32
  pageFile = $pageFile
33
33
  pageFileMaxMb = [int64]$pageFileMax
34
- })
34
+ }
35
+ # The disks' queue length now (all together); left out when the counter is not there.
36
+ try {
37
+ $disk = Get-CimInstance -ClassName Win32_PerfFormattedData_PerfDisk_PhysicalDisk -Filter "Name='_Total'" -ErrorAction Stop
38
+ if ($disk) { $result['diskQueue'] = [int64]$disk.CurrentDiskQueueLength }
39
+ } catch {
40
+ # No counter: memory and CPU only.
41
+ }
42
+ ConvertTo-Json -Compress -InputObject $result
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "windows-development-sandbox",
3
- "version": "1.9.0",
3
+ "version": "1.10.0",
4
4
  "description": "A disposable Windows Sandbox for development, with or without an AI agent (Claude Code, Codex, Gemini, opencode, ...): the folder you run it from is the workspace, toolchains and databases for your project install themselves, and nothing else on your PC is reachable.",
5
5
  "type": "module",
6
6
  "license": "MIT",