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.
- package/config.example.json +2 -1
- package/dist/app/features/defs.d.ts +1 -1
- package/dist/app/features/projects/projects.def.d.ts +1 -1
- package/dist/app/features/projects/projects.run.d.ts +1 -1
- package/dist/app/features/projects/projects.run.js +10 -2
- package/dist/app/features/sandbox/sandbox.def.js +3 -1
- package/dist/app/features/settings/categories.def.js +1 -0
- package/dist/app/features/storage/storage.run.js +1 -1
- package/dist/bin/agent-sandbox.js +2 -2
- package/dist/bin/{chunk-VENWSGX2.js → chunk-D7CM63BS.js} +2303 -1462
- package/dist/bin/{chunk-K4SU3MHJ.js → chunk-HTQT5SBY.js} +1 -1
- package/dist/bin/{chunk-IL7BXJF2.js → chunk-RQ4BWBYX.js} +1 -1
- package/dist/bin/claude-host.js +2 -2
- package/dist/bin/claude-sbx.js +2 -2
- package/dist/bin/codex-sbx.js +2 -2
- package/dist/bin/gemini-sbx.js +2 -2
- package/dist/bin/sandbox.js +2 -2
- package/dist/bin/sbx-hide.js +2 -2
- package/dist/bin/sbx-show.js +2 -2
- package/dist/cli/commands/launch/launch.js +1 -1
- package/dist/cli/commands/projects/here.js +2 -0
- package/dist/cli/commands/repair/checks/contained.js +1 -1
- package/dist/cli/services/health-service.js +15 -3
- package/dist/cli/services/projects-service.d.ts +8 -3
- package/dist/cli/services/projects-service.js +21 -4
- package/dist/core/config/defaults.js +1 -0
- package/dist/core/schema/config.d.ts +4 -0
- package/dist/core/schema/projects.d.ts +4 -0
- package/dist/core/schema/projects.js +3 -0
- package/dist/guest/boot/boot.js +8 -2
- package/dist/guest/daemon/daemon.js +3 -0
- package/dist/guest/daemon/live-folders.d.ts +48 -0
- package/dist/guest/daemon/live-folders.js +245 -0
- package/dist/guest/daemon/live.d.ts +4 -0
- package/dist/guest/daemon/live.js +57 -0
- package/dist/guest/daemon/sessions.d.ts +7 -0
- package/dist/guest/daemon/sessions.js +23 -2
- package/dist/guest/entries/daemon.js +5 -2
- package/dist/guest/runtime/csc.d.ts +1 -0
- package/dist/guest/runtime/csc.js +1 -0
- package/dist/guest/runtime/secrets.d.ts +5 -2
- package/dist/guest/runtime/secrets.js +12 -5
- package/dist/guest/sessions/pty-host.d.ts +10 -0
- package/dist/guest/sessions/pty-host.js +48 -3
- package/dist/host/channel/channel.js +1 -1
- package/dist/host/engines/hcs/live-folders.d.ts +117 -0
- package/dist/host/engines/hcs/live-folders.js +347 -0
- package/dist/host/engines/index.d.ts +1 -0
- package/dist/host/engines/index.js +1 -0
- package/dist/host/install/install-record.d.ts +1 -0
- package/dist/host/install/install-record.js +2 -0
- package/dist/host/launch/generate.js +8 -1
- package/dist/host/projects/join.d.ts +4 -0
- package/dist/host/projects/join.js +11 -3
- package/dist/host/projects/mapper.d.ts +34 -5
- package/dist/host/projects/mapper.js +88 -20
- package/dist/host/projects/registry.d.ts +2 -0
- package/dist/host/projects/registry.js +1 -1
- package/dist/host/sandbox/contained-deps.d.ts +2 -1
- package/dist/host/sandbox/contained-deps.js +6 -4
- package/dist/host/secrets/launch-secrets.js +2 -1
- package/dist/host/sharing/lifecycle.js +9 -1
- package/dist/host/storage/contained/contained-storage.js +13 -0
- package/dist/host/storage/contained/disk-port.d.ts +5 -0
- package/dist/host/storage/contained/helper-port.d.ts +72 -0
- package/dist/host/storage/contained/helper-port.js +162 -0
- package/dist/host/storage/contained/index.d.ts +1 -0
- package/dist/host/storage/contained/index.js +1 -0
- package/dist/host/web/bridge.d.ts +5 -0
- package/dist/host/web/bridge.js +11 -1
- package/dist/host/web/server.d.ts +2 -0
- package/dist/host/web/server.js +11 -1
- package/dist/runtime/agent.js +23 -0
- package/dist/runtime/daemon.js +523 -135
- package/dist/runtime/db.js +23 -0
- package/dist/runtime/main.js +221 -15
- package/dist/runtime/netproxy.js +23 -0
- package/dist/runtime/view.js +209 -12
- package/dist/selftest/channel.test.js +72 -1
- package/dist/selftest/contained.test.js +1 -37
- package/dist/selftest/hcs.test.js +5 -2
- package/dist/selftest/index.js +4 -0
- package/dist/selftest/install.test.js +11 -0
- package/dist/selftest/layout.test.js +1 -0
- package/dist/selftest/live-daemon.test.js +51 -1
- package/dist/selftest/live-folders.test.d.ts +2 -0
- package/dist/selftest/live-folders.test.js +588 -0
- package/dist/selftest/live-terminal.test.js +112 -4
- package/dist/selftest/projects.test.js +7 -7
- package/dist/selftest/storage-helper.test.d.ts +2 -0
- package/dist/selftest/storage-helper.test.js +493 -0
- package/dist/selftest/support/engine-golden.js +1 -0
- package/dist/selftest/support/fixtures.d.ts +1 -0
- package/dist/selftest/support/live.js +2 -0
- package/dist/selftest/support/vhdx.d.ts +14 -0
- package/dist/selftest/support/vhdx.js +72 -0
- package/dist/selftest/web.test.js +14 -2
- package/dist/shared/channel/index.d.ts +1 -0
- package/dist/shared/channel/index.js +1 -0
- package/dist/shared/channel/live.d.ts +53 -0
- package/dist/shared/channel/live.js +92 -0
- package/dist/shared/channel/run-dir.d.ts +11 -0
- package/dist/shared/channel/run-dir.js +20 -0
- package/dist/shared/channel/secrets.d.ts +1 -0
- package/dist/shared/channel/secrets.js +5 -0
- package/dist/shared/paths/projects.d.ts +6 -1
- package/dist/shared/paths/projects.js +21 -1
- package/dist/shared/pty/index.d.ts +3 -1
- package/dist/shared/pty/index.js +3 -1
- package/dist/shared/pty/key-gate.d.ts +24 -0
- package/dist/shared/pty/key-gate.js +45 -0
- package/dist/shared/pty/mouse-modes.d.ts +27 -0
- package/dist/shared/pty/mouse-modes.js +155 -0
- package/dist/shared/pty/terminal.d.ts +0 -5
- package/dist/shared/pty/terminal.js +51 -20
- package/dist/sync/watcher.js +10 -1
- package/dist/web/index.html +1 -1
- package/dist/web/{main-NBQ3LF4E.js → main-7DJZ2M27.js} +34 -34
- package/native/csharp/HcsHost/AssemblyInfo.cs +4 -4
- package/native/csharp/HcsHost/FolderAccess.cs +361 -0
- package/native/csharp/HcsHost/Hcs.cs +31 -3
- package/native/csharp/HcsHost/Program.cs +54 -6
- package/native/csharp/HcsHost/Share.cs +64 -11
- package/native/csharp/HcsHost/WsbGuard.cs +130 -0
- package/native/csharp/HcsHost/WsbShare.cs +295 -0
- package/native/csharp/HostTools/AssemblyInfo.cs +3 -3
- package/native/csharp/StorageHelper/AssemblyInfo.cs +15 -0
- package/native/csharp/StorageHelper/Client.cs +297 -0
- package/native/csharp/StorageHelper/Log.cs +177 -0
- package/native/csharp/StorageHelper/MountRecords.cs +160 -0
- package/native/csharp/StorageHelper/Operations.cs +596 -0
- package/native/csharp/StorageHelper/Pins.cs +270 -0
- package/native/csharp/StorageHelper/Privileges.cs +78 -0
- package/native/csharp/StorageHelper/Program.cs +349 -0
- package/native/csharp/StorageHelper/Protocol.cs +107 -0
- package/native/csharp/StorageHelper/RateLimiter.cs +97 -0
- package/native/csharp/StorageHelper/Rules.cs +156 -0
- package/native/csharp/StorageHelper/Server.cs +656 -0
- package/native/csharp/StorageHelper/Settings.cs +297 -0
- package/native/csharp/StorageHelper/StorageService.cs +53 -0
- package/native/csharp/StorageHelper/UserSettings.cs +206 -0
- package/native/csharp/StorageHelper/Vhdx.cs +230 -0
- package/native/csharp/StorageHelper/VirtualDisk.cs +241 -0
- package/native/csharp/StorageHelper/Volumes.cs +379 -0
- package/native/csharp/StorageHelper/Watchdog.cs +128 -0
- package/native/csharp/StorageHelper/Win32.cs +250 -0
- package/native/scripts/contained-common.ps1 +4 -0
- package/native/scripts/hcs-live-share.ps1 +191 -0
- package/native/scripts/set-storage-helper.ps1 +203 -0
- package/package.json +1 -1
- package/sandbox/csharp/BindMount.cs +120 -0
- package/sandbox/csharp/PtyHost.cs +236 -1
- package/schemas/config.schema.json +8 -0
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
// Windows Terminal's own pseudo console) and runs the real viewer/attach client (runTerminalClient), which is attached
|
|
3
3
|
// to an inner ptyhost running a stand-in agent. The agent turns SGR mouse tracking on and echoes every byte it reads.
|
|
4
4
|
// Checks: clicks and the wheel reach the agent, Backspace stays Backspace, a split escape sequence arrives whole,
|
|
5
|
-
// 500 printed lines scroll through as lines (no alternate screen, no scrollback clear),
|
|
6
|
-
// a screen clear still gets mouse tracking.
|
|
5
|
+
// 500 printed lines scroll through as lines (no alternate screen, no scrollback clear), a viewer that joins after
|
|
6
|
+
// a screen clear still gets mouse tracking, and drag and motion reports reach the agent only once it asked for them.
|
|
7
7
|
import fs from 'node:fs';
|
|
8
8
|
import net from 'node:net';
|
|
9
9
|
import path from 'node:path';
|
|
@@ -14,7 +14,7 @@ import { SYSTEM } from '../shared/system.js';
|
|
|
14
14
|
import { CSC, openViewer, tail, WAIT } from './support/live.js';
|
|
15
15
|
/**
|
|
16
16
|
* The stand-in agent: VT input and SGR mouse tracking on, echoes what it reads ("got <1b>[<0;10;5M"). L prints 500
|
|
17
|
-
* lines, K clears the screen, q quits.
|
|
17
|
+
* lines, K clears the screen, D turns drag reports on (1002), A every motion (1003), q quits.
|
|
18
18
|
*/
|
|
19
19
|
const AGENT_CS = String.raw `
|
|
20
20
|
using System;
|
|
@@ -44,6 +44,8 @@ static class MouseAgent
|
|
|
44
44
|
Say("got " + got + "\r\n");
|
|
45
45
|
if (got == "L") { var lines = new StringBuilder(); for (int i = 1; i <= 500; i++) lines.Append("line " + i + "\r\n"); Say(lines.ToString() + "LINES-DONE\r\n"); }
|
|
46
46
|
if (got == "K") Say("\x1b[2J\x1b[HCLEARED\r\n");
|
|
47
|
+
if (got == "D") Say("\x1b[?1002hDRAG-ON\r\n");
|
|
48
|
+
if (got == "A") Say("\x1b[?1003hMOVE-ON\r\n");
|
|
47
49
|
if (got == "q") break;
|
|
48
50
|
}
|
|
49
51
|
// Exits with mouse tracking still on (like a crash): the client must turn it off.
|
|
@@ -120,6 +122,75 @@ export const liveTerminalSuite = {
|
|
|
120
122
|
stdio: 'ignore',
|
|
121
123
|
});
|
|
122
124
|
}
|
|
125
|
+
t.section('a ptyhost ended from outside: what it recorded is ended, and nothing else');
|
|
126
|
+
{
|
|
127
|
+
// ptyhost records its program's process tree (PTYHOST_TREE_FILE); --end-tree ends it once ptyhost is gone,
|
|
128
|
+
// checking every process by id and start time.
|
|
129
|
+
const record = t.at('tree.json');
|
|
130
|
+
const ping = path.join(process.env.SystemRoot ?? 'C:\\Windows', 'System32', 'PING.EXE');
|
|
131
|
+
const holder = '"' + SYSTEM.cmd() + '" /c "' + ping + '" -n 120 127.0.0.1 >nul';
|
|
132
|
+
const host = spawn(ptyhost, [String(await freePort()), 'tok', '80', '24', t.at('tree.log'), holder], {
|
|
133
|
+
env: { ...process.env, PTYHOST_BIND: 'loopback', PTYHOST_TREE_FILE: record },
|
|
134
|
+
stdio: 'ignore',
|
|
135
|
+
windowsHide: true,
|
|
136
|
+
});
|
|
137
|
+
const read = (file) => {
|
|
138
|
+
try {
|
|
139
|
+
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
const alive = (pid) => {
|
|
146
|
+
try {
|
|
147
|
+
process.kill(pid, 0);
|
|
148
|
+
return true;
|
|
149
|
+
}
|
|
150
|
+
catch {
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
// The program (cmd) and the PING it started, once ptyhost has followed the tree (every 3 s).
|
|
155
|
+
const recorded = await t.waitFor(() => {
|
|
156
|
+
const r = read(record);
|
|
157
|
+
return r && r.ptyhost.pid === host.pid && r.processes.length >= 2 ? r : null;
|
|
158
|
+
}, WAIT.screen);
|
|
159
|
+
t.check(recorded !== null, 'ptyhost records its program and what it started', JSON.stringify(read(record)));
|
|
160
|
+
if (recorded) {
|
|
161
|
+
const endTree = (file) => {
|
|
162
|
+
const r = spawnSync(ptyhost, ['--end-tree', file], { encoding: 'utf8', windowsHide: true });
|
|
163
|
+
return { status: r.status, stdout: (r.stdout ?? '').trim() };
|
|
164
|
+
};
|
|
165
|
+
const copy = t.at('tree-copy.json');
|
|
166
|
+
fs.writeFileSync(copy, JSON.stringify(recorded));
|
|
167
|
+
const whileRunning = endTree(copy);
|
|
168
|
+
// The same ids with other start times: what a reused id looks like.
|
|
169
|
+
const forged = t.at('tree-forged.json');
|
|
170
|
+
fs.writeFileSync(forged, JSON.stringify({
|
|
171
|
+
ptyhost: { pid: recorded.ptyhost.pid, start: '1' },
|
|
172
|
+
processes: recorded.processes.map((p) => ({ pid: p.pid, start: String(BigInt(p.start) + 1n) })),
|
|
173
|
+
}));
|
|
174
|
+
const reused = endTree(forged);
|
|
175
|
+
t.check(whileRunning.status === 3 &&
|
|
176
|
+
reused.status === 0 &&
|
|
177
|
+
reused.stdout === 'ended 0 process(es)' &&
|
|
178
|
+
recorded.processes.every((p) => alive(p.pid)), 'nothing is ended while the ptyhost that recorded it runs, nor a process whose id was reused (start time)', JSON.stringify({ whileRunning, reused }));
|
|
179
|
+
host.kill();
|
|
180
|
+
await t.waitFor(() => host.exitCode !== null, 10000);
|
|
181
|
+
const left = fs.existsSync(record);
|
|
182
|
+
const ended = endTree(record);
|
|
183
|
+
await t.waitFor(() => recorded.processes.every((p) => !alive(p.pid)), 10000);
|
|
184
|
+
t.check(left && ended.status === 0 && recorded.processes.every((p) => !alive(p.pid)), 'a ptyhost ended from outside leaves its record, and --end-tree ends the processes in it', JSON.stringify({ left, ended, alive: recorded.processes.filter((p) => alive(p.pid)) }));
|
|
185
|
+
}
|
|
186
|
+
try {
|
|
187
|
+
if (host.exitCode === null)
|
|
188
|
+
host.kill();
|
|
189
|
+
}
|
|
190
|
+
catch {
|
|
191
|
+
/* it ended between the check and the kill: nothing left to stop */
|
|
192
|
+
}
|
|
193
|
+
}
|
|
123
194
|
t.section("the agents' job: a last-resort backstop, set once");
|
|
124
195
|
{
|
|
125
196
|
// A job of the test's own (PTYHOST_JOB_NAME), never the agents' real one.
|
|
@@ -289,9 +360,46 @@ export const liveTerminalSuite = {
|
|
|
289
360
|
t.check(head.includes('\x1b[?1000h') && head.includes('\x1b[?1006h'), "a viewer joining after a screen clear gets the agent's mouse tracking before the replay", 'late viewer: ' + JSON.stringify(late.output().slice(0, 300)));
|
|
290
361
|
late.close();
|
|
291
362
|
}
|
|
363
|
+
if (kind) {
|
|
364
|
+
// Raw mouse input (passed through, the client sees the agent's modes): a drag and a move are dropped while
|
|
365
|
+
// the agent tracks clicks only (1000), the drag passes with 1002, every move with 1003.
|
|
366
|
+
const drag = '\x1b[<32;11;5M';
|
|
367
|
+
const move = '\x1b[<35;12;6M';
|
|
368
|
+
const round = async (setup, ready, marker) => {
|
|
369
|
+
if (setup) {
|
|
370
|
+
send(setup);
|
|
371
|
+
await t.waitFor(() => screen().includes(ready), WAIT.screen);
|
|
372
|
+
}
|
|
373
|
+
const from = screen().length;
|
|
374
|
+
send(drag);
|
|
375
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
376
|
+
send(move);
|
|
377
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
378
|
+
send(marker);
|
|
379
|
+
await t.waitFor(() => screen()
|
|
380
|
+
.slice(from)
|
|
381
|
+
.includes('got ' + marker), WAIT.screen);
|
|
382
|
+
return screen().slice(from);
|
|
383
|
+
};
|
|
384
|
+
const clicksOnly = await round('', '', 'y');
|
|
385
|
+
const drags = await round('D', 'DRAG-ON', 'z');
|
|
386
|
+
const moves = await round('A', 'MOVE-ON', 'w');
|
|
387
|
+
// (Two reports read at once are echoed on one line.)
|
|
388
|
+
const has = (text, report) => text.includes('<1b>' + report.slice(1));
|
|
389
|
+
t.check(clicksOnly.includes('got y') &&
|
|
390
|
+
!has(clicksOnly, drag) &&
|
|
391
|
+
!has(clicksOnly, move) &&
|
|
392
|
+
drags.includes('got z') &&
|
|
393
|
+
has(drags, drag) &&
|
|
394
|
+
!has(drags, move) &&
|
|
395
|
+
has(moves, drag) &&
|
|
396
|
+
has(moves, move), 'drag and motion reports reach the agent only once it asked for them (1002 drags, 1003 every move)', JSON.stringify({ clicksOnly: tail(clicksOnly, 200), drags: tail(drags, 200), moves: tail(moves, 200) }));
|
|
397
|
+
}
|
|
292
398
|
send('q');
|
|
293
399
|
const ended = await t.waitFor(() => screen().includes('CLIENT-DONE'), WAIT.exit);
|
|
294
|
-
|
|
400
|
+
const last = screen().slice(screen().lastIndexOf('got q'));
|
|
401
|
+
t.check(Boolean(ended) && (!kind || (last.includes('\x1b[?1000l') && last.includes('\x1b[?1003l'))), 'the client ends with the agent' +
|
|
402
|
+
(kind ? ' and turns mouse tracking (clicks, drags, motion) off in the terminal again' : ''), 'screen: ' + tail(screen()));
|
|
295
403
|
}
|
|
296
404
|
finally {
|
|
297
405
|
for (const v of viewers)
|
|
@@ -23,9 +23,9 @@ import { severalAgentsArgv } from '../cli/menu/index.js';
|
|
|
23
23
|
import { generateSandbox } from '../host/launch/index.js';
|
|
24
24
|
import { describeSessions } from '../cli/commands/projects/index.js';
|
|
25
25
|
/** The error `fn` throws, or null. */
|
|
26
|
-
const caught = (fn) => {
|
|
26
|
+
const caught = async (fn) => {
|
|
27
27
|
try {
|
|
28
|
-
fn();
|
|
28
|
+
await fn();
|
|
29
29
|
return null;
|
|
30
30
|
}
|
|
31
31
|
catch (e) {
|
|
@@ -126,10 +126,10 @@ export const projectsSuite = {
|
|
|
126
126
|
new ProjectRegistry(storage).here(path.join(f.workA, 'src'))?.guestDir === 'C:\\Workspace\\work-a\\src' &&
|
|
127
127
|
new ProjectRegistry(storage).here(outside) === null, 'at launch the registry holds the launch folder (primary) and the projects mapped with it; a folder inside a project finds it, one in no project does not');
|
|
128
128
|
const noWsb = projectMapper({ storageRoot: storage, packageRoot: f.install, wsbShare: true, detect: () => null });
|
|
129
|
-
const web = noWsb.register(inRoot);
|
|
129
|
+
const web = await noWsb.register(inRoot);
|
|
130
130
|
t.check(web.mechanism === 'root' &&
|
|
131
131
|
web.guest === 'C:\\Projects\\proj-root\\web' &&
|
|
132
|
-
noWsb.register(inRoot).name === web.name &&
|
|
132
|
+
(await noWsb.register(inRoot)).name === web.name &&
|
|
133
133
|
new ProjectRegistry(storage).read()?.projects.filter((p) => p.host === inRoot).length === 1, 'a folder inside a root joins at once, and only once');
|
|
134
134
|
t.section('projects: a registry another copy wrote, lost or left from an earlier sandbox');
|
|
135
135
|
const lostStorage = t.at('proj-lost');
|
|
@@ -169,9 +169,9 @@ export const projectsSuite = {
|
|
|
169
169
|
t.check(runningRegistry(lostStorage, lostData, base)?.roots.length === 1, "a registry left from an earlier sandbox is replaced by the running one's");
|
|
170
170
|
lostStatus('1700000000002-def456');
|
|
171
171
|
t.check(runningRegistry(lostStorage, lostData, base) === null, 'no launch folder for the running sandbox: no registry (never an earlier sandbox one)');
|
|
172
|
-
const needs = caught(() => noWsb.register(outside));
|
|
173
|
-
const shareOff = caught(() => projectMapper({ storageRoot: storage, packageRoot: f.install, wsbShare: false }).register(outside));
|
|
174
|
-
const listFails = caught(() => projectMapper({
|
|
172
|
+
const needs = await caught(() => noWsb.register(outside));
|
|
173
|
+
const shareOff = await caught(() => projectMapper({ storageRoot: storage, packageRoot: f.install, wsbShare: false }).register(outside));
|
|
174
|
+
const listFails = await caught(() => projectMapper({
|
|
175
175
|
storageRoot: storage,
|
|
176
176
|
packageRoot: f.install,
|
|
177
177
|
wsbShare: true,
|