windows-development-sandbox 1.14.0 → 1.16.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 (100) hide show
  1. package/config.example.json +2 -1
  2. package/dist/app/features/accounts/accounts.def.js +4 -3
  3. package/dist/app/features/defs.d.ts +4 -2
  4. package/dist/app/features/defs.js +26 -12
  5. package/dist/app/features/projects/projects.def.d.ts +1 -1
  6. package/dist/app/features/projects/projects.run.d.ts +1 -1
  7. package/dist/app/features/projects/projects.run.js +10 -2
  8. package/dist/app/features/sandbox/sandbox.def.js +12 -6
  9. package/dist/app/features/sessions/sessions.def.js +1 -1
  10. package/dist/app/features/settings/categories.def.js +1 -0
  11. package/dist/bin/agent-sandbox.js +2 -2
  12. package/dist/bin/{chunk-F7IMME5G.js → chunk-7EXDUNE7.js} +98 -79
  13. package/dist/bin/{chunk-BQ23GBLC.js → chunk-HIC4CUMY.js} +1925 -1430
  14. package/dist/bin/{chunk-LQHEUN2A.js → chunk-PXTLKIV4.js} +1 -1
  15. package/dist/bin/claude-host.js +2 -2
  16. package/dist/bin/claude-sbx.js +2 -2
  17. package/dist/bin/codex-sbx.js +2 -2
  18. package/dist/bin/gemini-sbx.js +2 -2
  19. package/dist/bin/sandbox.js +2 -2
  20. package/dist/bin/sbx-hide.js +2 -2
  21. package/dist/bin/sbx-show.js +2 -2
  22. package/dist/cli/commands/projects/here.js +2 -0
  23. package/dist/cli/index.js +4 -1
  24. package/dist/cli/report.d.ts +7 -0
  25. package/dist/cli/report.js +31 -0
  26. package/dist/cli/services/health-service.js +15 -3
  27. package/dist/cli/services/projects-service.d.ts +8 -3
  28. package/dist/cli/services/projects-service.js +21 -4
  29. package/dist/core/config/defaults.js +1 -0
  30. package/dist/core/schema/config.d.ts +4 -0
  31. package/dist/core/schema/projects.d.ts +4 -0
  32. package/dist/core/schema/projects.js +3 -0
  33. package/dist/guest/boot/boot.js +8 -2
  34. package/dist/guest/daemon/daemon.js +3 -0
  35. package/dist/guest/daemon/live-folders.d.ts +48 -0
  36. package/dist/guest/daemon/live-folders.js +245 -0
  37. package/dist/guest/daemon/live.d.ts +4 -0
  38. package/dist/guest/daemon/live.js +57 -0
  39. package/dist/guest/entries/daemon.js +4 -2
  40. package/dist/guest/runtime/csc.d.ts +1 -0
  41. package/dist/guest/runtime/csc.js +1 -0
  42. package/dist/guest/runtime/secrets.d.ts +5 -2
  43. package/dist/guest/runtime/secrets.js +12 -5
  44. package/dist/host/engines/hcs/live-folders.d.ts +117 -0
  45. package/dist/host/engines/hcs/live-folders.js +347 -0
  46. package/dist/host/engines/index.d.ts +1 -0
  47. package/dist/host/engines/index.js +1 -0
  48. package/dist/host/launch/generate.js +8 -1
  49. package/dist/host/projects/join.d.ts +4 -0
  50. package/dist/host/projects/join.js +11 -3
  51. package/dist/host/projects/mapper.d.ts +34 -5
  52. package/dist/host/projects/mapper.js +88 -20
  53. package/dist/host/projects/registry.d.ts +2 -0
  54. package/dist/host/projects/registry.js +1 -1
  55. package/dist/host/secrets/launch-secrets.js +2 -1
  56. package/dist/runtime/agent.js +14 -0
  57. package/dist/runtime/daemon.js +466 -132
  58. package/dist/runtime/db.js +14 -0
  59. package/dist/runtime/main.js +26 -3
  60. package/dist/runtime/netproxy.js +14 -0
  61. package/dist/runtime/view.js +14 -0
  62. package/dist/selftest/accounts.test.js +22 -0
  63. package/dist/selftest/app.test.js +2 -2
  64. package/dist/selftest/index.js +2 -0
  65. package/dist/selftest/layout.test.js +1 -0
  66. package/dist/selftest/live-folders.test.d.ts +2 -0
  67. package/dist/selftest/live-folders.test.js +588 -0
  68. package/dist/selftest/projects.test.js +7 -7
  69. package/dist/selftest/support/engine-golden.js +1 -0
  70. package/dist/selftest/support/fixtures.d.ts +1 -0
  71. package/dist/selftest/support/live.js +2 -0
  72. package/dist/shared/channel/index.d.ts +1 -0
  73. package/dist/shared/channel/index.js +1 -0
  74. package/dist/shared/channel/live.d.ts +53 -0
  75. package/dist/shared/channel/live.js +92 -0
  76. package/dist/shared/channel/run-dir.d.ts +5 -0
  77. package/dist/shared/channel/run-dir.js +11 -0
  78. package/dist/shared/channel/secrets.d.ts +1 -0
  79. package/dist/shared/channel/secrets.js +5 -0
  80. package/dist/shared/paths/projects.d.ts +6 -1
  81. package/dist/shared/paths/projects.js +21 -1
  82. package/dist/sync/watcher.js +9 -0
  83. package/dist/web/index.html +2 -2
  84. package/dist/web/main-JTH3FT4O.js +221 -0
  85. package/dist/web/{main-UAQPI5XD.css → main-XT7T6ODM.css} +1 -1
  86. package/native/csharp/HcsHost/AssemblyInfo.cs +4 -4
  87. package/native/csharp/HcsHost/FolderAccess.cs +361 -0
  88. package/native/csharp/HcsHost/Hcs.cs +31 -3
  89. package/native/csharp/HcsHost/Program.cs +54 -6
  90. package/native/csharp/HcsHost/Share.cs +64 -11
  91. package/native/csharp/HcsHost/WsbGuard.cs +130 -0
  92. package/native/csharp/HcsHost/WsbShare.cs +295 -0
  93. package/native/csharp/HostTools/AssemblyInfo.cs +3 -3
  94. package/native/csharp/StorageHelper/AssemblyInfo.cs +3 -3
  95. package/native/scripts/contained-common.ps1 +4 -0
  96. package/native/scripts/hcs-live-share.ps1 +191 -0
  97. package/package.json +1 -1
  98. package/sandbox/csharp/BindMount.cs +120 -0
  99. package/schemas/config.schema.json +8 -0
  100. package/dist/web/main-6XNBTJLT.js +0 -221
@@ -0,0 +1,191 @@
1
+ #Requires -Version 5.1
2
+ # Live folders (opt-in alpha, docs/design/hcs-live-folders.md): adds or removes one folder share on the running Windows
3
+ # Sandbox through HcsHost.exe wsb-share. Run elevated (host/engines/hcs/live-folders.ts starts it through
4
+ # run-elevated.ps1: one Windows permission prompt per add or remove). The request is a JSON file in a private temp
5
+ # folder: { action, id, name, path, readOnly, storageRoot, dataPath, userSid }; its SHA-256 is on the command line, and
6
+ # HcsHost hashes the exact bytes it parses again.
7
+ # Safety, all checked here again: the temp folder is the user's and not a link; HcsHost.exe is compiled here, from the
8
+ # sources next to this script (copied first), into a fresh folder under %ProgramData%\AgentSandboxLiveFolders that only
9
+ # administrators and SYSTEM may use (checked, and repaired when someone else made it, on every run), never taken from
10
+ # the caller; one add or remove at a time (a machine-wide lock). HcsHost does the rest (the caller must be the account
11
+ # that answered the prompt, which compute system, which folder, the canary). Work folders left by an earlier run (a
12
+ # canary the sandbox kept busy) are swept. -DryRun checks the request and prints what it would do, and does nothing
13
+ # else. The result goes to result.json next to the request file.
14
+ param(
15
+ [Parameter(Mandatory = $true)][string]$Json,
16
+ [string]$Sha256 = '',
17
+ [switch]$DryRun
18
+ )
19
+ $ErrorActionPreference = 'Stop'
20
+ Set-StrictMode -Version 2.0
21
+ . (Join-Path $PSScriptRoot 'contained-common.ps1')
22
+
23
+ $result = @{ ok = $false; message = ''; id = ''; canary = '' }
24
+ $AdminSids = @('S-1-5-18', 'S-1-5-32-544')
25
+
26
+ function Assert-LiveRequest($Req) {
27
+ foreach ($name in @('action', 'name', 'userSid')) {
28
+ if (-not ($Req.PSObject.Properties.Name -contains $name) -or -not $Req.$name) { throw "The request has no $name." }
29
+ }
30
+ if (@('add', 'remove') -notcontains [string]$Req.action) { throw 'The action must be add or remove.' }
31
+ if ([string]$Req.name -cnotmatch '^agentsbx-[0-9a-f]{8,32}$') { throw "Not one of this tool's share names: $($Req.name)" }
32
+ if ([string]$Req.userSid -notmatch '^S-1-[0-9-]{3,120}$') { throw 'The request has no valid user.' }
33
+ if ($Req.PSObject.Properties.Name -contains 'id' -and $Req.id -and ([string]$Req.id -notmatch '^[0-9A-Fa-f]{8}-([0-9A-Fa-f]{4}-){3}[0-9A-Fa-f]{12}$')) {
34
+ throw 'The sandbox id must be a GUID.'
35
+ }
36
+ if ($Req.action -eq 'remove' -and -not $Req.id) { throw 'A remove needs the id of the sandbox the folder was added to.' }
37
+ if ($Req.action -eq 'add') {
38
+ if (-not ($Req.PSObject.Properties.Name -contains 'path') -or -not (Test-PlainAbsolute $Req.path)) { throw 'The request has no plain full path to share.' }
39
+ if (-not ($Req.PSObject.Properties.Name -contains 'readOnly') -or -not ($Req.readOnly -is [bool])) { throw 'readOnly must be true or false.' }
40
+ foreach ($name in @('storageRoot', 'dataPath')) {
41
+ if (-not ($Req.PSObject.Properties.Name -contains $name) -or -not (Test-PlainAbsolute $Req.$name)) { throw "The request has no plain full $name." }
42
+ }
43
+ }
44
+ }
45
+
46
+ # The request's own folder: a private temp folder of the user who asked, not a link.
47
+ function Assert-PrivateTemp($Req, [string]$Dir) {
48
+ if (Test-ReparsePoint $Dir) { throw "Refusing a link: $Dir" }
49
+ $acl = Get-Acl -LiteralPath $Dir
50
+ $owner = $acl.GetOwner([Security.Principal.SecurityIdentifier]).Value
51
+ if (@($Req.userSid, 'S-1-5-32-544') -notcontains $owner) { throw "The request folder is not yours (owner $owner)." }
52
+ foreach ($rule in $acl.GetAccessRules($true, $true, [Security.Principal.SecurityIdentifier])) {
53
+ if ($rule.AccessControlType -ne 'Allow') { continue }
54
+ if (@('S-1-1-0', 'S-1-5-11', 'S-1-5-32-545') -contains $rule.IdentityReference.Value) { throw 'The request folder is open to other users.' }
55
+ }
56
+ }
57
+
58
+ function New-AdminSecurity {
59
+ $acl = New-Object Security.AccessControl.DirectorySecurity
60
+ $acl.SetOwner((New-Object Security.Principal.SecurityIdentifier('S-1-5-32-544')))
61
+ $acl.SetAccessRuleProtection($true, $false)
62
+ $inherit = [Security.AccessControl.InheritanceFlags]'ContainerInherit, ObjectInherit'
63
+ foreach ($sid in $AdminSids) {
64
+ $who = New-Object Security.Principal.SecurityIdentifier($sid)
65
+ $acl.AddAccessRule((New-Object Security.AccessControl.FileSystemAccessRule($who, 'FullControl', $inherit, 'None', 'Allow')))
66
+ }
67
+ return $acl
68
+ }
69
+
70
+ function Test-AdminOnly([string]$Path) {
71
+ $acl = Get-Acl -LiteralPath $Path
72
+ if ($AdminSids -notcontains $acl.GetOwner([Security.Principal.SecurityIdentifier]).Value) { return $false }
73
+ if (-not $acl.AreAccessRulesProtected) { return $false }
74
+ foreach ($rule in $acl.GetAccessRules($true, $true, [Security.Principal.SecurityIdentifier])) {
75
+ if ($rule.AccessControlType -eq 'Allow' -and $AdminSids -notcontains $rule.IdentityReference.Value) { return $false }
76
+ }
77
+ return $true
78
+ }
79
+
80
+ # %ProgramData%\AgentSandboxLiveFolders: made with an administrator-only DACL. One that someone else made first (any
81
+ # user may create folders in ProgramData) is taken over: owner Administrators and the DACL written whole; its old
82
+ # content stays, unused (each run works in a new random folder made with its own DACL).
83
+ function Get-AdminParent {
84
+ $base = [Environment]::GetFolderPath('CommonApplicationData')
85
+ if (-not $base -or (Test-ReparsePoint $base)) { throw "ProgramData is missing or a link: $base" }
86
+ $parent = Join-Path $base 'AgentSandboxLiveFolders'
87
+ if (-not (Test-Path -LiteralPath $parent)) {
88
+ [void][IO.Directory]::CreateDirectory($parent, (New-AdminSecurity))
89
+ }
90
+ if (Test-ReparsePoint $parent) { throw "Refusing a link: $parent" }
91
+ if (-not (Test-AdminOnly $parent)) {
92
+ Add-Note "taking over $parent (someone else made it or may use it)"
93
+ Set-Acl -LiteralPath $parent -AclObject (New-AdminSecurity)
94
+ if ((Test-ReparsePoint $parent) -or -not (Test-AdminOnly $parent)) { throw "$parent could not be made administrator-only." }
95
+ }
96
+ # Work folders an earlier run could not delete (the sandbox kept its canary busy): gone after an hour.
97
+ foreach ($old in @(Get-ChildItem -LiteralPath $parent -Force -Directory -ErrorAction SilentlyContinue)) {
98
+ if ($old.Name -match '^[0-9a-f]{32}$' -and -not ($old.Attributes -band [IO.FileAttributes]::ReparsePoint) -and $old.CreationTimeUtc -lt [DateTime]::UtcNow.AddHours(-1)) {
99
+ Remove-Item -LiteralPath $old.FullName -Recurse -Force -ErrorAction SilentlyContinue
100
+ }
101
+ }
102
+ return $parent
103
+ }
104
+
105
+ # HcsHost.exe built here from this package's sources (copied into the admin-only work folder first), with the
106
+ # compiler that ships with Windows: nothing the caller names is ever run elevated.
107
+ function Build-HcsHost([string]$Work) {
108
+ $sources = [IO.Path]::GetFullPath((Join-Path $PSScriptRoot '..\csharp\HcsHost'))
109
+ if (-not (Test-Path -LiteralPath $sources -PathType Container) -or (Test-ReparsePoint $sources)) { throw "The HcsHost sources are missing or a link: $sources" }
110
+ $copy = Join-Path $Work 'src'
111
+ [void][IO.Directory]::CreateDirectory($copy)
112
+ foreach ($file in @(Get-ChildItem -LiteralPath $sources -Filter '*.cs' -File)) {
113
+ if ($file.Attributes -band [IO.FileAttributes]::ReparsePoint) { throw "Refusing a link: $($file.FullName)" }
114
+ [IO.File]::Copy($file.FullName, (Join-Path $copy $file.Name), $false)
115
+ }
116
+ $csc = Join-Path ([Environment]::GetFolderPath('Windows')) 'Microsoft.NET\Framework64\v4.0.30319\csc.exe'
117
+ $exe = Join-Path $Work 'HcsHost.exe'
118
+ $files = @(Get-ChildItem -LiteralPath $copy -Filter '*.cs' -File | ForEach-Object { $_.FullName })
119
+ if ($files.Count -eq 0) { throw 'No HcsHost sources were found.' }
120
+ & $csc '/nologo' '/optimize' '/target:exe' '/r:System.Web.Extensions.dll' ('/out:' + $exe) @files | Out-Null
121
+ if (-not (Test-Path -LiteralPath $exe -PathType Leaf)) { throw 'HcsHost.exe could not be built (the .NET Framework compiler that ships with Windows is needed).' }
122
+ return $exe
123
+ }
124
+
125
+ function Invoke-Live($Req, [string]$JsonPath, [string]$Checksum) {
126
+ $parent = Get-AdminParent
127
+ $work = Join-Path $parent ([Guid]::NewGuid().ToString('N'))
128
+ if (Test-Path -LiteralPath $work) { throw "The work folder exists already: $work" }
129
+ [void][IO.Directory]::CreateDirectory($work, (New-AdminSecurity))
130
+ try {
131
+ $exe = Build-HcsHost $work
132
+ $out = Join-Path $work 'reply.json'
133
+ & $exe 'wsb-share' $JsonPath '--sha256' $Checksum '--out' $out | Out-Null
134
+ if (-not (Test-Path -LiteralPath $out)) { throw 'HcsHost.exe gave no answer.' }
135
+ $bytes = [IO.File]::ReadAllBytes($out)
136
+ if ($bytes.Length -gt 65536) { throw 'HcsHost.exe answered with too much text.' }
137
+ $reply = [Text.Encoding]::UTF8.GetString($bytes) | ConvertFrom-Json
138
+ $script:result.ok = [bool]$reply.ok
139
+ $script:result.message = [string]$reply.message
140
+ $names = @($reply.values.PSObject.Properties | ForEach-Object { $_.Name })
141
+ if ($names -contains 'id') { $script:result.id = [string]$reply.values.id }
142
+ if ($names -contains 'canary') { $script:result.canary = [string]$reply.values.canary }
143
+ } finally {
144
+ if (-not (Test-ReparsePoint $work)) { Remove-Item -LiteralPath $work -Recurse -Force -ErrorAction SilentlyContinue }
145
+ }
146
+ }
147
+
148
+ # result.json in the caller's folder: checked for a link right before, and only made as a new file.
149
+ function Write-LiveResult([hashtable]$Result, [string]$JsonPath) {
150
+ $Result.notes = @(Get-Notes)
151
+ $dir = Split-Path -Parent $JsonPath
152
+ if (Test-ReparsePoint $dir) { return }
153
+ $bytes = (New-Object Text.UTF8Encoding($false)).GetBytes(($Result | ConvertTo-Json -Compress))
154
+ $stream = New-Object IO.FileStream((Join-Path $dir 'result.json'), [IO.FileMode]::CreateNew, [IO.FileAccess]::Write, [IO.FileShare]::None)
155
+ try { $stream.Write($bytes, 0, $bytes.Length) } finally { $stream.Dispose() }
156
+ }
157
+
158
+ $mutex = $null
159
+ $owned = $false
160
+ try {
161
+ $request = Read-ContainedRequest -JsonPath $Json -Sha256 $Sha256 -Dry $DryRun.IsPresent
162
+ Assert-LiveRequest $request
163
+ if ($DryRun) {
164
+ Add-Note ("would {0} the share {1}{2} on the running Windows Sandbox{3}" -f $request.action, $request.name, $(if ($request.action -eq 'add') { ' (' + $request.path + ', ' + $(if ($request.readOnly) { 'read-only' } else { 'writable' }) + ')' } else { '' }), $(if ($request.id) { ' ' + $request.id } else { '' }))
165
+ $result.ok = $true
166
+ $result.message = 'checked'
167
+ } else {
168
+ if (-not (Test-IsAdministrator)) { throw 'Adding a folder to the running sandbox needs administrator rights.' }
169
+ Assert-PrivateTemp $request (Split-Path -Parent $Json)
170
+ $mutex = New-Object System.Threading.Mutex($false, 'Global\AgentSandboxLiveFolders')
171
+ try { $owned = $mutex.WaitOne(120000) } catch [System.Threading.AbandonedMutexException] { $owned = $true }
172
+ if (-not $owned) { throw 'Another live folder is being added or removed (maybe by another user of this PC). Try again in a few minutes.' }
173
+ Invoke-Live $request $Json $Sha256
174
+ }
175
+ } catch {
176
+ $result.ok = $false
177
+ $result.message = $_.Exception.Message
178
+ } finally {
179
+ if ($mutex) {
180
+ if ($owned) { $mutex.ReleaseMutex() }
181
+ $mutex.Dispose()
182
+ }
183
+ }
184
+ if ($DryRun) {
185
+ Write-ContainedResult $result $Json $true
186
+ } else {
187
+ try { Write-LiveResult $result $Json } catch { [Console]::Error.WriteLine('The result could not be written: ' + $_.Exception.Message) }
188
+ }
189
+ if ($result.ok) { exit 0 }
190
+ if ($DryRun) { [Console]::Error.WriteLine($result.message) }
191
+ exit 1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "windows-development-sandbox",
3
- "version": "1.14.0",
3
+ "version": "1.16.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",
@@ -0,0 +1,120 @@
1
+ // Live folders, guest side (docs/design/hcs-live-folders.md): shows a vSMB share the host added to this running sandbox
2
+ // at C:\Workspace\<name> with the bind filter, the way Windows Sandbox's own guest agent shows .wsb mapped folders, so
3
+ // tools see a plain folder (not a link). Run by the session daemon (an administrator) by path; compiled inside the
4
+ // sandbox with the .NET Framework csc.exe (C# 5). Verbs (exit 0 = done, 1 = failed, 2 = refused, 3 = no such share yet):
5
+ // BindMount map <C:\Workspace\name> <agentsbx-share> [rw] the folder shows the share (read-only unless rw)
6
+ // BindMount unmap <C:\Workspace\name> removes that mapping again
7
+ // BindMount mapped <C:\Workspace\name> exit 0 when the bind filter has a mapping there, else 4
8
+ // Only share names with this tool's prefix and only C:\Workspace\<plain name> (no dots or spaces, so no "..", "foo."
9
+ // or "foo " aliases of other folders), so it can never re-point a folder Windows Sandbox mapped. Flag names and the
10
+ // BfGetMappings layout: go-winio pkg/bindfilter (vendored in microsoft/hcsshim) and BuildXL's NativeContainerUtilities.
11
+ using System;
12
+ using System.IO;
13
+ using System.Runtime.InteropServices;
14
+ using System.Text;
15
+ using System.Text.RegularExpressions;
16
+
17
+ static class BindMount
18
+ {
19
+ private const string VsmbRoot = "\\\\?\\GLOBALROOT\\Device\\vmsmb\\VSMB-{dcc079ae-60ba-4d07-847c-3493609c0870}\\";
20
+ private const uint ReadOnlyMapping = 0x1;
21
+ private const uint NoMultipleTargets = 0x40;
22
+ private const uint GetMappingsVolume = 0x1;
23
+ /// <summary>
24
+ /// A share that has not arrived: ERROR_NOT_FOUND (seen in a Windows Sandbox, 2026-10-23, with C:\Workspace there) or
25
+ /// ERROR_FILE_NOT_FOUND (what the design note recorded; also the answer when C:\Workspace itself is missing).
26
+ /// </summary>
27
+ private const int NotFound = unchecked((int)0x80070490);
28
+ private const int FileNotFound = unchecked((int)0x80070002);
29
+ private const int ExitOk = 0;
30
+ private const int ExitFailed = 1;
31
+ private const int ExitRefused = 2;
32
+ private const int ExitNoShare = 3;
33
+ private const int ExitNotMapped = 4;
34
+ private static readonly Regex ShareName = new Regex("^agentsbx-[0-9a-f]{8,32}$");
35
+ private static readonly Regex GuestFolder = new Regex("^C:\\\\Workspace\\\\[a-z0-9][a-z0-9_-]{0,63}$", RegexOptions.IgnoreCase);
36
+
37
+ private static class NativeMethods
38
+ {
39
+ [DllImport("bindfltapi.dll", CharSet = CharSet.Unicode, ExactSpelling = true)]
40
+ public static extern int BfSetupFilter(IntPtr job, uint flags, string virtRoot, string target, IntPtr exceptions, uint exceptionCount);
41
+
42
+ [DllImport("bindfltapi.dll", CharSet = CharSet.Unicode, ExactSpelling = true)]
43
+ public static extern int BfRemoveMapping(IntPtr job, string virtRoot);
44
+
45
+ [DllImport("bindfltapi.dll", CharSet = CharSet.Unicode, ExactSpelling = true)]
46
+ public static extern int BfGetMappings(uint flags, IntPtr job, string virtRoot, IntPtr sid, ref uint bufferSize, byte[] buffer);
47
+ }
48
+
49
+ static int Main(string[] args)
50
+ {
51
+ if ((args.Length == 3 || args.Length == 4) && args[0] == "map")
52
+ {
53
+ if (!GuestFolder.IsMatch(args[1]) || !ShareName.IsMatch(args[2]) || (args.Length == 4 && args[3] != "rw"))
54
+ {
55
+ Console.WriteLine("Refused: map C:\\Workspace\\<plain name> to an agentsbx- share only");
56
+ return ExitRefused;
57
+ }
58
+ if (Directory.Exists(args[1]) || File.Exists(args[1]))
59
+ {
60
+ Console.WriteLine("Refused: " + args[1] + " exists already");
61
+ return ExitRefused;
62
+ }
63
+ uint flags = NoMultipleTargets | (args.Length == 4 ? 0u : ReadOnlyMapping);
64
+ int hr = NativeMethods.BfSetupFilter(IntPtr.Zero, flags, args[1], VsmbRoot + args[2] + "\\", IntPtr.Zero, 0);
65
+ Console.WriteLine("BfSetupFilter 0x" + hr.ToString("X8"));
66
+ return hr == 0 ? ExitOk : (hr == NotFound || hr == FileNotFound ? ExitNoShare : ExitFailed);
67
+ }
68
+ if (args.Length == 2 && (args[0] == "unmap" || args[0] == "mapped"))
69
+ {
70
+ if (!GuestFolder.IsMatch(args[1]))
71
+ {
72
+ Console.WriteLine("Refused: only C:\\Workspace\\<plain name>");
73
+ return ExitRefused;
74
+ }
75
+ if (args[0] == "mapped")
76
+ {
77
+ bool found;
78
+ int listed = HasMapping(args[1], out found);
79
+ Console.WriteLine("BfGetMappings 0x" + listed.ToString("X8") + (found ? " mapped" : " not mapped"));
80
+ return listed != 0 ? ExitFailed : (found ? ExitOk : ExitNotMapped);
81
+ }
82
+ int hr = NativeMethods.BfRemoveMapping(IntPtr.Zero, args[1]);
83
+ Console.WriteLine("BfRemoveMapping 0x" + hr.ToString("X8"));
84
+ return hr == 0 ? ExitOk : ExitFailed;
85
+ }
86
+ Console.WriteLine("Usage: BindMount map <folder> <share> [rw] | unmap <folder> | mapped <folder>");
87
+ return ExitRefused;
88
+ }
89
+
90
+ /// <summary>Whether the bind filter has a mapping whose virtual root is `folder` (BfGetMappings on C:).</summary>
91
+ private static int HasMapping(string folder, out bool found)
92
+ {
93
+ found = false;
94
+ uint size = 1024 * 1024;
95
+ byte[] buffer = new byte[size];
96
+ int hr = NativeMethods.BfGetMappings(GetMappingsVolume, IntPtr.Zero, "C:\\", IntPtr.Zero, ref size, buffer);
97
+ if (hr != 0 || size < 12)
98
+ {
99
+ return hr != 0 ? hr : ExitFailed;
100
+ }
101
+ uint count = BitConverter.ToUInt32(buffer, 8);
102
+ string wanted = "\\" + folder.Substring(3).TrimEnd('\\');
103
+ for (int i = 0; i < count && 12 + (i * 20) + 20 <= buffer.Length; i++)
104
+ {
105
+ int entry = 12 + (i * 20);
106
+ uint length = BitConverter.ToUInt32(buffer, entry);
107
+ uint offset = BitConverter.ToUInt32(buffer, entry + 4);
108
+ if (offset + length > buffer.Length)
109
+ {
110
+ continue;
111
+ }
112
+ string root = Encoding.Unicode.GetString(buffer, (int)offset, (int)length).TrimEnd('\\');
113
+ if (root.EndsWith(wanted, StringComparison.OrdinalIgnoreCase))
114
+ {
115
+ found = true;
116
+ }
117
+ }
118
+ return 0;
119
+ }
120
+ }
@@ -1801,6 +1801,14 @@
1801
1801
  "wsbShare": {
1802
1802
  "type": "boolean",
1803
1803
  "description": "On Windows 11 24H2+ with the Windows Sandbox command line (wsb), add other folders to the running sandbox with `wsb share` (untested). Otherwise starting a project outside every root offers a restart."
1804
+ },
1805
+ "liveFolders": {
1806
+ "type": "string",
1807
+ "enum": [
1808
+ "off",
1809
+ "hcs"
1810
+ ],
1811
+ "description": "ALPHA, UNTESTED ON A REAL PC: \"hcs\" adds a project folder to the running sandbox through the Host Compute Service where `wsb share` is missing (one administrator prompt per add or remove; always read-only in this alpha). If it fails, the sandbox may close: save your work first. Turn it on before the sandbox starts. \"off\" (default) offers a restart instead. The real-PC tests still to pass are in docs/design/hcs-live-folders.md (\"Test plan\")."
1804
1812
  }
1805
1813
  },
1806
1814
  "additionalProperties": false,