windows-development-sandbox 1.14.0 → 1.15.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 (90) hide show
  1. package/config.example.json +2 -1
  2. package/dist/app/features/defs.d.ts +1 -1
  3. package/dist/app/features/projects/projects.def.d.ts +1 -1
  4. package/dist/app/features/projects/projects.run.d.ts +1 -1
  5. package/dist/app/features/projects/projects.run.js +10 -2
  6. package/dist/app/features/sandbox/sandbox.def.js +3 -1
  7. package/dist/app/features/settings/categories.def.js +1 -0
  8. package/dist/bin/agent-sandbox.js +2 -2
  9. package/dist/bin/{chunk-BQ23GBLC.js → chunk-D7CM63BS.js} +1893 -1405
  10. package/dist/bin/{chunk-F7IMME5G.js → chunk-HTQT5SBY.js} +1 -1
  11. package/dist/bin/{chunk-LQHEUN2A.js → chunk-RQ4BWBYX.js} +1 -1
  12. package/dist/bin/claude-host.js +2 -2
  13. package/dist/bin/claude-sbx.js +2 -2
  14. package/dist/bin/codex-sbx.js +2 -2
  15. package/dist/bin/gemini-sbx.js +2 -2
  16. package/dist/bin/sandbox.js +2 -2
  17. package/dist/bin/sbx-hide.js +2 -2
  18. package/dist/bin/sbx-show.js +2 -2
  19. package/dist/cli/commands/projects/here.js +2 -0
  20. package/dist/cli/services/health-service.js +15 -3
  21. package/dist/cli/services/projects-service.d.ts +8 -3
  22. package/dist/cli/services/projects-service.js +21 -4
  23. package/dist/core/config/defaults.js +1 -0
  24. package/dist/core/schema/config.d.ts +4 -0
  25. package/dist/core/schema/projects.d.ts +4 -0
  26. package/dist/core/schema/projects.js +3 -0
  27. package/dist/guest/boot/boot.js +8 -2
  28. package/dist/guest/daemon/daemon.js +3 -0
  29. package/dist/guest/daemon/live-folders.d.ts +48 -0
  30. package/dist/guest/daemon/live-folders.js +245 -0
  31. package/dist/guest/daemon/live.d.ts +4 -0
  32. package/dist/guest/daemon/live.js +57 -0
  33. package/dist/guest/entries/daemon.js +4 -2
  34. package/dist/guest/runtime/csc.d.ts +1 -0
  35. package/dist/guest/runtime/csc.js +1 -0
  36. package/dist/guest/runtime/secrets.d.ts +5 -2
  37. package/dist/guest/runtime/secrets.js +12 -5
  38. package/dist/host/engines/hcs/live-folders.d.ts +117 -0
  39. package/dist/host/engines/hcs/live-folders.js +347 -0
  40. package/dist/host/engines/index.d.ts +1 -0
  41. package/dist/host/engines/index.js +1 -0
  42. package/dist/host/launch/generate.js +8 -1
  43. package/dist/host/projects/join.d.ts +4 -0
  44. package/dist/host/projects/join.js +11 -3
  45. package/dist/host/projects/mapper.d.ts +34 -5
  46. package/dist/host/projects/mapper.js +88 -20
  47. package/dist/host/projects/registry.d.ts +2 -0
  48. package/dist/host/projects/registry.js +1 -1
  49. package/dist/host/secrets/launch-secrets.js +2 -1
  50. package/dist/runtime/agent.js +14 -0
  51. package/dist/runtime/daemon.js +466 -132
  52. package/dist/runtime/db.js +14 -0
  53. package/dist/runtime/main.js +26 -3
  54. package/dist/runtime/netproxy.js +14 -0
  55. package/dist/runtime/view.js +14 -0
  56. package/dist/selftest/index.js +2 -0
  57. package/dist/selftest/layout.test.js +1 -0
  58. package/dist/selftest/live-folders.test.d.ts +2 -0
  59. package/dist/selftest/live-folders.test.js +588 -0
  60. package/dist/selftest/projects.test.js +7 -7
  61. package/dist/selftest/support/engine-golden.js +1 -0
  62. package/dist/selftest/support/fixtures.d.ts +1 -0
  63. package/dist/selftest/support/live.js +2 -0
  64. package/dist/shared/channel/index.d.ts +1 -0
  65. package/dist/shared/channel/index.js +1 -0
  66. package/dist/shared/channel/live.d.ts +53 -0
  67. package/dist/shared/channel/live.js +92 -0
  68. package/dist/shared/channel/run-dir.d.ts +5 -0
  69. package/dist/shared/channel/run-dir.js +11 -0
  70. package/dist/shared/channel/secrets.d.ts +1 -0
  71. package/dist/shared/channel/secrets.js +5 -0
  72. package/dist/shared/paths/projects.d.ts +6 -1
  73. package/dist/shared/paths/projects.js +21 -1
  74. package/dist/sync/watcher.js +9 -0
  75. package/dist/web/index.html +1 -1
  76. package/dist/web/{main-6XNBTJLT.js → main-7DJZ2M27.js} +1 -1
  77. package/native/csharp/HcsHost/AssemblyInfo.cs +4 -4
  78. package/native/csharp/HcsHost/FolderAccess.cs +361 -0
  79. package/native/csharp/HcsHost/Hcs.cs +31 -3
  80. package/native/csharp/HcsHost/Program.cs +54 -6
  81. package/native/csharp/HcsHost/Share.cs +64 -11
  82. package/native/csharp/HcsHost/WsbGuard.cs +130 -0
  83. package/native/csharp/HcsHost/WsbShare.cs +295 -0
  84. package/native/csharp/HostTools/AssemblyInfo.cs +3 -3
  85. package/native/csharp/StorageHelper/AssemblyInfo.cs +3 -3
  86. package/native/scripts/contained-common.ps1 +4 -0
  87. package/native/scripts/hcs-live-share.ps1 +191 -0
  88. package/package.json +1 -1
  89. package/sandbox/csharp/BindMount.cs +120 -0
  90. package/schemas/config.schema.json +8 -0
@@ -1,5 +1,6 @@
1
1
  import type { Missing } from '../../core/local-config/index.js';
2
2
  import { type Wsb } from '../sandbox/index.js';
3
+ import { type LiveFolders } from '../engines/index.js';
3
4
  import { type RegistryEntry } from './registry.js';
4
5
  /**
5
6
  * Why a folder cannot join the running sandbox without a restart (the caller offers one). With `missing`: it could
@@ -14,16 +15,44 @@ export declare class NeedsRestart extends Error {
14
15
  export interface ProjectMapper {
15
16
  /** The projects of the running sandbox (the launch folder first). */
16
17
  list(): RegistryEntry[];
17
- /** Makes a folder of this PC a project of the running sandbox; throws NeedsRestart when that needs a restart. */
18
- register(hostDir: string): RegistryEntry;
19
- /** Forgets a project (never the launch folder). Its folder stays visible until the next start if it is mapped. */
20
- unregister(name: string): void;
18
+ /**
19
+ * Makes a folder of this PC a project of the running sandbox; throws NeedsRestart when that needs a restart.
20
+ * `readOnly`: how the project's own settings would map it (a live folder is read-only when this is not known).
21
+ */
22
+ register(hostDir: string, o?: {
23
+ readOnly?: boolean;
24
+ onNote?: (note: string) => void;
25
+ }): Promise<RegistryEntry>;
26
+ /**
27
+ * Forgets a project (never the launch folder). A live folder (hcs) is hidden in the sandbox and its share removed;
28
+ * any other mapped folder stays visible until the next start.
29
+ */
30
+ unregister(name: string): Promise<void>;
21
31
  }
32
+ /** The parts of LiveFolders the mapper uses (a test hands in a fake). */
33
+ export type LivePort = Pick<LiveFolders, 'add' | 'remove' | 'unavailable' | 'shareOf'>;
22
34
  export interface MapperOptions {
23
35
  storageRoot: string;
24
36
  packageRoot: string;
25
37
  wsbShare: boolean;
38
+ /** projects.liveFolders: "hcs" adds folders through HCS where wsb share is missing (alpha). */
39
+ liveFolders?: 'off' | 'hcs';
40
+ /** The data folder of the running sandbox (needed for live folders). */
41
+ dataPath?: string;
26
42
  detect?: () => Wsb | null;
43
+ live?: () => LivePort;
44
+ /** This Windows user started the running sandbox (not another user's, shared or not). */
45
+ isMine?: () => boolean;
27
46
  }
28
- /** Roots first (instant), then `wsb share` where the Windows Sandbox CLI exists, else NeedsRestart. */
47
+ /** Roots first (instant), then `wsb share` where Windows has it, then HCS (opt-in), else NeedsRestart. */
29
48
  export declare function projectMapper(o: MapperOptions): ProjectMapper;
49
+ /**
50
+ * The doctor's "Add folders while the sandbox runs": which way a folder outside every root would get in (wsb share, HCS
51
+ * or a restart) and why. `liveUnavailable`: why the running sandbox cannot take live folders (null: it can).
52
+ */
53
+ export declare function addFoldersRow(o: {
54
+ wsbShare: boolean;
55
+ liveFolders: 'off' | 'hcs';
56
+ wsbHasShare: boolean;
57
+ liveUnavailable: string | null;
58
+ }): string;
@@ -1,8 +1,11 @@
1
+ import fs from 'node:fs';
1
2
  import path from 'node:path';
2
3
  import { assertSafeWorkspace } from '../../core/safe-paths.js';
3
4
  import { dirExists, fail } from '../../core/util/index.js';
4
- import { guestDirFor, hubName, ownGuestFolder, projectForHostDir } from '../../shared/paths/index.js';
5
+ import { guestDirFor, hubName, liveGuestFolder, ownGuestFolder, projectForHostDir, } from '../../shared/paths/index.js';
5
6
  import { detectWsb, runningSandboxId, shareFolder } from '../sandbox/index.js';
7
+ import { runningOwner } from '../sharing/index.js';
8
+ import { LIVE_FOLDERS_WARNING, realLiveFolders } from '../engines/index.js';
6
9
  import { ProjectRegistry } from './registry.js';
7
10
  /**
8
11
  * Why a folder cannot join the running sandbox without a restart (the caller offers one). With `missing`: it could
@@ -19,7 +22,7 @@ export class NeedsRestart extends Error {
19
22
  this.missing = missing;
20
23
  }
21
24
  }
22
- /** Roots first (instant), then `wsb share` where the Windows Sandbox CLI exists, else NeedsRestart. */
25
+ /** Roots first (instant), then `wsb share` where Windows has it, then HCS (opt-in), else NeedsRestart. */
23
26
  export function projectMapper(o) {
24
27
  const store = new ProjectRegistry(o.storageRoot);
25
28
  const registry = () => store.read() ??
@@ -41,9 +44,40 @@ export function projectMapper(o) {
41
44
  store.save({ ...latest, projects: [...latest.projects, entry] });
42
45
  return entry;
43
46
  };
47
+ const live = () => o.live
48
+ ? o.live()
49
+ : realLiveFolders(o.storageRoot, o.dataPath ?? fail('Live folders need the data folder of the running sandbox.'));
50
+ const taken = (reg) => [...reg.projects.map((p) => p.guest), ...reg.roots.map((r) => r.guest)];
51
+ /** Through HCS (projects.liveFolders "hcs", alpha): any refusal or failure is a NeedsRestart with the reason. */
52
+ const addLive = async (reg, host, name, readOnly, note) => {
53
+ if (!(o.isMine ?? (() => runningOwner().kind === 'mine'))())
54
+ throw new NeedsRestart(host, 'the running sandbox was not started by you, so no folder is added to it through HCS');
55
+ const port = live();
56
+ const why = port.unavailable(reg.launchId);
57
+ if (why)
58
+ throw new NeedsRestart(host, 'live folders through HCS cannot be used: ' + why);
59
+ // The real path (a subst drive is resolved here: the elevated helper does not see this session's drives).
60
+ let real;
61
+ try {
62
+ real = fs.realpathSync.native(host);
63
+ }
64
+ catch {
65
+ throw new NeedsRestart(host, 'the folder cannot be read');
66
+ }
67
+ assertSafeWorkspace(real, o.packageRoot, o.storageRoot);
68
+ const guest = liveGuestFolder(host, taken(reg));
69
+ note?.(LIVE_FOLDERS_WARNING);
70
+ try {
71
+ await port.add({ launchId: reg.launchId, host: real, guest, folder: path.win32.basename(guest), readOnly });
72
+ }
73
+ catch (e) {
74
+ throw new NeedsRestart(host, 'adding it through HCS failed (alpha): ' + (e instanceof Error ? e.message : String(e)));
75
+ }
76
+ return add(reg, host, guest, 'hcs', name);
77
+ };
44
78
  return {
45
79
  list: () => store.read()?.projects ?? [],
46
- register: (hostDir) => {
80
+ register: async (hostDir, opts = {}) => {
47
81
  const host = path.resolve(hostDir);
48
82
  const reg = registry();
49
83
  const known = reg.projects.find((p) => p.host.toLowerCase() === host.toLowerCase());
@@ -57,32 +91,66 @@ export function projectMapper(o) {
57
91
  const root = projectForHostDir(reg.roots, host);
58
92
  if (root)
59
93
  return add(reg, host, guestDirFor(root, host), 'root', name);
94
+ const hcs = o.liveFolders === 'hcs';
95
+ const wsb = o.wsbShare || hcs ? (o.detect ?? detectWsb)() : null;
96
+ // The official way wins wherever it exists: HCS only where wsb share is missing.
97
+ if (o.wsbShare && wsb?.has('share')) {
98
+ const sandbox = runningSandboxId(wsb);
99
+ if (!sandbox.id)
100
+ throw new NeedsRestart(host, 'wsb share could not find the running sandbox: ' + sandbox.error);
101
+ // Where it is as a launch folder too (C:\Workspace\<name>), unless another project already uses that place.
102
+ const guest = ownGuestFolder(host, false, taken(reg));
103
+ const shared = shareFolder(wsb, sandbox.id, host, guest);
104
+ if (!shared.ok)
105
+ throw new NeedsRestart(host, 'wsb share failed (untested on this Windows): ' + shared.error);
106
+ return add(reg, host, guest, 'wsb', name);
107
+ }
108
+ if (hcs && !wsb?.has('share'))
109
+ return addLive(reg, host, name, opts.readOnly ?? true, opts.onNote);
60
110
  if (!o.wsbShare)
61
111
  throw new NeedsRestart(host, 'it is not inside a project root (projects.roots), and adding folders with wsb share is off (projects.wsbShare)');
62
- const wsb = (o.detect ?? detectWsb)();
63
- if (!wsb?.has('share'))
64
- throw new NeedsRestart(host, 'it is not inside a project root (projects.roots), and this Windows has no Windows Sandbox command line (wsb share needs Windows 11 24H2+ with the updated Windows Sandbox)');
65
- const sandbox = runningSandboxId(wsb);
66
- if (!sandbox.id)
67
- throw new NeedsRestart(host, 'wsb share could not find the running sandbox: ' + sandbox.error);
68
- // Where it is as a launch folder too (C:\Workspace\<name>), unless another project already uses that place.
69
- const guest = ownGuestFolder(host, false, [
70
- ...reg.projects.map((p) => p.guest),
71
- ...reg.roots.map((r) => r.guest),
72
- ]);
73
- const shared = shareFolder(wsb, sandbox.id, host, guest);
74
- if (!shared.ok)
75
- throw new NeedsRestart(host, 'wsb share failed (untested on this Windows): ' + shared.error);
76
- return add(reg, host, guest, 'wsb', name);
112
+ throw new NeedsRestart(host, 'it is not inside a project root (projects.roots), and this Windows has no Windows Sandbox command line (wsb share needs Windows 11 24H2+ with the updated Windows Sandbox; projects.liveFolders "hcs" is an untested alpha for older Windows)');
77
113
  },
78
- unregister: (name) => {
114
+ unregister: async (name) => {
79
115
  const reg = registry();
80
116
  const entry = reg.projects.find((p) => p.name.toLowerCase() === name.toLowerCase());
81
117
  if (!entry)
82
118
  fail('No project "' + name + '" in the running sandbox.', 'sandbox projects lists them.');
83
119
  if (entry.primary)
84
120
  fail(entry.host + ' is the folder the sandbox started from; it stays until the sandbox stops.');
85
- store.save({ ...reg, projects: reg.projects.filter((p) => p !== entry) });
121
+ // A live folder: hidden in the sandbox first, then its share removed (one permission prompt); kept on failure.
122
+ if (entry.mechanism === 'hcs') {
123
+ const port = live();
124
+ let real = entry.host;
125
+ try {
126
+ real = fs.realpathSync.native(entry.host);
127
+ }
128
+ catch {
129
+ // Gone from this PC: its record still names the real path it had.
130
+ }
131
+ const share = port.shareOf(real) ?? port.shareOf(entry.host);
132
+ if (share)
133
+ await port.remove(reg.launchId, share.host);
134
+ }
135
+ const latest = store.read() ?? reg;
136
+ store.save({ ...latest, projects: latest.projects.filter((p) => p.name !== entry.name) });
86
137
  },
87
138
  };
88
139
  }
140
+ /**
141
+ * The doctor's "Add folders while the sandbox runs": which way a folder outside every root would get in (wsb share, HCS
142
+ * or a restart) and why. `liveUnavailable`: why the running sandbox cannot take live folders (null: it can).
143
+ */
144
+ export function addFoldersRow(o) {
145
+ if (o.wsbHasShare && o.wsbShare)
146
+ return 'wsb share (untested; a failure offers a restart instead)';
147
+ if (o.liveFolders === 'hcs') {
148
+ if (o.wsbHasShare)
149
+ return 'restart: wsb share is on this Windows but switched off (projects.wsbShare); HCS is not used where wsb share exists';
150
+ return ('HCS (alpha, untested on a real PC): needs an administrator prompt each time' +
151
+ (o.liveUnavailable ? '; not for the running sandbox: ' + o.liveUnavailable : ''));
152
+ }
153
+ return o.wsbHasShare
154
+ ? 'restart (wsb share is switched off: projects.wsbShare)'
155
+ : 'restart (no wsb share on this Windows; projects.liveFolders "hcs" is an untested alpha that adds folders without one)';
156
+ }
@@ -10,6 +10,7 @@ declare const RegistryEntrySchema: z.ZodObject<{
10
10
  guest: z.ZodString;
11
11
  primary: z.ZodBoolean;
12
12
  mechanism: z.ZodCatch<z.ZodEnum<{
13
+ hcs: "hcs";
13
14
  root: "root";
14
15
  launch: "launch";
15
16
  boot: "boot";
@@ -30,6 +31,7 @@ declare const RegistrySchema: z.ZodObject<{
30
31
  guest: z.ZodString;
31
32
  primary: z.ZodBoolean;
32
33
  mechanism: z.ZodCatch<z.ZodEnum<{
34
+ hcs: "hcs";
33
35
  root: "root";
34
36
  launch: "launch";
35
37
  boot: "boot";
@@ -22,7 +22,7 @@ const RegistryEntrySchema = z.object({
22
22
  host: AbsolutePathSchema,
23
23
  guest: z.string().max(1024),
24
24
  primary: z.boolean(),
25
- mechanism: z.enum(['launch', 'root', 'boot', 'wsb']).catch('root'),
25
+ mechanism: z.enum(['launch', 'root', 'boot', 'wsb', 'hcs']).catch('root'),
26
26
  registeredAt: z.string().max(64),
27
27
  });
28
28
  const RegistrySchema = z.object({
@@ -13,7 +13,8 @@ export function writeSecrets(dataPath, id, secrets) {
13
13
  !secrets.claudeCredentials &&
14
14
  !secrets.vpn &&
15
15
  !secrets.wireguardConfig &&
16
- !secrets.githubToken;
16
+ !secrets.githubToken &&
17
+ !secrets.liveFoldersKey;
17
18
  if (empty)
18
19
  return null;
19
20
  const run = new RunDir(dataPath);
@@ -26645,6 +26645,9 @@ var ProjectsSchema = external_exports.strictObject({
26645
26645
  ),
26646
26646
  wsbShare: external_exports.boolean().describe(
26647
26647
  "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."
26648
+ ),
26649
+ liveFolders: external_exports.enum(["off", "hcs"]).describe(
26650
+ '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").'
26648
26651
  )
26649
26652
  });
26650
26653
 
@@ -27180,6 +27183,9 @@ var RunDir = class {
27180
27183
  treesDir;
27181
27184
  /** One-shot secrets the host hands over (see secrets.ts). Next to run, not in it: shared-use users can change run. */
27182
27185
  secretsDir;
27186
+ /** Live folders (live.ts): signed host requests, and the daemon's replies. */
27187
+ liveDir;
27188
+ liveRepliesDir;
27183
27189
  constructor(dataDir2) {
27184
27190
  this.root = path5.join(dataDir2, "run");
27185
27191
  this.sandboxFile = path5.join(this.root, "sandbox.json");
@@ -27193,6 +27199,14 @@ var RunDir = class {
27193
27199
  this.auditDir = path5.join(this.root, "audit");
27194
27200
  this.treesDir = path5.join(this.root, "trees");
27195
27201
  this.secretsDir = path5.join(dataDir2, "secrets");
27202
+ this.liveDir = path5.join(this.root, "live");
27203
+ this.liveRepliesDir = path5.join(this.root, "live-replies");
27204
+ }
27205
+ liveFile(id) {
27206
+ return path5.join(this.liveDir, safeId(id) + ".json");
27207
+ }
27208
+ liveReplyFile(id) {
27209
+ return path5.join(this.liveRepliesDir, safeId(id) + ".json");
27196
27210
  }
27197
27211
  requestFile(id) {
27198
27212
  return path5.join(this.requestsDir, safeId(id) + ".json");