windows-development-sandbox 1.13.0 → 1.14.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 (125) hide show
  1. package/dist/app/features/accounts/accounts.def.d.ts +1 -0
  2. package/dist/app/features/accounts/accounts.def.js +12 -3
  3. package/dist/app/features/accounts/accounts.run.d.ts +1 -0
  4. package/dist/app/features/accounts/accounts.run.js +20 -11
  5. package/dist/app/features/defs.d.ts +1 -0
  6. package/dist/app/features/settings/settings-file.d.ts +7 -0
  7. package/dist/app/features/settings/settings-file.js +19 -2
  8. package/dist/app/features/settings/settings-rows.js +9 -1
  9. package/dist/app/features/storage/storage.run.js +1 -1
  10. package/dist/bin/agent-sandbox.js +7 -1
  11. package/dist/bin/chunk-BQ23GBLC.js +78790 -0
  12. package/dist/bin/chunk-F7IMME5G.js +10432 -0
  13. package/dist/bin/chunk-LQHEUN2A.js +7 -0
  14. package/dist/bin/claude-host.js +10 -4
  15. package/dist/bin/claude-sbx.js +8 -2
  16. package/dist/bin/codex-sbx.js +8 -2
  17. package/dist/bin/gemini-sbx.js +8 -2
  18. package/dist/bin/sandbox.js +7 -2
  19. package/dist/bin/sbx-hide.js +8 -2
  20. package/dist/bin/sbx-show.js +8 -2
  21. package/dist/cli/commands/launch/launch.js +1 -1
  22. package/dist/cli/commands/launch/prepare.js +24 -7
  23. package/dist/cli/commands/launch/running.js +4 -0
  24. package/dist/cli/commands/repair/checks/contained.js +1 -1
  25. package/dist/cli/commands/storage/offer.js +1 -1
  26. package/dist/cli/menu/live.d.ts +29 -0
  27. package/dist/cli/menu/live.js +92 -21
  28. package/dist/cli/menu/menu.d.ts +2 -1
  29. package/dist/cli/menu/menu.js +16 -2
  30. package/dist/cli/menu/pages/list-page.d.ts +5 -0
  31. package/dist/cli/menu/pages/list-page.js +19 -0
  32. package/dist/cli/menu/shell.d.ts +2 -0
  33. package/dist/cli/menu/shell.js +1 -1
  34. package/dist/cli/services/accounts-service.d.ts +7 -0
  35. package/dist/cli/services/accounts-service.js +20 -0
  36. package/dist/cli/tui/app.d.ts +4 -0
  37. package/dist/cli/tui/app.js +5 -0
  38. package/dist/cli/tui/terminal.d.ts +6 -0
  39. package/dist/cli/tui/terminal.js +8 -1
  40. package/dist/cli/validate.js +8 -4
  41. package/dist/core/package-info.js +1 -1
  42. package/dist/guest/daemon/sessions.d.ts +7 -0
  43. package/dist/guest/daemon/sessions.js +23 -2
  44. package/dist/guest/entries/daemon.js +1 -0
  45. package/dist/guest/sessions/pty-host.d.ts +10 -0
  46. package/dist/guest/sessions/pty-host.js +48 -3
  47. package/dist/host/channel/channel.js +1 -1
  48. package/dist/host/install/install-record.d.ts +1 -0
  49. package/dist/host/install/install-record.js +2 -0
  50. package/dist/host/sandbox/contained-deps.d.ts +2 -1
  51. package/dist/host/sandbox/contained-deps.js +6 -4
  52. package/dist/host/sandbox/platform.d.ts +2 -0
  53. package/dist/host/sandbox/platform.js +19 -4
  54. package/dist/host/sharing/lifecycle.js +9 -1
  55. package/dist/host/storage/contained/contained-storage.js +13 -0
  56. package/dist/host/storage/contained/disk-port.d.ts +5 -0
  57. package/dist/host/storage/contained/helper-port.d.ts +72 -0
  58. package/dist/host/storage/contained/helper-port.js +162 -0
  59. package/dist/host/storage/contained/index.d.ts +1 -0
  60. package/dist/host/storage/contained/index.js +1 -0
  61. package/dist/host/storage/contained/state.d.ts +3 -2
  62. package/dist/host/storage/contained/state.js +5 -2
  63. package/dist/host/web/bridge.d.ts +5 -0
  64. package/dist/host/web/bridge.js +11 -1
  65. package/dist/host/web/server.d.ts +2 -0
  66. package/dist/host/web/server.js +11 -1
  67. package/dist/runtime/agent.js +9 -0
  68. package/dist/runtime/daemon.js +57 -3
  69. package/dist/runtime/db.js +9 -0
  70. package/dist/runtime/main.js +195 -12
  71. package/dist/runtime/netproxy.js +9 -0
  72. package/dist/runtime/view.js +195 -12
  73. package/dist/selftest/channel.test.js +72 -1
  74. package/dist/selftest/contained.test.js +1 -37
  75. package/dist/selftest/hcs.test.js +5 -2
  76. package/dist/selftest/index.js +2 -0
  77. package/dist/selftest/install.test.js +11 -0
  78. package/dist/selftest/layout.test.js +10 -1
  79. package/dist/selftest/live-daemon.test.js +51 -1
  80. package/dist/selftest/live-terminal.test.js +112 -4
  81. package/dist/selftest/menu-perf.test.js +198 -12
  82. package/dist/selftest/menu.test.js +4 -0
  83. package/dist/selftest/storage-helper.test.d.ts +2 -0
  84. package/dist/selftest/storage-helper.test.js +493 -0
  85. package/dist/selftest/support/vhdx.d.ts +14 -0
  86. package/dist/selftest/support/vhdx.js +72 -0
  87. package/dist/selftest/web.test.js +14 -2
  88. package/dist/shared/channel/run-dir.d.ts +6 -0
  89. package/dist/shared/channel/run-dir.js +9 -0
  90. package/dist/shared/pty/index.d.ts +3 -1
  91. package/dist/shared/pty/index.js +3 -1
  92. package/dist/shared/pty/key-gate.d.ts +24 -0
  93. package/dist/shared/pty/key-gate.js +45 -0
  94. package/dist/shared/pty/mouse-modes.d.ts +27 -0
  95. package/dist/shared/pty/mouse-modes.js +155 -0
  96. package/dist/shared/pty/terminal.d.ts +0 -5
  97. package/dist/shared/pty/terminal.js +51 -20
  98. package/dist/sync/watcher.js +1 -1
  99. package/dist/web/index.html +1 -1
  100. package/dist/web/{main-Z2WYAE2Z.js → main-6XNBTJLT.js} +34 -34
  101. package/native/csharp/HcsHost/AssemblyInfo.cs +3 -3
  102. package/native/csharp/HostTools/AssemblyInfo.cs +3 -3
  103. package/native/csharp/StorageHelper/AssemblyInfo.cs +15 -0
  104. package/native/csharp/StorageHelper/Client.cs +297 -0
  105. package/native/csharp/StorageHelper/Log.cs +177 -0
  106. package/native/csharp/StorageHelper/MountRecords.cs +160 -0
  107. package/native/csharp/StorageHelper/Operations.cs +596 -0
  108. package/native/csharp/StorageHelper/Pins.cs +270 -0
  109. package/native/csharp/StorageHelper/Privileges.cs +78 -0
  110. package/native/csharp/StorageHelper/Program.cs +349 -0
  111. package/native/csharp/StorageHelper/Protocol.cs +107 -0
  112. package/native/csharp/StorageHelper/RateLimiter.cs +97 -0
  113. package/native/csharp/StorageHelper/Rules.cs +156 -0
  114. package/native/csharp/StorageHelper/Server.cs +656 -0
  115. package/native/csharp/StorageHelper/Settings.cs +297 -0
  116. package/native/csharp/StorageHelper/StorageService.cs +53 -0
  117. package/native/csharp/StorageHelper/UserSettings.cs +206 -0
  118. package/native/csharp/StorageHelper/Vhdx.cs +230 -0
  119. package/native/csharp/StorageHelper/VirtualDisk.cs +241 -0
  120. package/native/csharp/StorageHelper/Volumes.cs +379 -0
  121. package/native/csharp/StorageHelper/Watchdog.cs +128 -0
  122. package/native/csharp/StorageHelper/Win32.cs +250 -0
  123. package/native/scripts/set-storage-helper.ps1 +203 -0
  124. package/package.json +1 -1
  125. package/sandbox/csharp/PtyHost.cs +236 -1
@@ -155,12 +155,15 @@ export function mountedSets(storageRoot) {
155
155
  }
156
156
  /**
157
157
  * A line for the start of a command when the record says an image is mounted that no running process holds and no
158
- * sandbox uses (nothing is looked up on the disk: this is only the record), or null.
158
+ * sandbox uses (nothing is looked up on the disk: this is only the record), or null. `sandboxRunning` can be a
159
+ * function: it is only asked (it runs tasklist) when the record says an image is mounted.
159
160
  */
160
161
  export function staleMountNote(storageRoot, set, sandboxRunning, alive = processAlive) {
161
162
  const record = setStateOf(new JsonFile(ContainedStateSchema, path.join(path.resolve(storageRoot), 'mounts', 'state.json')).read() ??
162
163
  EMPTY_STATE, set);
163
- if (!record.mounted || sandboxRunning || record.holders.some((h) => alive(h.pid)))
164
+ if (!record.mounted || record.holders.some((h) => alive(h.pid)))
165
+ return null;
166
+ if (typeof sandboxRunning === 'function' ? sandboxRunning() : sandboxRunning)
164
167
  return null;
165
168
  return 'Contained storage is still mounted from an earlier sandbox. Run sandbox repair to dismount it (Windows asks for permission).';
166
169
  }
@@ -23,6 +23,11 @@ export declare class TerminalBridge {
23
23
  start(): Promise<void>;
24
24
  private fromBrowser;
25
25
  private fromSession;
26
+ /**
27
+ * The program's mouse tracking ends with it: the browser's terminal stops sending clicks and moves, and a drag
28
+ * selects text again (an ended terminal stays on screen).
29
+ */
30
+ private mouseOff;
26
31
  /** Stops reading from the session until the browser has taken what it was sent. */
27
32
  private hold;
28
33
  private tell;
@@ -1,5 +1,5 @@
1
1
  import { safeText } from '../../core/util/index.js';
2
- import { connect, encodeFrame, FrameReader, FrameType, helloFrame, OutputFilter, PING_INTERVAL_MS, resizeFrame, } from '../../shared/pty/index.js';
2
+ import { connect, encodeFrame, FrameReader, FrameType, helloFrame, MOUSE_OFF, OutputFilter, PING_INTERVAL_MS, resizeFrame, } from '../../shared/pty/index.js';
3
3
  import { parseClientMessage } from '../../shared/web/index.js';
4
4
  /** The browser is slower than the sandbox: past this much waiting to be sent, output is held back in the socket. */
5
5
  const MAX_BUFFERED_BYTES = 4 * 1024 * 1024;
@@ -94,9 +94,11 @@ export class TerminalBridge {
94
94
  break;
95
95
  }
96
96
  case FrameType.Restarting:
97
+ this.mouseOff();
97
98
  this.tell({ t: 'restarting' });
98
99
  break;
99
100
  case FrameType.Exit:
101
+ this.mouseOff();
100
102
  this.tell({ t: 'exit', code: frame.payload.length >= 4 ? frame.payload.readInt32LE(0) : 1 });
101
103
  this.finish(1000, 'exit');
102
104
  return;
@@ -111,6 +113,14 @@ export class TerminalBridge {
111
113
  if (this.ws.bufferedAmount > MAX_BUFFERED_BYTES)
112
114
  this.hold();
113
115
  }
116
+ /**
117
+ * The program's mouse tracking ends with it: the browser's terminal stops sending clicks and moves, and a drag
118
+ * selects text again (an ended terminal stays on screen).
119
+ */
120
+ mouseOff() {
121
+ if (this.ws.readyState === this.ws.OPEN)
122
+ this.ws.send(Buffer.from(MOUSE_OFF, 'latin1'), { binary: true });
123
+ }
114
124
  /** Stops reading from the session until the browser has taken what it was sent. */
115
125
  hold() {
116
126
  if (this.drain || !this.socket)
@@ -55,6 +55,7 @@ export declare class WebServer {
55
55
  private readonly live;
56
56
  private actualPort;
57
57
  private lastUsed;
58
+ private lastCheck;
58
59
  private heartbeat;
59
60
  private idleCheck;
60
61
  constructor(options: WebServerOptions);
@@ -69,6 +70,7 @@ export declare class WebServer {
69
70
  private beat;
70
71
  /** No browser has used the server for a while: it ends (sooner when no sandbox runs). */
71
72
  private checkIdle;
73
+ private checkEveryMs;
72
74
  /** How long unused before it ends when no sandbox runs. */
73
75
  private quietMs;
74
76
  /** Counts a new socket; at the limit, the oldest of its kind makes room (a new tab always works). */
@@ -55,6 +55,7 @@ export class WebServer {
55
55
  live;
56
56
  actualPort = 0;
57
57
  lastUsed = Date.now();
58
+ lastCheck = Date.now();
58
59
  heartbeat;
59
60
  idleCheck;
60
61
  constructor(options) {
@@ -82,7 +83,8 @@ export class WebServer {
82
83
  this.heartbeat = setInterval(() => this.beat(), this.options.heartbeatMs ?? HEARTBEAT_MS);
83
84
  this.heartbeat.unref();
84
85
  if (this.options.idleExitMs) {
85
- this.idleCheck = setInterval(() => void this.checkIdle(), Math.min(60_000, this.quietMs()));
86
+ this.lastCheck = Date.now();
87
+ this.idleCheck = setInterval(() => void this.checkIdle(), this.checkEveryMs());
86
88
  this.idleCheck.unref();
87
89
  }
88
90
  const gate = this.options.gate;
@@ -127,6 +129,11 @@ export class WebServer {
127
129
  /** No browser has used the server for a while: it ends (sooner when no sandbox runs). */
128
130
  async checkIdle() {
129
131
  const limit = this.options.idleExitMs ?? 0;
132
+ const now = Date.now();
133
+ // A check much later than planned: the PC slept. Its tabs get time to reconnect before anything counts as idle.
134
+ if (now - this.lastCheck > 3 * this.checkEveryMs())
135
+ this.lastUsed = now;
136
+ this.lastCheck = now;
130
137
  if (!limit || this.tracked.size > 0)
131
138
  return;
132
139
  const idle = Date.now() - this.lastUsed;
@@ -143,6 +150,9 @@ export class WebServer {
143
150
  }
144
151
  this.options.onStop?.();
145
152
  }
153
+ checkEveryMs() {
154
+ return Math.min(60_000, this.quietMs());
155
+ }
146
156
  /** How long unused before it ends when no sandbox runs. */
147
157
  quietMs() {
148
158
  return Math.min(this.options.idleExitMs ?? 0, this.options.idleNoSandboxMs ?? IDLE_NO_SANDBOX_MS);
@@ -27173,6 +27173,11 @@ var RunDir = class {
27173
27173
  projectsDir;
27174
27174
  /** audit.processes: programs the sandbox started, in small .jsonl files the host takes in and deletes. */
27175
27175
  auditDir;
27176
+ /**
27177
+ * Each running session's process tree, recorded by its ptyhost (guest only: the daemon ends what a ptyhost that was
27178
+ * itself ended left running).
27179
+ */
27180
+ treesDir;
27176
27181
  /** One-shot secrets the host hands over (see secrets.ts). Next to run, not in it: shared-use users can change run. */
27177
27182
  secretsDir;
27178
27183
  constructor(dataDir2) {
@@ -27186,6 +27191,7 @@ var RunDir = class {
27186
27191
  this.sessionsDir = path5.join(this.root, "sessions");
27187
27192
  this.projectsDir = path5.join(this.root, "projects");
27188
27193
  this.auditDir = path5.join(this.root, "audit");
27194
+ this.treesDir = path5.join(this.root, "trees");
27189
27195
  this.secretsDir = path5.join(dataDir2, "secrets");
27190
27196
  }
27191
27197
  requestFile(id) {
@@ -27197,6 +27203,9 @@ var RunDir = class {
27197
27203
  sessionFile(id) {
27198
27204
  return path5.join(this.sessionsDir, safeId(id) + ".json");
27199
27205
  }
27206
+ treeFile(id) {
27207
+ return path5.join(this.treesDir, safeId(id) + ".json");
27208
+ }
27200
27209
  projectFile(name) {
27201
27210
  return path5.join(this.projectsDir, safeId(name) + ".json");
27202
27211
  }
@@ -27338,6 +27338,11 @@ var RunDir = class {
27338
27338
  projectsDir;
27339
27339
  /** audit.processes: programs the sandbox started, in small .jsonl files the host takes in and deletes. */
27340
27340
  auditDir;
27341
+ /**
27342
+ * Each running session's process tree, recorded by its ptyhost (guest only: the daemon ends what a ptyhost that was
27343
+ * itself ended left running).
27344
+ */
27345
+ treesDir;
27341
27346
  /** One-shot secrets the host hands over (see secrets.ts). Next to run, not in it: shared-use users can change run. */
27342
27347
  secretsDir;
27343
27348
  constructor(dataDir2) {
@@ -27351,6 +27356,7 @@ var RunDir = class {
27351
27356
  this.sessionsDir = path7.join(this.root, "sessions");
27352
27357
  this.projectsDir = path7.join(this.root, "projects");
27353
27358
  this.auditDir = path7.join(this.root, "audit");
27359
+ this.treesDir = path7.join(this.root, "trees");
27354
27360
  this.secretsDir = path7.join(dataDir2, "secrets");
27355
27361
  }
27356
27362
  requestFile(id) {
@@ -27362,6 +27368,9 @@ var RunDir = class {
27362
27368
  sessionFile(id) {
27363
27369
  return path7.join(this.sessionsDir, safeId(id) + ".json");
27364
27370
  }
27371
+ treeFile(id) {
27372
+ return path7.join(this.treesDir, safeId(id) + ".json");
27373
+ }
27365
27374
  projectFile(name) {
27366
27375
  return path7.join(this.projectsDir, safeId(name) + ".json");
27367
27376
  }
@@ -32426,7 +32435,7 @@ function shellLaunch(asked, paths) {
32426
32435
  // src/guest/sessions/pty-host.ts
32427
32436
  import fs50 from "node:fs";
32428
32437
  import path65 from "node:path";
32429
- import { spawn as spawn6 } from "node:child_process";
32438
+ import { execFile, spawn as spawn6 } from "node:child_process";
32430
32439
  function ensurePtyHost() {
32431
32440
  return ensureCompiled("ptyhost");
32432
32441
  }
@@ -32458,7 +32467,12 @@ var conptyDll = () => {
32458
32467
  return session.provisioning.conpty?.enabled !== false && fs50.existsSync(dll) && fs50.existsSync(path65.join(toolsDir, "conpty", "OpenConsole.exe")) ? dll : null;
32459
32468
  };
32460
32469
  function startPtyHost(o) {
32461
- const env = { ...o.env, PTYHOST_BIND: o.remote ? "any" : "loopback" };
32470
+ fs50.mkdirSync(run.treesDir, { recursive: true });
32471
+ const env = {
32472
+ ...o.env,
32473
+ PTYHOST_BIND: o.remote ? "any" : "loopback",
32474
+ PTYHOST_TREE_FILE: run.treeFile(o.id)
32475
+ };
32462
32476
  delete env.PTYHOST_CONPTY;
32463
32477
  const dll = conptyDll();
32464
32478
  if (dll) env.PTYHOST_CONPTY = dll;
@@ -32468,6 +32482,29 @@ function startPtyHost(o) {
32468
32482
  { cwd: o.cwd, env, detached: true, stdio: "ignore", windowsHide: true }
32469
32483
  );
32470
32484
  }
32485
+ var END_TREE_MS = 3e4;
32486
+ function endLeftoverTree(id, exe = ensurePtyHost()) {
32487
+ const file3 = run.treeFile(id);
32488
+ if (!fs50.existsSync(file3)) return Promise.resolve(null);
32489
+ return new Promise((resolve2) => {
32490
+ execFile(exe, ["--end-tree", file3], { timeout: END_TREE_MS, windowsHide: true }, (error62, stdout) => {
32491
+ const code = error62 ? typeof error62.code === "number" ? error62.code : -1 : 0;
32492
+ if (code !== 3)
32493
+ try {
32494
+ fs50.rmSync(file3, { force: true });
32495
+ } catch {
32496
+ }
32497
+ resolve2(code === 0 ? stdout.trim().slice(0, 200) : code === 3 ? null : "could not end them (code " + code + ")");
32498
+ });
32499
+ });
32500
+ }
32501
+ function recordedTrees() {
32502
+ try {
32503
+ return fs50.readdirSync(run.treesDir).filter((n) => /^[A-Za-z0-9_-]{1,64}\.json$/.test(n)).map((n) => n.slice(0, -".json".length));
32504
+ } catch {
32505
+ return [];
32506
+ }
32507
+ }
32471
32508
 
32472
32509
  // src/guest/agents/agent-installer.ts
32473
32510
  import fs51 from "node:fs";
@@ -32797,7 +32834,23 @@ var SessionManager = class {
32797
32834
  }
32798
32835
  rootStarted(id, child, from) {
32799
32836
  if (child.pid) this.ptyHosts.set(id, { session: id, pid: child.pid, from });
32800
- child.once("exit", () => this.rootEnded(id));
32837
+ child.once("exit", () => {
32838
+ this.rootEnded(id);
32839
+ this.endLeftovers(id);
32840
+ });
32841
+ }
32842
+ /** What a session's ptyhost left running when it was itself ended (Task Manager, a crash) is ended now. */
32843
+ endLeftovers(id) {
32844
+ void endLeftoverTree(id).then((done) => {
32845
+ if (done) log("Session " + id + ": its session host was ended from outside; " + done + " it left running");
32846
+ }).catch((e) => warn("Ending what session " + id + " left running failed: " + errMsg(e)));
32847
+ }
32848
+ /**
32849
+ * At the daemon's start: trees recorded by session hosts that are gone (a daemon that ended with them) are ended
32850
+ * too. One whose session host still runs is left alone (ptyhost --end-tree checks).
32851
+ */
32852
+ sweepLeftovers() {
32853
+ for (const id of recordedTrees()) if (!this.live.has(id)) this.endLeftovers(id);
32801
32854
  }
32802
32855
  rootEnded(id) {
32803
32856
  const r = this.ptyHosts.get(id);
@@ -33505,6 +33558,7 @@ adoptBootSecrets();
33505
33558
  log("Session daemon started (pid " + process.pid + ").");
33506
33559
  safely("port forwards", startRelays);
33507
33560
  safely("audit", () => startProcessCollector(() => sessions.roots()));
33561
+ safely("leftover processes", () => sessions.sweepLeftovers());
33508
33562
  safely("memory governor", () => governor.start());
33509
33563
  log("Memory governor: " + governor.describe());
33510
33564
  new Daemon().start();
@@ -27211,6 +27211,11 @@ var RunDir = class {
27211
27211
  projectsDir;
27212
27212
  /** audit.processes: programs the sandbox started, in small .jsonl files the host takes in and deletes. */
27213
27213
  auditDir;
27214
+ /**
27215
+ * Each running session's process tree, recorded by its ptyhost (guest only: the daemon ends what a ptyhost that was
27216
+ * itself ended left running).
27217
+ */
27218
+ treesDir;
27214
27219
  /** One-shot secrets the host hands over (see secrets.ts). Next to run, not in it: shared-use users can change run. */
27215
27220
  secretsDir;
27216
27221
  constructor(dataDir2) {
@@ -27224,6 +27229,7 @@ var RunDir = class {
27224
27229
  this.sessionsDir = path5.join(this.root, "sessions");
27225
27230
  this.projectsDir = path5.join(this.root, "projects");
27226
27231
  this.auditDir = path5.join(this.root, "audit");
27232
+ this.treesDir = path5.join(this.root, "trees");
27227
27233
  this.secretsDir = path5.join(dataDir2, "secrets");
27228
27234
  }
27229
27235
  requestFile(id) {
@@ -27235,6 +27241,9 @@ var RunDir = class {
27235
27241
  sessionFile(id) {
27236
27242
  return path5.join(this.sessionsDir, safeId(id) + ".json");
27237
27243
  }
27244
+ treeFile(id) {
27245
+ return path5.join(this.treesDir, safeId(id) + ".json");
27246
+ }
27238
27247
  projectFile(name) {
27239
27248
  return path5.join(this.projectsDir, safeId(name) + ".json");
27240
27249
  }
@@ -27322,6 +27322,11 @@ var RunDir = class {
27322
27322
  projectsDir;
27323
27323
  /** audit.processes: programs the sandbox started, in small .jsonl files the host takes in and deletes. */
27324
27324
  auditDir;
27325
+ /**
27326
+ * Each running session's process tree, recorded by its ptyhost (guest only: the daemon ends what a ptyhost that was
27327
+ * itself ended left running).
27328
+ */
27329
+ treesDir;
27325
27330
  /** One-shot secrets the host hands over (see secrets.ts). Next to run, not in it: shared-use users can change run. */
27326
27331
  secretsDir;
27327
27332
  constructor(dataDir2) {
@@ -27335,6 +27340,7 @@ var RunDir = class {
27335
27340
  this.sessionsDir = path6.join(this.root, "sessions");
27336
27341
  this.projectsDir = path6.join(this.root, "projects");
27337
27342
  this.auditDir = path6.join(this.root, "audit");
27343
+ this.treesDir = path6.join(this.root, "trees");
27338
27344
  this.secretsDir = path6.join(dataDir2, "secrets");
27339
27345
  }
27340
27346
  requestFile(id) {
@@ -27346,6 +27352,9 @@ var RunDir = class {
27346
27352
  sessionFile(id) {
27347
27353
  return path6.join(this.sessionsDir, safeId(id) + ".json");
27348
27354
  }
27355
+ treeFile(id) {
27356
+ return path6.join(this.treesDir, safeId(id) + ".json");
27357
+ }
27349
27358
  projectFile(name) {
27350
27359
  return path6.join(this.projectsDir, safeId(name) + ".json");
27351
27360
  }
@@ -35339,12 +35348,175 @@ var CtrlCGuard = class {
35339
35348
  }
35340
35349
  };
35341
35350
 
35342
- // src/shared/pty/terminal.ts
35343
- var MOUSE_OFF = "\x1B[?1000l\x1B[?1002l\x1B[?1003l\x1B[?1005l\x1B[?1006l\x1B[?1015l";
35351
+ // src/shared/pty/key-gate.ts
35344
35352
  function fixWindowsKeys(chunk) {
35345
35353
  if (!chunk.includes(8) && !chunk.includes(127)) return chunk;
35346
35354
  return Buffer.from(chunk.map((b) => b === 8 ? 127 : b === 127 ? 8 : b));
35347
35355
  }
35356
+ var KeyGate = class {
35357
+ pending = [];
35358
+ decided;
35359
+ swap = false;
35360
+ /** windowsConsole: input comes from a Windows console in raw mode (its mode is decided later); else it goes as is. */
35361
+ constructor(windowsConsole) {
35362
+ this.decided = !windowsConsole;
35363
+ }
35364
+ /** The bytes to send now (empty while the mode is not known yet). */
35365
+ push(data) {
35366
+ if (!data.length) return data;
35367
+ if (!this.decided) {
35368
+ this.pending.push(data);
35369
+ return Buffer.alloc(0);
35370
+ }
35371
+ return this.swap ? fixWindowsKeys(data) : data;
35372
+ }
35373
+ /** The console's mode is known: what was held, ready to send. Later calls only change how new input is read. */
35374
+ decide(vtInput) {
35375
+ this.swap = !vtInput;
35376
+ this.decided = true;
35377
+ const held = Buffer.concat(this.pending);
35378
+ this.pending = [];
35379
+ return this.swap ? fixWindowsKeys(held) : held;
35380
+ }
35381
+ };
35382
+
35383
+ // src/shared/pty/mouse-modes.ts
35384
+ var TRACKING = [9, 1e3, 1002, 1003];
35385
+ var MAX_PARAMS = 64;
35386
+ var MOTION = 32;
35387
+ var NO_BUTTON = 3;
35388
+ var MouseModes = class {
35389
+ on = /* @__PURE__ */ new Set();
35390
+ /** 0 text, 1 after ESC, 2 after ESC[, 3 in ESC[? parameters, 4 after ESC[! */
35391
+ state = 0;
35392
+ params = "";
35393
+ /** Reads the program's output (after OutputFilter): the mode changes in it. */
35394
+ watch(output3) {
35395
+ for (const b of output3) {
35396
+ if (b === 27) {
35397
+ this.state = 1;
35398
+ continue;
35399
+ }
35400
+ if (this.state === 1) {
35401
+ if (b === 99) this.on.clear();
35402
+ this.state = b === 91 ? 2 : 0;
35403
+ } else if (this.state === 2) {
35404
+ this.state = b === 63 ? 3 : b === 33 ? 4 : 0;
35405
+ this.params = "";
35406
+ } else if (this.state === 4) {
35407
+ if (b === 112) this.on.clear();
35408
+ this.state = 0;
35409
+ } else if (this.state === 3) {
35410
+ if (b >= 48 && b <= 57 || b === 59) {
35411
+ this.params += String.fromCharCode(b);
35412
+ if (this.params.length > MAX_PARAMS) this.state = 0;
35413
+ } else {
35414
+ if (b === 104 || b === 108) this.apply(this.params, b === 104);
35415
+ this.state = 0;
35416
+ }
35417
+ }
35418
+ }
35419
+ }
35420
+ /** The program is gone (exited or restarting): its modes went with it. */
35421
+ reset() {
35422
+ this.on.clear();
35423
+ this.state = 0;
35424
+ this.params = "";
35425
+ }
35426
+ /** True while the program tracks the mouse in any way. */
35427
+ tracking() {
35428
+ return TRACKING.some((m) => this.on.has(m));
35429
+ }
35430
+ /** True while the program has a mode on (2004: bracketed paste). */
35431
+ has(mode) {
35432
+ return this.on.has(mode);
35433
+ }
35434
+ /** The mode numbers on now (for the report and tests). */
35435
+ modes() {
35436
+ return [...this.on].sort((a, b) => a - b);
35437
+ }
35438
+ /** `input` without the mouse reports the program has not asked for. Sequences must be whole (InputJoiner). */
35439
+ filter(input2) {
35440
+ if (!input2.includes(27)) return input2;
35441
+ const parts2 = [];
35442
+ let from = 0;
35443
+ let i = input2.indexOf(27);
35444
+ while (i !== -1) {
35445
+ const report = mouseReport(input2, i, this.on.has(1005), this.on.has(1015));
35446
+ if (report && !this.allows(report.button)) {
35447
+ parts2.push(input2.subarray(from, i));
35448
+ from = i + report.length;
35449
+ i = input2.indexOf(27, from);
35450
+ } else i = input2.indexOf(27, i + 1);
35451
+ }
35452
+ if (from === 0) return input2;
35453
+ parts2.push(input2.subarray(from));
35454
+ return Buffer.concat(parts2);
35455
+ }
35456
+ allows(button) {
35457
+ if (!this.tracking()) return false;
35458
+ if ((button & MOTION) === 0) return true;
35459
+ if (this.on.has(1003)) return true;
35460
+ return this.on.has(1002) && (button & NO_BUTTON) !== NO_BUTTON;
35461
+ }
35462
+ apply(params, set2) {
35463
+ for (const p of params.split(";")) {
35464
+ const mode = Number(p);
35465
+ if (!p || !Number.isSafeInteger(mode)) continue;
35466
+ if (set2) this.on.add(mode);
35467
+ else this.on.delete(mode);
35468
+ }
35469
+ }
35470
+ };
35471
+ function mouseReport(data, at, utf8, urxvt) {
35472
+ if (data[at + 1] !== 91) return null;
35473
+ const sgr = data[at + 2] === 60;
35474
+ if (sgr || urxvt && data[at + 2] >= 48 && data[at + 2] <= 57) {
35475
+ let i = at + (sgr ? 3 : 2);
35476
+ let digits = "";
35477
+ let fields = 1;
35478
+ for (; i < data.length && i - at < 32; i++) {
35479
+ const b = data[i];
35480
+ if (b >= 48 && b <= 57) {
35481
+ if (fields === 1) digits += String.fromCharCode(b);
35482
+ } else if (b === 59) fields++;
35483
+ else break;
35484
+ }
35485
+ if (i >= data.length || fields !== 3 || !digits) return null;
35486
+ if (sgr ? data[i] !== 77 && data[i] !== 109 : data[i] !== 77) return null;
35487
+ const button = Number(digits) - (sgr ? 0 : 32);
35488
+ return button >= 0 ? { button, length: i - at + 1 } : null;
35489
+ }
35490
+ if (data[at + 2] === 77 && !utf8 && data.length - at >= 6) {
35491
+ const button = data[at + 3] - 32;
35492
+ return button >= 0 ? { button, length: 6 } : null;
35493
+ }
35494
+ return null;
35495
+ }
35496
+
35497
+ // src/shared/pty/terminal.ts
35498
+ var MOUSE_OFF = "\x1B[?1000l\x1B[?1002l\x1B[?1003l\x1B[?1005l\x1B[?1006l\x1B[?1015l";
35499
+ var SETTLE_QUIET_MS = 50;
35500
+ var SETTLE_MAX_MS = 300;
35501
+ function drainInput(stream) {
35502
+ return new Promise((done) => {
35503
+ let quiet;
35504
+ const end = () => {
35505
+ clearTimeout(quiet);
35506
+ clearTimeout(cap);
35507
+ stream.off("data", onData);
35508
+ done();
35509
+ };
35510
+ const onData = () => {
35511
+ clearTimeout(quiet);
35512
+ quiet = setTimeout(end, SETTLE_QUIET_MS);
35513
+ };
35514
+ const cap = setTimeout(end, SETTLE_MAX_MS);
35515
+ stream.on("data", onData);
35516
+ onData();
35517
+ });
35518
+ }
35519
+ var VT_WAIT_MS = 1e4;
35348
35520
  function runTerminalClient(socket, token, options = {}) {
35349
35521
  const stdin = process.stdin;
35350
35522
  const stdout = process.stdout;
@@ -35352,14 +35524,16 @@ function runTerminalClient(socket, token, options = {}) {
35352
35524
  const [c, r] = stdout.isTTY ? stdout.getWindowSize() : [0, 0];
35353
35525
  return [c || 120, r || 30];
35354
35526
  };
35355
- let swapKeys = process.platform === "win32" && stdin.isTTY;
35527
+ const keys = new KeyGate(process.platform === "win32" && Boolean(stdin.isTTY));
35356
35528
  return new Promise((resolve2) => {
35357
35529
  let done = false;
35358
35530
  const reader = new FrameReader();
35359
35531
  const filter = new OutputFilter({ clipboardWrites: options.clipboardWrites });
35360
35532
  let sent = size();
35361
35533
  let resizeTimer;
35362
- let vtInput;
35534
+ let vtInput, vtTimer;
35535
+ const mouse = new MouseModes();
35536
+ let passthrough = false;
35363
35537
  const finish = (result) => {
35364
35538
  if (done) return;
35365
35539
  done = true;
@@ -35370,9 +35544,13 @@ function runTerminalClient(socket, token, options = {}) {
35370
35544
  clearInterval(sizeWatch);
35371
35545
  clearTimeout(joinTimer);
35372
35546
  clearTimeout(hintTimer);
35547
+ clearTimeout(vtTimer);
35373
35548
  socket.destroy();
35374
- stdout.write("\x1B[0m\x1B[?25h" + MOUSE_OFF + "\r\n");
35375
- void (vtInput?.stop() ?? Promise.resolve()).then(() => {
35549
+ const tracked = mouse.tracking();
35550
+ const reset = "\x1B[0m\x1B[?25h" + MOUSE_OFF + (mouse.has(2004) ? "\x1B[?2004l" : "") + "\r\n";
35551
+ const flushed = new Promise((written) => stdout.write(reset, () => written()));
35552
+ const settled = tracked ? flushed.then(() => drainInput(stdin)) : Promise.resolve();
35553
+ void Promise.all([settled, vtInput?.stop()]).then(() => {
35376
35554
  if (stdin.isTTY) stdin.setRawMode(false);
35377
35555
  stdin.pause();
35378
35556
  resolve2(result);
@@ -35380,9 +35558,10 @@ function runTerminalClient(socket, token, options = {}) {
35380
35558
  };
35381
35559
  const joiner = new InputJoiner();
35382
35560
  let joinTimer;
35383
- const sendInput = (data) => {
35384
- if (data.length && !done) socket.write(encodeFrame(FrameType.Input, swapKeys ? fixWindowsKeys(data) : data));
35561
+ const write = (data) => {
35562
+ if (data.length && !done) socket.write(encodeFrame(FrameType.Input, data));
35385
35563
  };
35564
+ const sendInput = (data) => write(keys.push(passthrough ? mouse.filter(data) : data));
35386
35565
  const guard = options.confirmCtrlC ? new CtrlCGuard() : null;
35387
35566
  let hintTimer;
35388
35567
  const onInput = (chunk) => {
@@ -35404,7 +35583,6 @@ function runTerminalClient(socket, token, options = {}) {
35404
35583
  sendInput(joiner.push(chunk));
35405
35584
  if (joiner.holding()) joinTimer = setTimeout(() => sendInput(joiner.flush()), JOIN_MS);
35406
35585
  };
35407
- let passthrough = false;
35408
35586
  const sendSize = () => {
35409
35587
  const [c2, r2] = size();
35410
35588
  if (c2 === sent[0] && r2 === sent[1]) return;
@@ -35434,11 +35612,14 @@ function runTerminalClient(socket, token, options = {}) {
35434
35612
  for (const frame of frames) {
35435
35613
  if (frame.type === FrameType.Output) {
35436
35614
  const out = filter.push(frame.payload);
35615
+ mouse.watch(out);
35437
35616
  guard?.watchOutput(out.toString("latin1"));
35438
35617
  stdout.write(out);
35439
35618
  } else if (frame.type === FrameType.Console) passthrough = frame.payload.toString("latin1") === "conpty.dll";
35440
- else if (frame.type === FrameType.Restarting) stdout.write(MOUSE_OFF + "\x1B[2J\x1B[3J\x1B[H");
35441
- else if (frame.type === FrameType.Exit)
35619
+ else if (frame.type === FrameType.Restarting) {
35620
+ mouse.reset();
35621
+ stdout.write(MOUSE_OFF + "\x1B[2J\x1B[3J\x1B[H");
35622
+ } else if (frame.type === FrameType.Exit)
35442
35623
  finish({ reason: "exit", exitCode: frame.payload.length >= 4 ? frame.payload.readInt32LE(0) : 1 });
35443
35624
  else if (frame.type === FrameType.Error)
35444
35625
  finish({
@@ -35454,8 +35635,10 @@ function runTerminalClient(socket, token, options = {}) {
35454
35635
  socket.write(helloFrame(token, c, r));
35455
35636
  if (stdin.isTTY) stdin.setRawMode(true);
35456
35637
  vtInput = enableVtInput();
35638
+ vtTimer = setTimeout(() => write(keys.decide(false)), VT_WAIT_MS);
35457
35639
  void vtInput.ready.then((on) => {
35458
- if (on) swapKeys = false;
35640
+ clearTimeout(vtTimer);
35641
+ write(keys.decide(on));
35459
35642
  });
35460
35643
  stdin.on("data", onInput);
35461
35644
  stdin.resume();
@@ -27202,6 +27202,11 @@ var RunDir = class {
27202
27202
  projectsDir;
27203
27203
  /** audit.processes: programs the sandbox started, in small .jsonl files the host takes in and deletes. */
27204
27204
  auditDir;
27205
+ /**
27206
+ * Each running session's process tree, recorded by its ptyhost (guest only: the daemon ends what a ptyhost that was
27207
+ * itself ended left running).
27208
+ */
27209
+ treesDir;
27205
27210
  /** One-shot secrets the host hands over (see secrets.ts). Next to run, not in it: shared-use users can change run. */
27206
27211
  secretsDir;
27207
27212
  constructor(dataDir2) {
@@ -27215,6 +27220,7 @@ var RunDir = class {
27215
27220
  this.sessionsDir = path3.join(this.root, "sessions");
27216
27221
  this.projectsDir = path3.join(this.root, "projects");
27217
27222
  this.auditDir = path3.join(this.root, "audit");
27223
+ this.treesDir = path3.join(this.root, "trees");
27218
27224
  this.secretsDir = path3.join(dataDir2, "secrets");
27219
27225
  }
27220
27226
  requestFile(id) {
@@ -27226,6 +27232,9 @@ var RunDir = class {
27226
27232
  sessionFile(id) {
27227
27233
  return path3.join(this.sessionsDir, safeId(id) + ".json");
27228
27234
  }
27235
+ treeFile(id) {
27236
+ return path3.join(this.treesDir, safeId(id) + ".json");
27237
+ }
27229
27238
  projectFile(name) {
27230
27239
  return path3.join(this.projectsDir, safeId(name) + ".json");
27231
27240
  }