windows-development-sandbox 1.13.2 → 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 (153) 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/app/features/storage/storage.run.js +1 -1
  9. package/dist/bin/agent-sandbox.js +2 -2
  10. package/dist/bin/{chunk-VENWSGX2.js → chunk-D7CM63BS.js} +2303 -1462
  11. package/dist/bin/{chunk-K4SU3MHJ.js → chunk-HTQT5SBY.js} +1 -1
  12. package/dist/bin/{chunk-IL7BXJF2.js → chunk-RQ4BWBYX.js} +1 -1
  13. package/dist/bin/claude-host.js +2 -2
  14. package/dist/bin/claude-sbx.js +2 -2
  15. package/dist/bin/codex-sbx.js +2 -2
  16. package/dist/bin/gemini-sbx.js +2 -2
  17. package/dist/bin/sandbox.js +2 -2
  18. package/dist/bin/sbx-hide.js +2 -2
  19. package/dist/bin/sbx-show.js +2 -2
  20. package/dist/cli/commands/launch/launch.js +1 -1
  21. package/dist/cli/commands/projects/here.js +2 -0
  22. package/dist/cli/commands/repair/checks/contained.js +1 -1
  23. package/dist/cli/services/health-service.js +15 -3
  24. package/dist/cli/services/projects-service.d.ts +8 -3
  25. package/dist/cli/services/projects-service.js +21 -4
  26. package/dist/core/config/defaults.js +1 -0
  27. package/dist/core/schema/config.d.ts +4 -0
  28. package/dist/core/schema/projects.d.ts +4 -0
  29. package/dist/core/schema/projects.js +3 -0
  30. package/dist/guest/boot/boot.js +8 -2
  31. package/dist/guest/daemon/daemon.js +3 -0
  32. package/dist/guest/daemon/live-folders.d.ts +48 -0
  33. package/dist/guest/daemon/live-folders.js +245 -0
  34. package/dist/guest/daemon/live.d.ts +4 -0
  35. package/dist/guest/daemon/live.js +57 -0
  36. package/dist/guest/daemon/sessions.d.ts +7 -0
  37. package/dist/guest/daemon/sessions.js +23 -2
  38. package/dist/guest/entries/daemon.js +5 -2
  39. package/dist/guest/runtime/csc.d.ts +1 -0
  40. package/dist/guest/runtime/csc.js +1 -0
  41. package/dist/guest/runtime/secrets.d.ts +5 -2
  42. package/dist/guest/runtime/secrets.js +12 -5
  43. package/dist/guest/sessions/pty-host.d.ts +10 -0
  44. package/dist/guest/sessions/pty-host.js +48 -3
  45. package/dist/host/channel/channel.js +1 -1
  46. package/dist/host/engines/hcs/live-folders.d.ts +117 -0
  47. package/dist/host/engines/hcs/live-folders.js +347 -0
  48. package/dist/host/engines/index.d.ts +1 -0
  49. package/dist/host/engines/index.js +1 -0
  50. package/dist/host/install/install-record.d.ts +1 -0
  51. package/dist/host/install/install-record.js +2 -0
  52. package/dist/host/launch/generate.js +8 -1
  53. package/dist/host/projects/join.d.ts +4 -0
  54. package/dist/host/projects/join.js +11 -3
  55. package/dist/host/projects/mapper.d.ts +34 -5
  56. package/dist/host/projects/mapper.js +88 -20
  57. package/dist/host/projects/registry.d.ts +2 -0
  58. package/dist/host/projects/registry.js +1 -1
  59. package/dist/host/sandbox/contained-deps.d.ts +2 -1
  60. package/dist/host/sandbox/contained-deps.js +6 -4
  61. package/dist/host/secrets/launch-secrets.js +2 -1
  62. package/dist/host/sharing/lifecycle.js +9 -1
  63. package/dist/host/storage/contained/contained-storage.js +13 -0
  64. package/dist/host/storage/contained/disk-port.d.ts +5 -0
  65. package/dist/host/storage/contained/helper-port.d.ts +72 -0
  66. package/dist/host/storage/contained/helper-port.js +162 -0
  67. package/dist/host/storage/contained/index.d.ts +1 -0
  68. package/dist/host/storage/contained/index.js +1 -0
  69. package/dist/host/web/bridge.d.ts +5 -0
  70. package/dist/host/web/bridge.js +11 -1
  71. package/dist/host/web/server.d.ts +2 -0
  72. package/dist/host/web/server.js +11 -1
  73. package/dist/runtime/agent.js +23 -0
  74. package/dist/runtime/daemon.js +523 -135
  75. package/dist/runtime/db.js +23 -0
  76. package/dist/runtime/main.js +221 -15
  77. package/dist/runtime/netproxy.js +23 -0
  78. package/dist/runtime/view.js +209 -12
  79. package/dist/selftest/channel.test.js +72 -1
  80. package/dist/selftest/contained.test.js +1 -37
  81. package/dist/selftest/hcs.test.js +5 -2
  82. package/dist/selftest/index.js +4 -0
  83. package/dist/selftest/install.test.js +11 -0
  84. package/dist/selftest/layout.test.js +1 -0
  85. package/dist/selftest/live-daemon.test.js +51 -1
  86. package/dist/selftest/live-folders.test.d.ts +2 -0
  87. package/dist/selftest/live-folders.test.js +588 -0
  88. package/dist/selftest/live-terminal.test.js +112 -4
  89. package/dist/selftest/projects.test.js +7 -7
  90. package/dist/selftest/storage-helper.test.d.ts +2 -0
  91. package/dist/selftest/storage-helper.test.js +493 -0
  92. package/dist/selftest/support/engine-golden.js +1 -0
  93. package/dist/selftest/support/fixtures.d.ts +1 -0
  94. package/dist/selftest/support/live.js +2 -0
  95. package/dist/selftest/support/vhdx.d.ts +14 -0
  96. package/dist/selftest/support/vhdx.js +72 -0
  97. package/dist/selftest/web.test.js +14 -2
  98. package/dist/shared/channel/index.d.ts +1 -0
  99. package/dist/shared/channel/index.js +1 -0
  100. package/dist/shared/channel/live.d.ts +53 -0
  101. package/dist/shared/channel/live.js +92 -0
  102. package/dist/shared/channel/run-dir.d.ts +11 -0
  103. package/dist/shared/channel/run-dir.js +20 -0
  104. package/dist/shared/channel/secrets.d.ts +1 -0
  105. package/dist/shared/channel/secrets.js +5 -0
  106. package/dist/shared/paths/projects.d.ts +6 -1
  107. package/dist/shared/paths/projects.js +21 -1
  108. package/dist/shared/pty/index.d.ts +3 -1
  109. package/dist/shared/pty/index.js +3 -1
  110. package/dist/shared/pty/key-gate.d.ts +24 -0
  111. package/dist/shared/pty/key-gate.js +45 -0
  112. package/dist/shared/pty/mouse-modes.d.ts +27 -0
  113. package/dist/shared/pty/mouse-modes.js +155 -0
  114. package/dist/shared/pty/terminal.d.ts +0 -5
  115. package/dist/shared/pty/terminal.js +51 -20
  116. package/dist/sync/watcher.js +10 -1
  117. package/dist/web/index.html +1 -1
  118. package/dist/web/{main-NBQ3LF4E.js → main-7DJZ2M27.js} +34 -34
  119. package/native/csharp/HcsHost/AssemblyInfo.cs +4 -4
  120. package/native/csharp/HcsHost/FolderAccess.cs +361 -0
  121. package/native/csharp/HcsHost/Hcs.cs +31 -3
  122. package/native/csharp/HcsHost/Program.cs +54 -6
  123. package/native/csharp/HcsHost/Share.cs +64 -11
  124. package/native/csharp/HcsHost/WsbGuard.cs +130 -0
  125. package/native/csharp/HcsHost/WsbShare.cs +295 -0
  126. package/native/csharp/HostTools/AssemblyInfo.cs +3 -3
  127. package/native/csharp/StorageHelper/AssemblyInfo.cs +15 -0
  128. package/native/csharp/StorageHelper/Client.cs +297 -0
  129. package/native/csharp/StorageHelper/Log.cs +177 -0
  130. package/native/csharp/StorageHelper/MountRecords.cs +160 -0
  131. package/native/csharp/StorageHelper/Operations.cs +596 -0
  132. package/native/csharp/StorageHelper/Pins.cs +270 -0
  133. package/native/csharp/StorageHelper/Privileges.cs +78 -0
  134. package/native/csharp/StorageHelper/Program.cs +349 -0
  135. package/native/csharp/StorageHelper/Protocol.cs +107 -0
  136. package/native/csharp/StorageHelper/RateLimiter.cs +97 -0
  137. package/native/csharp/StorageHelper/Rules.cs +156 -0
  138. package/native/csharp/StorageHelper/Server.cs +656 -0
  139. package/native/csharp/StorageHelper/Settings.cs +297 -0
  140. package/native/csharp/StorageHelper/StorageService.cs +53 -0
  141. package/native/csharp/StorageHelper/UserSettings.cs +206 -0
  142. package/native/csharp/StorageHelper/Vhdx.cs +230 -0
  143. package/native/csharp/StorageHelper/VirtualDisk.cs +241 -0
  144. package/native/csharp/StorageHelper/Volumes.cs +379 -0
  145. package/native/csharp/StorageHelper/Watchdog.cs +128 -0
  146. package/native/csharp/StorageHelper/Win32.cs +250 -0
  147. package/native/scripts/contained-common.ps1 +4 -0
  148. package/native/scripts/hcs-live-share.ps1 +191 -0
  149. package/native/scripts/set-storage-helper.ps1 +203 -0
  150. package/package.json +1 -1
  151. package/sandbox/csharp/BindMount.cs +120 -0
  152. package/sandbox/csharp/PtyHost.cs +236 -1
  153. package/schemas/config.schema.json +8 -0
@@ -0,0 +1,57 @@
1
+ // The daemon's live-folder handler with the real bind filter (BindMount.exe, compiled from sandbox\csharp each boot and
2
+ // run by path) and its state on the sandbox's own disk, in a folder only administrators and SYSTEM may use.
3
+ import fs from 'node:fs';
4
+ import path from 'node:path';
5
+ import { spawnSync } from 'node:child_process';
6
+ import { SYSTEM } from '../../shared/system.js';
7
+ import { ensureCompiled } from '../runtime/csc.js';
8
+ import { isWin, localDir, run, testMode } from '../runtime/paths.js';
9
+ import { session } from '../runtime/session.js';
10
+ import { warn } from '../runtime/log.js';
11
+ import { LiveFolderHandler } from './live-folders.js';
12
+ const BIND_TIMEOUT_MS = 30000;
13
+ /** A test (it names its own local folder) never touches C:\ProgramData of the machine it runs on. */
14
+ const testing = Boolean(process.env.AGENT_DAEMON_STATE_DIR || process.env.AGENT_LOCAL_DIR);
15
+ const stateDir = process.env.AGENT_DAEMON_STATE_DIR ??
16
+ (process.env.AGENT_LOCAL_DIR
17
+ ? path.join(localDir, 'daemon-state')
18
+ : path.join(process.env.ProgramData ?? 'C:\\ProgramData', 'AgentSandboxDaemon'));
19
+ function bindMount(args) {
20
+ const r = spawnSync(ensureCompiled('bindmount'), args, {
21
+ windowsHide: true,
22
+ timeout: BIND_TIMEOUT_MS,
23
+ encoding: 'utf8',
24
+ });
25
+ return r.status ?? 1;
26
+ }
27
+ const realBind = {
28
+ map: (folder, share, readOnly) => bindMount(['map', folder, share, ...(readOnly ? [] : ['rw'])]),
29
+ unmap: (folder) => bindMount(['unmap', folder]),
30
+ mapped: (folder) => bindMount(['mapped', folder]),
31
+ };
32
+ /** The state folder, made administrator-only (inside a real sandbox; a test names its own temp folder). */
33
+ function protectStateDir() {
34
+ fs.mkdirSync(stateDir, { recursive: true });
35
+ if (!isWin || testMode || testing)
36
+ return;
37
+ spawnSync(SYSTEM.icacls(), [stateDir, '/inheritance:r', '/grant:r', '*S-1-5-32-544:(OI)(CI)F', '*S-1-5-18:(OI)(CI)F'], {
38
+ windowsHide: true,
39
+ timeout: BIND_TIMEOUT_MS,
40
+ });
41
+ }
42
+ export const liveFolders = new LiveFolderHandler({
43
+ run,
44
+ stateFile: path.join(stateDir, 'live-folders.json'),
45
+ launchId: session.launchId,
46
+ bind: realBind,
47
+ log: warn,
48
+ });
49
+ /** At daemon start: the folder protected, the boot's key taken in, the recorded mappings made again. */
50
+ export function startLiveFolders(key) {
51
+ protectStateDir();
52
+ if (key)
53
+ liveFolders.setKey(key);
54
+ const again = liveFolders.reapply();
55
+ if (again)
56
+ warn('Live folders: ' + again + ' shown again after the daemon restarted.');
57
+ }
@@ -22,6 +22,13 @@ export declare class SessionManager {
22
22
  /** The sessions' ptyhosts, running or ended within ROOTS_KEPT_MS (audit events arrive after they happen). */
23
23
  roots(): SessionRoot[];
24
24
  private rootStarted;
25
+ /** What a session's ptyhost left running when it was itself ended (Task Manager, a crash) is ended now. */
26
+ private endLeftovers;
27
+ /**
28
+ * At the daemon's start: trees recorded by session hosts that are gone (a daemon that ended with them) are ended
29
+ * too. One whose session host still runs is left alone (ptyhost --end-tree checks).
30
+ */
31
+ sweepLeftovers(): void;
25
32
  private rootEnded;
26
33
  /**
27
34
  * `session` requests: an agent (or a plain shell) in its own ptyhost, in the launch folder's project or a registered
@@ -16,7 +16,7 @@ import { commandLine, shellLaunch } from '../sessions/cmdline.js';
16
16
  import { pwshDir } from '../toolchains/tools/pwsh.js';
17
17
  import { freePort } from '../runtime/ports.js';
18
18
  import { primaryProject } from '../boot/project-status.js';
19
- import { ensurePtyHost, ptyHostLogTail, startPtyHost, waitForPtyHost } from '../sessions/pty-host.js';
19
+ import { endLeftoverTree, ensurePtyHost, ptyHostLogTail, recordedTrees, startPtyHost, waitForPtyHost, } from '../sessions/pty-host.js';
20
20
  import { agents } from '../agents/agent-installer.js';
21
21
  import { saveClaudeMem } from '../setup/claude-mem.js';
22
22
  import { saveLocalState } from '../agents/local-state.js';
@@ -224,7 +224,28 @@ export class SessionManager {
224
224
  rootStarted(id, child, from) {
225
225
  if (child.pid)
226
226
  this.ptyHosts.set(id, { session: id, pid: child.pid, from });
227
- child.once('exit', () => this.rootEnded(id));
227
+ child.once('exit', () => {
228
+ this.rootEnded(id);
229
+ this.endLeftovers(id);
230
+ });
231
+ }
232
+ /** What a session's ptyhost left running when it was itself ended (Task Manager, a crash) is ended now. */
233
+ endLeftovers(id) {
234
+ void endLeftoverTree(id)
235
+ .then((done) => {
236
+ if (done)
237
+ log('Session ' + id + ': its session host was ended from outside; ' + done + ' it left running');
238
+ })
239
+ .catch((e) => warn('Ending what session ' + id + ' left running failed: ' + errMsg(e)));
240
+ }
241
+ /**
242
+ * At the daemon's start: trees recorded by session hosts that are gone (a daemon that ended with them) are ended
243
+ * too. One whose session host still runs is left alone (ptyhost --end-tree checks).
244
+ */
245
+ sweepLeftovers() {
246
+ for (const id of recordedTrees())
247
+ if (!this.live.has(id))
248
+ this.endLeftovers(id);
228
249
  }
229
250
  rootEnded(id) {
230
251
  const r = this.ptyHosts.get(id);
@@ -1,5 +1,5 @@
1
1
  // Session daemon (runs detached inside the sandbox for its whole life). It is the only thing that acts on host
2
- // requests, and it only knows four request kinds: start a session, run a command, restart sessions, ping.
2
+ // requests: sessions, commands, restarts, pings (run\requests) and the signed live-folder requests (run\live).
3
3
  // It must never die quietly: every iteration is guarded, and anything unexpected is written to its log.
4
4
  // The handlers live in daemon/ (session, exec); this file is the polling loop.
5
5
  import { startRelays } from '../daemon/relays.js';
@@ -9,14 +9,17 @@ import { log, warn } from '../runtime/log.js';
9
9
  import { adoptBootSecrets } from '../runtime/secrets.js';
10
10
  import { errMsg } from '../../core/util/errors.js';
11
11
  import { Daemon } from '../daemon/daemon.js';
12
+ import { startLiveFolders } from '../daemon/live.js';
12
13
  import { sessions } from '../daemon/sessions.js';
13
14
  import { governor } from '../governor/index.js';
14
15
  process.on('uncaughtException', (e) => warn('daemon: unexpected error (continuing): ' + (e.stack ?? errMsg(e))));
15
16
  process.on('unhandledRejection', (e) => warn('daemon: unexpected error (continuing): ' + errMsg(e)));
16
- adoptBootSecrets();
17
+ const liveKey = adoptBootSecrets();
17
18
  log('Session daemon started (pid ' + process.pid + ').');
19
+ safely('live folders', () => startLiveFolders(liveKey));
18
20
  safely('port forwards', startRelays);
19
21
  safely('audit', () => startProcessCollector(() => sessions.roots()));
22
+ safely('leftover processes', () => sessions.sweepLeftovers());
20
23
  safely('memory governor', () => governor.start());
21
24
  log('Memory governor: ' + governor.describe());
22
25
  new Daemon().start();
@@ -8,6 +8,7 @@ declare const SOURCES: {
8
8
  readonly linkgate: "LinkGate.cs";
9
9
  readonly pathsync: "PathSync.cs";
10
10
  readonly memgov: "MemGov.cs";
11
+ readonly bindmount: "BindMount.cs";
11
12
  };
12
13
  export type Helper = keyof typeof SOURCES;
13
14
  /** %TEMP%\agent-sandbox\<name>-<hash>.exe, compiled from this launch's source the first time it is needed. */
@@ -21,6 +21,7 @@ const SOURCES = {
21
21
  linkgate: 'LinkGate.cs',
22
22
  pathsync: 'PathSync.cs',
23
23
  memgov: 'MemGov.cs',
24
+ bindmount: 'BindMount.cs',
24
25
  };
25
26
  function sourceOf(name) {
26
27
  return path.join(launchDir, 'sandbox', 'csharp', SOURCES[name]);
@@ -6,7 +6,10 @@ export declare const BOOT_SECRETS_VARIABLE = "SBX_BOOT_SECRETS";
6
6
  export declare function takeSecrets(id: string): SecretsFile;
7
7
  /** The agent the sandbox booted with gets its environment (in memory only). */
8
8
  export declare function applyBootEnvironment(environment: Record<string, string> | undefined): void;
9
- /** The daemon takes what main.js passed it, and removes it from its own environment before any session starts. */
10
- export declare function adoptBootSecrets(): void;
9
+ /**
10
+ * The daemon takes what main.js passed it, and removes it from its own environment before any session starts. Returns
11
+ * the live-folder key, if the launch made one (the daemon keeps it in its own state).
12
+ */
13
+ export declare function adoptBootSecrets(): string | undefined;
11
14
  /** A request with the agent environment values that travelled in its own secrets file put back (in memory only). */
12
15
  export declare function withRequestSecrets(req: GuestRequest): GuestRequest;
@@ -40,20 +40,27 @@ export function applyBootEnvironment(environment) {
40
40
  if (session.harness && environment)
41
41
  session.harness.environment = { ...environment };
42
42
  }
43
- /** The daemon takes what main.js passed it, and removes it from its own environment before any session starts. */
43
+ /**
44
+ * The daemon takes what main.js passed it, and removes it from its own environment before any session starts. Returns
45
+ * the live-folder key, if the launch made one (the daemon keeps it in its own state).
46
+ */
44
47
  export function adoptBootSecrets() {
45
48
  const text = process.env[BOOT_SECRETS_VARIABLE];
46
49
  delete process.env[BOOT_SECRETS_VARIABLE];
47
50
  if (!text)
48
- return;
51
+ return undefined;
49
52
  try {
50
- const parsed = SecretsFileSchema.pick({ harnessEnvironment: true }).safeParse(JSON.parse(text));
51
- if (parsed.success)
53
+ const parsed = SecretsFileSchema.pick({ harnessEnvironment: true, liveFoldersKey: true }).safeParse(JSON.parse(text));
54
+ if (parsed.success) {
52
55
  applyBootEnvironment(parsed.data.harnessEnvironment);
56
+ return parsed.data.liveFoldersKey;
57
+ }
53
58
  }
54
59
  catch {
55
- warn('The boot secrets given to the daemon were not valid.');
60
+ // Reported below.
56
61
  }
62
+ warn('The boot secrets given to the daemon were not valid.');
63
+ return undefined;
57
64
  }
58
65
  /** A request with the agent environment values that travelled in its own secrets file put back (in memory only). */
59
66
  export function withRequestSecrets(req) {
@@ -20,3 +20,13 @@ export interface PtyHostOptions {
20
20
  }
21
21
  /** Starts ptyhost detached, so closing a terminal tab never kills the agent; only the agent exiting ends it. */
22
22
  export declare function startPtyHost(o: PtyHostOptions): ChildProcess;
23
+ /**
24
+ * Ends what a session's ptyhost left running when it was itself ended (Task Manager, a crash): its program and what
25
+ * that started, from the tree it recorded in run/trees (PTYHOST_TREE_FILE). ptyhost deletes the record when it ends the
26
+ * tree itself, so a record left behind is the sign. `ptyhost --end-tree` checks every process by id and start time (a
27
+ * reused id is never ended), spares the shared ones (databases, sccache ...) and does nothing while the ptyhost that
28
+ * wrote it still runs. Resolves with what it did, or null when there was nothing to do.
29
+ */
30
+ export declare function endLeftoverTree(id: string, exe?: string): Promise<string | null>;
31
+ /** Session ids with a process tree recorded (running sessions, and any whose ptyhost was ended). */
32
+ export declare function recordedTrees(): string[];
@@ -1,10 +1,10 @@
1
1
  // Starts one ptyhost (the ConPTY bridge, sandbox/csharp/PtyHost.cs) per agent session and waits for it to listen.
2
2
  import fs from 'node:fs';
3
3
  import path from 'node:path';
4
- import { spawn } from 'node:child_process';
4
+ import { execFile, spawn } from 'node:child_process';
5
5
  import { ensureCompiled } from '../runtime/csc.js';
6
6
  import { portOpen } from '../runtime/ports.js';
7
- import { logsDir, toolsDir } from '../runtime/paths.js';
7
+ import { logsDir, run, toolsDir } from '../runtime/paths.js';
8
8
  import { session } from '../runtime/session.js';
9
9
  export function ensurePtyHost() {
10
10
  return ensureCompiled('ptyhost');
@@ -51,10 +51,55 @@ const conptyDll = () => {
51
51
  };
52
52
  /** Starts ptyhost detached, so closing a terminal tab never kills the agent; only the agent exiting ends it. */
53
53
  export function startPtyHost(o) {
54
- const env = { ...o.env, PTYHOST_BIND: o.remote ? 'any' : 'loopback' };
54
+ fs.mkdirSync(run.treesDir, { recursive: true });
55
+ const env = {
56
+ ...o.env,
57
+ PTYHOST_BIND: o.remote ? 'any' : 'loopback',
58
+ PTYHOST_TREE_FILE: run.treeFile(o.id),
59
+ };
55
60
  delete env.PTYHOST_CONPTY;
56
61
  const dll = conptyDll();
57
62
  if (dll)
58
63
  env.PTYHOST_CONPTY = dll;
59
64
  return spawn(o.exe, [String(o.port), o.token, String(o.cols), String(o.rows), ptyHostLog(o.id), o.command, o.restartCommand], { cwd: o.cwd, env, detached: true, stdio: 'ignore', windowsHide: true });
60
65
  }
66
+ /** How long `ptyhost --end-tree` may take (a process snapshot and a few TerminateProcess calls). */
67
+ const END_TREE_MS = 30000;
68
+ /**
69
+ * Ends what a session's ptyhost left running when it was itself ended (Task Manager, a crash): its program and what
70
+ * that started, from the tree it recorded in run/trees (PTYHOST_TREE_FILE). ptyhost deletes the record when it ends the
71
+ * tree itself, so a record left behind is the sign. `ptyhost --end-tree` checks every process by id and start time (a
72
+ * reused id is never ended), spares the shared ones (databases, sccache ...) and does nothing while the ptyhost that
73
+ * wrote it still runs. Resolves with what it did, or null when there was nothing to do.
74
+ */
75
+ export function endLeftoverTree(id, exe = ensurePtyHost()) {
76
+ const file = run.treeFile(id);
77
+ if (!fs.existsSync(file))
78
+ return Promise.resolve(null);
79
+ return new Promise((resolve) => {
80
+ execFile(exe, ['--end-tree', file], { timeout: END_TREE_MS, windowsHide: true }, (error, stdout) => {
81
+ const code = error ? (typeof error.code === 'number' ? error.code : -1) : 0;
82
+ // 3: the ptyhost that wrote it still runs (its session goes on): the record stays.
83
+ if (code !== 3)
84
+ try {
85
+ fs.rmSync(file, { force: true });
86
+ }
87
+ catch {
88
+ /* removed by the next sweep (or the host's reset at the next launch) */
89
+ }
90
+ resolve(code === 0 ? stdout.trim().slice(0, 200) : code === 3 ? null : 'could not end them (code ' + code + ')');
91
+ });
92
+ });
93
+ }
94
+ /** Session ids with a process tree recorded (running sessions, and any whose ptyhost was ended). */
95
+ export function recordedTrees() {
96
+ try {
97
+ return fs
98
+ .readdirSync(run.treesDir)
99
+ .filter((n) => /^[A-Za-z0-9_-]{1,64}\.json$/.test(n))
100
+ .map((n) => n.slice(0, -'.json'.length));
101
+ }
102
+ catch {
103
+ return [];
104
+ }
105
+ }
@@ -231,7 +231,7 @@ export class SandboxChannel {
231
231
  removeLink(p.root);
232
232
  return;
233
233
  }
234
- for (const target of [p.sandboxFile, p.requestsDir, p.responsesDir, p.sessionsDir, p.projectsDir]) {
234
+ for (const target of [p.sandboxFile, p.requestsDir, p.responsesDir, p.sessionsDir, p.projectsDir, p.treesDir]) {
235
235
  safeRemove(target, this.dataPath);
236
236
  }
237
237
  }
@@ -0,0 +1,117 @@
1
+ import { z } from 'zod';
2
+ import { SecretStore } from '../../secrets/index.js';
3
+ /** The warning shown wherever live folders are offered or used. */
4
+ export declare const LIVE_FOLDERS_WARNING = "Live folders through HCS are an alpha, untested on a real PC: if it fails, the sandbox may close, so save your work first. Each add or remove asks for administrator permission.";
5
+ declare const ShareSchema: z.ZodObject<{
6
+ name: z.ZodString;
7
+ host: z.ZodString;
8
+ guest: z.ZodString;
9
+ folder: z.ZodString;
10
+ readOnly: z.ZodBoolean;
11
+ addedAt: z.ZodString;
12
+ }, z.core.$strip>;
13
+ declare const RecordSchema: z.ZodObject<{
14
+ launchId: z.ZodString;
15
+ dataPath: z.ZodString;
16
+ key: z.ZodString;
17
+ systemId: z.ZodNullable<z.ZodString>;
18
+ shares: z.ZodArray<z.ZodObject<{
19
+ name: z.ZodString;
20
+ host: z.ZodString;
21
+ guest: z.ZodString;
22
+ folder: z.ZodString;
23
+ readOnly: z.ZodBoolean;
24
+ addedAt: z.ZodString;
25
+ }, z.core.$strip>>;
26
+ }, z.core.$strip>;
27
+ export type LiveRecord = z.output<typeof RecordSchema>;
28
+ export type LiveShare = z.output<typeof ShareSchema>;
29
+ /** What the elevated script was asked (request.json; HcsHost checks every field again). */
30
+ export interface LiveShareRequest {
31
+ readonly action: 'add' | 'remove';
32
+ readonly id: string;
33
+ readonly name: string;
34
+ readonly path?: string;
35
+ readonly readOnly?: boolean;
36
+ readonly storageRoot: string;
37
+ readonly dataPath: string;
38
+ readonly userSid: string;
39
+ }
40
+ declare const OutcomeSchema: z.ZodObject<{
41
+ ok: z.ZodBoolean;
42
+ message: z.ZodString;
43
+ id: z.ZodOptional<z.ZodString>;
44
+ canary: z.ZodOptional<z.ZodString>;
45
+ }, z.core.$strip>;
46
+ export type LiveOutcome = z.output<typeof OutcomeSchema>;
47
+ /** The elevated half: one run of hcs-live-share.ps1. `onCanary` gets the canary share's name, returns its token. */
48
+ export interface ElevatedLivePort {
49
+ run(request: LiveShareRequest, onCanary: (share: string) => Promise<string | null>): Promise<LiveOutcome>;
50
+ }
51
+ export type LiveRunner = (file: string, args: readonly string[], timeoutMs: number) => Promise<{
52
+ status: number | null;
53
+ }>;
54
+ export declare const spawnLiveRunner: LiveRunner;
55
+ /** run-elevated.ps1 -Script hcs-live-share.ps1 with the request in a private temp folder; relays the canary. */
56
+ export declare class ScriptLivePort implements ElevatedLivePort {
57
+ private readonly runner;
58
+ private readonly tempRoot;
59
+ private readonly pollMs;
60
+ constructor(runner?: LiveRunner, tempRoot?: string, pollMs?: number);
61
+ run(request: LiveShareRequest, onCanary: (share: string) => Promise<string | null>): Promise<LiveOutcome>;
62
+ }
63
+ export interface LiveFolderDeps {
64
+ readonly storageRoot: string;
65
+ readonly dataPath: string;
66
+ readonly secrets: SecretStore;
67
+ readonly elevated: ElevatedLivePort;
68
+ readonly userSid: () => string;
69
+ readonly isAlive?: () => boolean;
70
+ readonly pollMs?: number;
71
+ readonly now?: () => Date;
72
+ /** How long the sandbox's daemon has for the canary and for a map or unmap (tests make them short). */
73
+ readonly canaryMs?: number;
74
+ readonly mapMs?: number;
75
+ }
76
+ /** <storage>\state\live-folders.json. */
77
+ export declare function liveRecordFile(storageRoot: string): string;
78
+ export declare class LiveFolders {
79
+ private readonly d;
80
+ private readonly file;
81
+ private readonly run;
82
+ constructor(d: LiveFolderDeps);
83
+ read(): LiveRecord | null;
84
+ /** At launch (projects.liveFolders "hcs"): a new key for this launch, kept sealed; returned for the secrets file. */
85
+ startLaunch(launchId: string): string;
86
+ /** The sandbox stopped (its shares ended with it), or a launch without live folders: the record goes. */
87
+ clear(): void;
88
+ /** Why live folders cannot be used for this launch now (null: they can). */
89
+ unavailable(launchId: string): string | null;
90
+ private log;
91
+ private key;
92
+ /** Sends one signed request to the sandbox's daemon and waits for its reply. */
93
+ private ask;
94
+ private request;
95
+ /**
96
+ * Shares `host` (its real path) into the running sandbox and shows it at C:\Workspace\`folder`. Throws with the
97
+ * reason when anything refuses or fails; the folder is then not shared.
98
+ */
99
+ add(o: {
100
+ launchId: string;
101
+ host: string;
102
+ guest: string;
103
+ folder: string;
104
+ readOnly: boolean;
105
+ }): Promise<LiveShare>;
106
+ /** The share itself (elevated). True when it is gone; the record is kept when not. */
107
+ private removeShare;
108
+ /** The live folder of `host`, if there is one. */
109
+ shareOf(host: string): LiveShare | null;
110
+ /** Removes a live folder: the guest mapping first, then the share. Throws (and keeps it) when either fails. */
111
+ remove(launchId: string, host: string): Promise<void>;
112
+ }
113
+ /** The real thing: DPAPI, the elevated script (it builds HcsHost.exe itself), this user's SID. */
114
+ export declare function realLiveFolders(storageRoot: string, dataPath: string): LiveFolders;
115
+ /** The sandbox of `dataPath` stopped: its live-folder record (and key) goes; its shares ended with the VM. */
116
+ export declare function clearLiveRecord(storageRoot: string, dataPath: string): boolean;
117
+ export {};