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
package/dist/bin/claude-host.js
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
import { createRequire as __createRequire } from 'node:module'; const require = __createRequire(import.meta.url);
|
|
3
3
|
import {
|
|
4
4
|
run
|
|
5
|
-
} from "./chunk-
|
|
6
|
-
import "./chunk-
|
|
5
|
+
} from "./chunk-HTQT5SBY.js";
|
|
6
|
+
import "./chunk-D7CM63BS.js";
|
|
7
7
|
|
|
8
8
|
// src/bin/claude-host.ts
|
|
9
9
|
var args = process.argv.slice(2);
|
package/dist/bin/claude-sbx.js
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
import { createRequire as __createRequire } from 'node:module'; const require = __createRequire(import.meta.url);
|
|
3
3
|
import {
|
|
4
4
|
run
|
|
5
|
-
} from "./chunk-
|
|
6
|
-
import "./chunk-
|
|
5
|
+
} from "./chunk-HTQT5SBY.js";
|
|
6
|
+
import "./chunk-D7CM63BS.js";
|
|
7
7
|
|
|
8
8
|
// src/bin/claude-sbx.ts
|
|
9
9
|
run(process.argv.slice(2), { harness: "claude" });
|
package/dist/bin/codex-sbx.js
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
import { createRequire as __createRequire } from 'node:module'; const require = __createRequire(import.meta.url);
|
|
3
3
|
import {
|
|
4
4
|
run
|
|
5
|
-
} from "./chunk-
|
|
6
|
-
import "./chunk-
|
|
5
|
+
} from "./chunk-HTQT5SBY.js";
|
|
6
|
+
import "./chunk-D7CM63BS.js";
|
|
7
7
|
|
|
8
8
|
// src/bin/codex-sbx.ts
|
|
9
9
|
run(process.argv.slice(2), { harness: "codex" });
|
package/dist/bin/gemini-sbx.js
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
import { createRequire as __createRequire } from 'node:module'; const require = __createRequire(import.meta.url);
|
|
3
3
|
import {
|
|
4
4
|
run
|
|
5
|
-
} from "./chunk-
|
|
6
|
-
import "./chunk-
|
|
5
|
+
} from "./chunk-HTQT5SBY.js";
|
|
6
|
+
import "./chunk-D7CM63BS.js";
|
|
7
7
|
|
|
8
8
|
// src/bin/gemini-sbx.ts
|
|
9
9
|
run(process.argv.slice(2), { harness: "gemini" });
|
package/dist/bin/sandbox.js
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
import { createRequire as __createRequire } from 'node:module'; const require = __createRequire(import.meta.url);
|
|
3
3
|
import {
|
|
4
4
|
run
|
|
5
|
-
} from "./chunk-
|
|
6
|
-
import "./chunk-
|
|
5
|
+
} from "./chunk-HTQT5SBY.js";
|
|
6
|
+
import "./chunk-D7CM63BS.js";
|
|
7
7
|
|
|
8
8
|
// src/bin/sandbox.ts
|
|
9
9
|
run(process.argv.slice(2));
|
package/dist/bin/sbx-hide.js
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
import { createRequire as __createRequire } from 'node:module'; const require = __createRequire(import.meta.url);
|
|
3
3
|
import {
|
|
4
4
|
run
|
|
5
|
-
} from "./chunk-
|
|
6
|
-
import "./chunk-
|
|
5
|
+
} from "./chunk-HTQT5SBY.js";
|
|
6
|
+
import "./chunk-D7CM63BS.js";
|
|
7
7
|
|
|
8
8
|
// src/bin/sbx-hide.ts
|
|
9
9
|
run(process.argv.slice(2), { command: "hide" });
|
package/dist/bin/sbx-show.js
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
import { createRequire as __createRequire } from 'node:module'; const require = __createRequire(import.meta.url);
|
|
3
3
|
import {
|
|
4
4
|
run
|
|
5
|
-
} from "./chunk-
|
|
6
|
-
import "./chunk-
|
|
5
|
+
} from "./chunk-HTQT5SBY.js";
|
|
6
|
+
import "./chunk-D7CM63BS.js";
|
|
7
7
|
|
|
8
8
|
// src/bin/sbx-show.ts
|
|
9
9
|
run(process.argv.slice(2), { command: "show" });
|
|
@@ -130,7 +130,7 @@ class Launcher {
|
|
|
130
130
|
const engine = await openEngine(config);
|
|
131
131
|
const provider = providerFor(config, config.storage.contained === 'off'
|
|
132
132
|
? {}
|
|
133
|
-
: { contained: realContainedDeps(ctx.storageRoot, config.storage.containedMaxGb) });
|
|
133
|
+
: { contained: realContainedDeps(ctx.storageRoot, config.storage.containedMaxGb, Boolean(ctx.options.config)) });
|
|
134
134
|
if (!generateOnly)
|
|
135
135
|
assertCanLaunch(isSandboxRunning() ? runningOwner() : undefined);
|
|
136
136
|
// Contained storage: the image is mounted now (before anything reads or writes the data set), and let go of again
|
|
@@ -77,6 +77,8 @@ export async function joinHere(ctx, status, o = {}) {
|
|
|
77
77
|
packageRoot,
|
|
78
78
|
hostDir: dir,
|
|
79
79
|
wsbShare: loaded.config.projects.wsbShare,
|
|
80
|
+
liveFolders: loaded.config.projects.liveFolders,
|
|
81
|
+
onNote: (note) => console.log(c.yellow(note)),
|
|
80
82
|
resolve: (root) => resolveRun({
|
|
81
83
|
packageRoot,
|
|
82
84
|
loaded,
|
|
@@ -10,7 +10,7 @@ import { containedAvailability, ContainedStorage, containedPaths } from '../../.
|
|
|
10
10
|
import { ok } from './common.js';
|
|
11
11
|
function storageOf(env) {
|
|
12
12
|
const { config } = env.ctx.loaded;
|
|
13
|
-
return new ContainedStorage(realContainedDeps(env.storageRoot, config.storage.containedMaxGb), config.storage.dataName);
|
|
13
|
+
return new ContainedStorage(realContainedDeps(env.storageRoot, config.storage.containedMaxGb, Boolean(env.ctx.options.config)), config.storage.dataName);
|
|
14
14
|
}
|
|
15
15
|
/** Only when contained storage is on, or an image or a record of one exists. */
|
|
16
16
|
const inUse = (env) => {
|
|
@@ -4,7 +4,8 @@ import { spawnSync } from 'node:child_process';
|
|
|
4
4
|
import { expandPath, fail, isWin } from '../../core/util/index.js';
|
|
5
5
|
import { detectWsb, doctorRows, insideSandbox, isSandboxRunning } from '../../host/sandbox/index.js';
|
|
6
6
|
import { machineDoctorRows } from '../../host/sharing/index.js';
|
|
7
|
-
import { KnownProjects, ProjectRegistry } from '../../host/projects/index.js';
|
|
7
|
+
import { addFoldersRow, KnownProjects, ProjectRegistry } from '../../host/projects/index.js';
|
|
8
|
+
import { realLiveFolders } from '../../host/engines/index.js';
|
|
8
9
|
import { nativeScript } from '../../host/native.js';
|
|
9
10
|
import { refuseInSelfTest } from '../../host/test-guard.js';
|
|
10
11
|
import { SYSTEM } from '../../shared/system.js';
|
|
@@ -29,7 +30,9 @@ export class HealthService {
|
|
|
29
30
|
// Several projects: the roots mapped at every start, what the running sandbox has, and whether wsb share is there.
|
|
30
31
|
const p = ctx.loaded.config.projects;
|
|
31
32
|
const registry = isSandboxRunning() ? new ProjectRegistry(ctx.storageRoot).read() : null;
|
|
32
|
-
const wsb = p.wsbShare ? detectWsb() : null;
|
|
33
|
+
const wsb = p.wsbShare || p.liveFolders === 'hcs' ? detectWsb() : null;
|
|
34
|
+
const launchId = registry?.launchId ?? null;
|
|
35
|
+
const liveUnavailable = p.liveFolders === 'hcs' && launchId ? realLiveFolders(ctx.storageRoot, ctx.dataPath).unavailable(launchId) : null;
|
|
33
36
|
return [
|
|
34
37
|
...rows,
|
|
35
38
|
...machineDoctorRows(ctx.packageRoot, ctx.dataPath),
|
|
@@ -59,7 +62,16 @@ export class HealthService {
|
|
|
59
62
|
wsb.exe +
|
|
60
63
|
(wsb.version ? ' ' + wsb.version : '') +
|
|
61
64
|
' (untested; a failure offers a restart instead)'
|
|
62
|
-
: 'not on this Windows (needs Windows 11 24H2+ with the updated Windows Sandbox)
|
|
65
|
+
: 'not on this Windows (needs Windows 11 24H2+ with the updated Windows Sandbox)',
|
|
66
|
+
],
|
|
67
|
+
[
|
|
68
|
+
'Add folders while the sandbox runs',
|
|
69
|
+
addFoldersRow({
|
|
70
|
+
wsbShare: p.wsbShare,
|
|
71
|
+
liveFolders: p.liveFolders,
|
|
72
|
+
wsbHasShare: wsb?.has('share') === true,
|
|
73
|
+
liveUnavailable,
|
|
74
|
+
}),
|
|
63
75
|
],
|
|
64
76
|
];
|
|
65
77
|
}
|
|
@@ -26,11 +26,16 @@ export declare class ProjectsService {
|
|
|
26
26
|
list(): ProjectsListing;
|
|
27
27
|
/** Remembers a project folder (it must pass the same checks as a workspace). Returns the folder. */
|
|
28
28
|
add(folder: string): string;
|
|
29
|
-
/**
|
|
30
|
-
|
|
29
|
+
/**
|
|
30
|
+
* Forgets a project, in the running sandbox too (a mapped folder stays visible until the sandbox stops; a live folder
|
|
31
|
+
* is hidden and its share removed, with one permission prompt).
|
|
32
|
+
*/
|
|
33
|
+
forget(folder: string): Promise<{
|
|
31
34
|
dir: string;
|
|
32
35
|
removed: boolean;
|
|
33
|
-
}
|
|
36
|
+
}>;
|
|
37
|
+
/** The folder is a live folder (HCS) of the running sandbox: forgetting it removes its share (one prompt). */
|
|
38
|
+
isLive(folder: string): boolean;
|
|
34
39
|
/** Excludes a folder from the projects mapped at every start. True when that changed anything. */
|
|
35
40
|
exclude(folder: string): boolean;
|
|
36
41
|
/** Includes a folder again (and remembers it). True when it was excluded before. */
|
|
@@ -51,20 +51,37 @@ export class ProjectsService {
|
|
|
51
51
|
new KnownProjects(this.ctx.storageRoot).remember(dir);
|
|
52
52
|
return dir;
|
|
53
53
|
}
|
|
54
|
-
/**
|
|
55
|
-
|
|
54
|
+
/**
|
|
55
|
+
* Forgets a project, in the running sandbox too (a mapped folder stays visible until the sandbox stops; a live folder
|
|
56
|
+
* is hidden and its share removed, with one permission prompt).
|
|
57
|
+
*/
|
|
58
|
+
async forget(folder) {
|
|
56
59
|
const { ctx } = this;
|
|
57
60
|
const dir = path.resolve(folder);
|
|
58
|
-
const forgot = new KnownProjects(ctx.storageRoot).forget(dir);
|
|
59
61
|
const registered = isSandboxRunning()
|
|
60
62
|
? new ProjectRegistry(ctx.storageRoot)
|
|
61
63
|
.read()
|
|
62
64
|
?.projects.find((p) => !p.primary && p.host.toLowerCase() === dir.toLowerCase())
|
|
63
65
|
: undefined;
|
|
66
|
+
// The live folder first: when it cannot be removed, the project stays (in the list and in the sandbox).
|
|
64
67
|
if (registered)
|
|
65
|
-
projectMapper({
|
|
68
|
+
await projectMapper({
|
|
69
|
+
storageRoot: ctx.storageRoot,
|
|
70
|
+
packageRoot: ctx.packageRoot,
|
|
71
|
+
wsbShare: false,
|
|
72
|
+
dataPath: ctx.dataPath,
|
|
73
|
+
}).unregister(registered.name);
|
|
74
|
+
const forgot = new KnownProjects(ctx.storageRoot).forget(dir);
|
|
66
75
|
return { dir, removed: forgot || Boolean(registered) };
|
|
67
76
|
}
|
|
77
|
+
/** The folder is a live folder (HCS) of the running sandbox: forgetting it removes its share (one prompt). */
|
|
78
|
+
isLive(folder) {
|
|
79
|
+
const dir = path.resolve(folder);
|
|
80
|
+
return (isSandboxRunning() &&
|
|
81
|
+
new ProjectRegistry(this.ctx.storageRoot)
|
|
82
|
+
.read()
|
|
83
|
+
?.projects.some((p) => p.mechanism === 'hcs' && p.host.toLowerCase() === dir.toLowerCase()) === true);
|
|
84
|
+
}
|
|
68
85
|
/** Excludes a folder from the projects mapped at every start. True when that changed anything. */
|
|
69
86
|
exclude(folder) {
|
|
70
87
|
return addToList(this.ctx, ['projects', 'exclude'], path.resolve(folder));
|
|
@@ -356,6 +356,10 @@ export declare const ConfigSchema: z.ZodObject<{
|
|
|
356
356
|
declinedRoots: z.ZodArray<z.ZodString>;
|
|
357
357
|
allowOutsideMappings: z.ZodUnion<readonly [z.ZodLiteral<"ask">, z.ZodBoolean]>;
|
|
358
358
|
wsbShare: z.ZodBoolean;
|
|
359
|
+
liveFolders: z.ZodEnum<{
|
|
360
|
+
hcs: "hcs";
|
|
361
|
+
off: "off";
|
|
362
|
+
}>;
|
|
359
363
|
}, z.core.$strict>;
|
|
360
364
|
resume: z.ZodObject<{
|
|
361
365
|
sessions: z.ZodEnum<{
|
|
@@ -7,4 +7,8 @@ export declare const ProjectsSchema: z.ZodObject<{
|
|
|
7
7
|
declinedRoots: z.ZodArray<z.ZodString>;
|
|
8
8
|
allowOutsideMappings: z.ZodUnion<readonly [z.ZodLiteral<"ask">, z.ZodBoolean]>;
|
|
9
9
|
wsbShare: z.ZodBoolean;
|
|
10
|
+
liveFolders: z.ZodEnum<{
|
|
11
|
+
hcs: "hcs";
|
|
12
|
+
off: "off";
|
|
13
|
+
}>;
|
|
10
14
|
}, z.core.$strict>;
|
|
@@ -19,4 +19,7 @@ export const ProjectsSchema = z.strictObject({
|
|
|
19
19
|
wsbShare: z
|
|
20
20
|
.boolean()
|
|
21
21
|
.describe('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.'),
|
|
22
|
+
liveFolders: z
|
|
23
|
+
.enum(['off', 'hcs'])
|
|
24
|
+
.describe('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").'),
|
|
22
25
|
});
|
package/dist/guest/boot/boot.js
CHANGED
|
@@ -73,8 +73,14 @@ function startDaemon(env, secrets) {
|
|
|
73
73
|
...env,
|
|
74
74
|
AGENT_LOG_PREFIX: 'daemon',
|
|
75
75
|
// The agent's environment (it may hold API keys): the daemon moves it into memory and removes this at once.
|
|
76
|
-
|
|
77
|
-
|
|
76
|
+
// The live-folder key (projects.liveFolders "hcs") goes the same way into the daemon's own state.
|
|
77
|
+
...(secrets.harnessEnvironment || secrets.liveFoldersKey
|
|
78
|
+
? {
|
|
79
|
+
[BOOT_SECRETS_VARIABLE]: JSON.stringify({
|
|
80
|
+
...(secrets.harnessEnvironment ? { harnessEnvironment: secrets.harnessEnvironment } : {}),
|
|
81
|
+
...(secrets.liveFoldersKey ? { liveFoldersKey: secrets.liveFoldersKey } : {}),
|
|
82
|
+
}),
|
|
83
|
+
}
|
|
78
84
|
: {}),
|
|
79
85
|
},
|
|
80
86
|
}).unref();
|
|
@@ -8,6 +8,7 @@ import { governor } from '../governor/index.js';
|
|
|
8
8
|
import { respond, safely } from './state.js';
|
|
9
9
|
import { run } from '../runtime/paths.js';
|
|
10
10
|
import { withRequestSecrets } from '../runtime/secrets.js';
|
|
11
|
+
import { liveFolders } from './live.js';
|
|
11
12
|
import { warn } from '../runtime/log.js';
|
|
12
13
|
import { errMsg } from '../../core/util/errors.js';
|
|
13
14
|
const POLL_MS = 400;
|
|
@@ -38,6 +39,8 @@ export class Daemon {
|
|
|
38
39
|
// Not awaited: starting a session or running a command must not hold up other requests.
|
|
39
40
|
void Promise.resolve(this.handle(withRequestSecrets(req))).catch((e) => respond(req.id, { ok: false, message: errMsg(e) }));
|
|
40
41
|
}
|
|
42
|
+
// Live folders (signed host requests in run\live): claimed here, handled one after another.
|
|
43
|
+
safely('live folders', () => liveFolders.tick());
|
|
41
44
|
if (Date.now() - this.lastMemSave > MEM_SAVE_MS) {
|
|
42
45
|
this.lastMemSave = Date.now();
|
|
43
46
|
safely('claude-mem save', saveClaudeMem);
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { RunDir } from '../../shared/channel/run-dir.js';
|
|
2
|
+
/** The bind filter, behind an interface so the self-test can run the handler without one. */
|
|
3
|
+
export interface BindPort {
|
|
4
|
+
/** BindMount map: 0 done, 3 the share is not there (yet), anything else failed. */
|
|
5
|
+
map(folder: string, share: string, readOnly: boolean): number;
|
|
6
|
+
unmap(folder: string): number;
|
|
7
|
+
/** BindMount mapped: 0 mapped, 4 not mapped, anything else unknown. */
|
|
8
|
+
mapped(folder: string): number;
|
|
9
|
+
}
|
|
10
|
+
export interface LiveFolderOptions {
|
|
11
|
+
readonly run: RunDir;
|
|
12
|
+
/** The daemon's own state file on the sandbox's disk (made administrator-only by the caller). */
|
|
13
|
+
readonly stateFile: string;
|
|
14
|
+
readonly launchId: string;
|
|
15
|
+
readonly bind: BindPort;
|
|
16
|
+
readonly workspaces?: string;
|
|
17
|
+
readonly now?: () => number;
|
|
18
|
+
readonly sleep?: (ms: number) => Promise<void>;
|
|
19
|
+
/** How often a missing share is tried again (0.5 s apart). */
|
|
20
|
+
readonly tries?: number;
|
|
21
|
+
readonly log?: (line: string) => void;
|
|
22
|
+
}
|
|
23
|
+
export declare class LiveFolderHandler {
|
|
24
|
+
private readonly o;
|
|
25
|
+
private queue;
|
|
26
|
+
constructor(o: LiveFolderOptions);
|
|
27
|
+
private now;
|
|
28
|
+
private log;
|
|
29
|
+
private read;
|
|
30
|
+
private save;
|
|
31
|
+
/** The key the boot handed over (from the one-shot secrets file). */
|
|
32
|
+
setKey(key: string): void;
|
|
33
|
+
/** C:\Workspace\<name> from a checked name only. */
|
|
34
|
+
private folderPath;
|
|
35
|
+
/** One pass over run\live: every file is claimed (deleted) at once; valid ones are handled one after another. */
|
|
36
|
+
tick(): void;
|
|
37
|
+
/** Waits for what tick() queued (tests). */
|
|
38
|
+
idle(): Promise<void>;
|
|
39
|
+
handle(envelope: unknown): Promise<void>;
|
|
40
|
+
private reply;
|
|
41
|
+
/** map with retries while the share is not there yet. */
|
|
42
|
+
private mapWithRetry;
|
|
43
|
+
/** The folder must not exist yet: not as a folder, a file or a link. */
|
|
44
|
+
private assertFree;
|
|
45
|
+
private act;
|
|
46
|
+
/** When the daemon starts: the recorded mappings this launch made, made again where they are gone. */
|
|
47
|
+
reapply(): number;
|
|
48
|
+
}
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
// Live folders, the daemon's half (opt-in alpha, docs/design/hcs-live-folders.md): the host shares a folder into the
|
|
2
|
+
// running sandbox (vSMB), then asks here, with a request signed by this launch's key (shared/channel/live.ts), to show
|
|
3
|
+
// it at C:\Workspace\<name> through the bind filter (sandbox\csharp\BindMount.cs). A canary request does the same for a
|
|
4
|
+
// test share, reads the token in it and hides it again: that proves to the host that the share reached THIS sandbox.
|
|
5
|
+
// Every file in run\live is read at most 16 KB, links and bad names are deleted unread, and a request is acted on only
|
|
6
|
+
// with a valid signature, this launch's id, a fresh time and an id not seen before (the seen ids are kept on the
|
|
7
|
+
// sandbox's own disk, so restarting the daemon does not reopen them). Guest paths come only from the strict folder
|
|
8
|
+
// name: never a path from the request. The mappings made are recorded and made again when the daemon starts.
|
|
9
|
+
import fs from 'node:fs';
|
|
10
|
+
import path from 'node:path';
|
|
11
|
+
import { z } from 'zod';
|
|
12
|
+
import { LIVE_FOLDER_NAME, LIVE_KEY, LIVE_SHARE_NAME, LIVE_TOKEN, openLive } from '../../shared/channel/live.js';
|
|
13
|
+
import { writeJsonAtomic } from '../../shared/io/json.js';
|
|
14
|
+
import { WORKSPACES_GUEST } from '../../shared/paths/projects.js';
|
|
15
|
+
import { errMsg } from '../../core/util/errors.js';
|
|
16
|
+
const MAX_REQUEST_BYTES = 16 * 1024;
|
|
17
|
+
const MAX_SEEN = 512;
|
|
18
|
+
/** BindMount's exit codes. */
|
|
19
|
+
const EXIT_OK = 0;
|
|
20
|
+
const EXIT_NO_SHARE = 3;
|
|
21
|
+
const EXIT_NOT_MAPPED = 4;
|
|
22
|
+
const StateSchema = z.object({
|
|
23
|
+
key: z.string().regex(LIVE_KEY).nullable(),
|
|
24
|
+
launchId: z.string().max(64),
|
|
25
|
+
seen: z.array(z.object({ id: z.string().max(64), at: z.number() })).max(MAX_SEEN),
|
|
26
|
+
mappings: z
|
|
27
|
+
.array(z.object({
|
|
28
|
+
folder: z.string().regex(LIVE_FOLDER_NAME),
|
|
29
|
+
share: z.string().regex(LIVE_SHARE_NAME),
|
|
30
|
+
readOnly: z.boolean(),
|
|
31
|
+
}))
|
|
32
|
+
.max(256),
|
|
33
|
+
});
|
|
34
|
+
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
35
|
+
export class LiveFolderHandler {
|
|
36
|
+
o;
|
|
37
|
+
queue = Promise.resolve();
|
|
38
|
+
constructor(o) {
|
|
39
|
+
this.o = o;
|
|
40
|
+
}
|
|
41
|
+
now() {
|
|
42
|
+
return (this.o.now ?? Date.now)();
|
|
43
|
+
}
|
|
44
|
+
log(line) {
|
|
45
|
+
this.o.log?.(line);
|
|
46
|
+
}
|
|
47
|
+
read() {
|
|
48
|
+
try {
|
|
49
|
+
const text = fs.readFileSync(this.o.stateFile, 'utf8');
|
|
50
|
+
const parsed = StateSchema.safeParse(JSON.parse(text));
|
|
51
|
+
if (parsed.success && parsed.data.launchId === this.o.launchId)
|
|
52
|
+
return parsed.data;
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
// No state yet (or unreadable): a fresh one for this launch.
|
|
56
|
+
}
|
|
57
|
+
return { key: null, launchId: this.o.launchId, seen: [], mappings: [] };
|
|
58
|
+
}
|
|
59
|
+
save(state) {
|
|
60
|
+
fs.mkdirSync(path.dirname(this.o.stateFile), { recursive: true });
|
|
61
|
+
writeJsonAtomic(this.o.stateFile, state);
|
|
62
|
+
}
|
|
63
|
+
/** The key the boot handed over (from the one-shot secrets file). */
|
|
64
|
+
setKey(key) {
|
|
65
|
+
if (!LIVE_KEY.test(key))
|
|
66
|
+
return;
|
|
67
|
+
this.save({ ...this.read(), key });
|
|
68
|
+
}
|
|
69
|
+
/** C:\Workspace\<name> from a checked name only. */
|
|
70
|
+
folderPath(name) {
|
|
71
|
+
if (!LIVE_FOLDER_NAME.test(name))
|
|
72
|
+
throw new Error('not a plain folder name');
|
|
73
|
+
return path.win32.join(this.o.workspaces ?? WORKSPACES_GUEST, name);
|
|
74
|
+
}
|
|
75
|
+
/** One pass over run\live: every file is claimed (deleted) at once; valid ones are handled one after another. */
|
|
76
|
+
tick() {
|
|
77
|
+
const dir = this.o.run.liveDir;
|
|
78
|
+
let names;
|
|
79
|
+
try {
|
|
80
|
+
names = fs.readdirSync(dir);
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
return; // no requests yet
|
|
84
|
+
}
|
|
85
|
+
for (const name of names.slice(0, 64)) {
|
|
86
|
+
if (name.endsWith('.tmp'))
|
|
87
|
+
continue;
|
|
88
|
+
const file = path.join(dir, name);
|
|
89
|
+
let envelope = null;
|
|
90
|
+
try {
|
|
91
|
+
const stat = fs.lstatSync(file);
|
|
92
|
+
if (stat.isFile() && stat.size <= MAX_REQUEST_BYTES && /^[A-Za-z0-9_-]{1,64}\.json$/.test(name))
|
|
93
|
+
envelope = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
envelope = null;
|
|
97
|
+
}
|
|
98
|
+
finally {
|
|
99
|
+
try {
|
|
100
|
+
fs.rmSync(file, { recursive: true, force: true });
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
// Locked for a moment: the next pass deletes it (the id is then seen already).
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
if (envelope === null)
|
|
107
|
+
continue;
|
|
108
|
+
this.queue = this.queue
|
|
109
|
+
.then(() => this.handle(envelope))
|
|
110
|
+
.catch((e) => this.log('live folder: ' + errMsg(e)));
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
/** Waits for what tick() queued (tests). */
|
|
114
|
+
idle() {
|
|
115
|
+
return this.queue;
|
|
116
|
+
}
|
|
117
|
+
async handle(envelope) {
|
|
118
|
+
const state = this.read();
|
|
119
|
+
const now = this.now();
|
|
120
|
+
const opened = openLive(state.key, envelope, { launchId: this.o.launchId, now });
|
|
121
|
+
if ('refused' in opened) {
|
|
122
|
+
this.log('live folder request ignored: ' + opened.refused);
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
const p = opened.payload;
|
|
126
|
+
const seen = state.seen.filter((s) => now - s.at < 10 * 60 * 1000);
|
|
127
|
+
if (seen.some((s) => s.id === p.id)) {
|
|
128
|
+
this.log('live folder request ignored: seen before (' + p.id + ')');
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
this.save({ ...state, seen: [...seen, { id: p.id, at: now }].slice(-MAX_SEEN) });
|
|
132
|
+
let reply;
|
|
133
|
+
try {
|
|
134
|
+
reply = await this.act(p);
|
|
135
|
+
}
|
|
136
|
+
catch (e) {
|
|
137
|
+
reply = { ok: false, message: errMsg(e).slice(0, 900) };
|
|
138
|
+
}
|
|
139
|
+
this.reply(p.id, reply);
|
|
140
|
+
}
|
|
141
|
+
reply(id, r) {
|
|
142
|
+
try {
|
|
143
|
+
fs.mkdirSync(this.o.run.liveRepliesDir, { recursive: true });
|
|
144
|
+
writeJsonAtomic(this.o.run.liveReplyFile(id), { id, ...r, finishedAt: new Date(this.now()).toISOString() });
|
|
145
|
+
}
|
|
146
|
+
catch (e) {
|
|
147
|
+
this.log('live folder reply not written: ' + errMsg(e));
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
/** map with retries while the share is not there yet. */
|
|
151
|
+
async mapWithRetry(folder, share, readOnly) {
|
|
152
|
+
const tries = this.o.tries ?? 20;
|
|
153
|
+
let code = EXIT_NO_SHARE;
|
|
154
|
+
for (let i = 0; i < tries; i++) {
|
|
155
|
+
code = this.o.bind.map(folder, share, readOnly);
|
|
156
|
+
if (code !== EXIT_NO_SHARE)
|
|
157
|
+
return code;
|
|
158
|
+
await (this.o.sleep ?? wait)(500);
|
|
159
|
+
}
|
|
160
|
+
return code;
|
|
161
|
+
}
|
|
162
|
+
/** The folder must not exist yet: not as a folder, a file or a link. */
|
|
163
|
+
assertFree(folder) {
|
|
164
|
+
let exists = true;
|
|
165
|
+
try {
|
|
166
|
+
fs.lstatSync(folder);
|
|
167
|
+
}
|
|
168
|
+
catch {
|
|
169
|
+
exists = false;
|
|
170
|
+
}
|
|
171
|
+
if (exists)
|
|
172
|
+
throw new Error(folder + ' exists already');
|
|
173
|
+
// The bind filter needs the parent (C:\Workspace) to exist: without it BfSetupFilter answers "not found", exactly
|
|
174
|
+
// like a share that has not arrived (seen in a Windows Sandbox, 2026-10-23). It must be a plain folder.
|
|
175
|
+
const parent = path.dirname(folder);
|
|
176
|
+
fs.mkdirSync(parent, { recursive: true });
|
|
177
|
+
if (!fs.lstatSync(parent).isDirectory())
|
|
178
|
+
throw new Error(parent + ' is not a plain folder');
|
|
179
|
+
}
|
|
180
|
+
async act(p) {
|
|
181
|
+
const folder = this.folderPath(p.folder);
|
|
182
|
+
if (p.kind === 'unmap') {
|
|
183
|
+
const state = this.read();
|
|
184
|
+
const known = state.mappings.find((m) => m.folder === p.folder);
|
|
185
|
+
if (!known)
|
|
186
|
+
return { ok: false, message: 'no live folder at ' + folder };
|
|
187
|
+
const code = this.o.bind.unmap(folder);
|
|
188
|
+
if (code !== EXIT_OK || this.o.bind.mapped(folder) !== EXIT_NOT_MAPPED)
|
|
189
|
+
return { ok: false, message: 'the folder could not be hidden again (are files in it open?)' };
|
|
190
|
+
const after = this.read();
|
|
191
|
+
this.save({ ...after, mappings: after.mappings.filter((m) => m.folder !== p.folder) });
|
|
192
|
+
return { ok: true, message: 'hidden: ' + folder };
|
|
193
|
+
}
|
|
194
|
+
const share = p.share ?? '';
|
|
195
|
+
this.assertFree(folder);
|
|
196
|
+
if (p.kind === 'canary') {
|
|
197
|
+
const code = await this.mapWithRetry(folder, share, true);
|
|
198
|
+
if (code !== EXIT_OK)
|
|
199
|
+
return { ok: false, message: 'the test share could not be shown (' + code + ')' };
|
|
200
|
+
try {
|
|
201
|
+
const file = path.join(folder, 'token.txt');
|
|
202
|
+
const stat = fs.statSync(file);
|
|
203
|
+
const token = stat.size <= 64 ? fs.readFileSync(file, 'utf8').trim() : '';
|
|
204
|
+
return LIVE_TOKEN.test(token)
|
|
205
|
+
? { ok: true, message: 'read the test share', token }
|
|
206
|
+
: { ok: false, message: 'the test share holds no token' };
|
|
207
|
+
}
|
|
208
|
+
finally {
|
|
209
|
+
this.o.bind.unmap(folder);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
const code = await this.mapWithRetry(folder, share, p.readOnly);
|
|
213
|
+
if (code !== EXIT_OK)
|
|
214
|
+
return {
|
|
215
|
+
ok: false,
|
|
216
|
+
message: code === EXIT_NO_SHARE
|
|
217
|
+
? 'the share did not reach the sandbox'
|
|
218
|
+
: 'the folder could not be shown (' + code + ')',
|
|
219
|
+
};
|
|
220
|
+
const state = this.read();
|
|
221
|
+
this.save({
|
|
222
|
+
...state,
|
|
223
|
+
mappings: [
|
|
224
|
+
...state.mappings.filter((m) => m.folder !== p.folder),
|
|
225
|
+
{ folder: p.folder, share, readOnly: p.readOnly },
|
|
226
|
+
],
|
|
227
|
+
});
|
|
228
|
+
return { ok: true, message: 'shown at ' + folder };
|
|
229
|
+
}
|
|
230
|
+
/** When the daemon starts: the recorded mappings this launch made, made again where they are gone. */
|
|
231
|
+
reapply() {
|
|
232
|
+
let done = 0;
|
|
233
|
+
for (const m of this.read().mappings) {
|
|
234
|
+
const folder = this.folderPath(m.folder);
|
|
235
|
+
if (this.o.bind.mapped(folder) === EXIT_OK)
|
|
236
|
+
continue;
|
|
237
|
+
fs.mkdirSync(path.dirname(folder), { recursive: true });
|
|
238
|
+
if (this.o.bind.map(folder, m.share, m.readOnly) === EXIT_OK)
|
|
239
|
+
done++;
|
|
240
|
+
else
|
|
241
|
+
this.log('live folder ' + folder + ' could not be shown again');
|
|
242
|
+
}
|
|
243
|
+
return done;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { LiveFolderHandler } from './live-folders.js';
|
|
2
|
+
export declare const liveFolders: LiveFolderHandler;
|
|
3
|
+
/** At daemon start: the folder protected, the boot's key taken in, the recorded mappings made again. */
|
|
4
|
+
export declare function startLiveFolders(key: string | undefined): void;
|