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
|
@@ -10,6 +10,7 @@ declare const RegistryEntrySchema: z.ZodObject<{
|
|
|
10
10
|
guest: z.ZodString;
|
|
11
11
|
primary: z.ZodBoolean;
|
|
12
12
|
mechanism: z.ZodCatch<z.ZodEnum<{
|
|
13
|
+
hcs: "hcs";
|
|
13
14
|
root: "root";
|
|
14
15
|
launch: "launch";
|
|
15
16
|
boot: "boot";
|
|
@@ -30,6 +31,7 @@ declare const RegistrySchema: z.ZodObject<{
|
|
|
30
31
|
guest: z.ZodString;
|
|
31
32
|
primary: z.ZodBoolean;
|
|
32
33
|
mechanism: z.ZodCatch<z.ZodEnum<{
|
|
34
|
+
hcs: "hcs";
|
|
33
35
|
root: "root";
|
|
34
36
|
launch: "launch";
|
|
35
37
|
boot: "boot";
|
|
@@ -22,7 +22,7 @@ const RegistryEntrySchema = z.object({
|
|
|
22
22
|
host: AbsolutePathSchema,
|
|
23
23
|
guest: z.string().max(1024),
|
|
24
24
|
primary: z.boolean(),
|
|
25
|
-
mechanism: z.enum(['launch', 'root', 'boot', 'wsb']).catch('root'),
|
|
25
|
+
mechanism: z.enum(['launch', 'root', 'boot', 'wsb', 'hcs']).catch('root'),
|
|
26
26
|
registeredAt: z.string().max(64),
|
|
27
27
|
});
|
|
28
28
|
const RegistrySchema = z.object({
|
|
@@ -1,2 +1,3 @@
|
|
|
1
1
|
import { type ContainedDeps } from '../storage/index.js';
|
|
2
|
-
|
|
2
|
+
/** `configOverridden`: the command runs with --config (the helper reads only your usual settings: it is not asked). */
|
|
3
|
+
export declare function realContainedDeps(storageRoot: string, maxGb: number, configOverridden?: boolean): ContainedDeps;
|
|
@@ -1,17 +1,19 @@
|
|
|
1
|
-
// The real wiring of contained storage on this PC: the
|
|
1
|
+
// The real wiring of contained storage on this PC: the storage helper when an all-users install added it, else the
|
|
2
|
+
// permission-prompt scripts, protectPrivateFolder for new
|
|
2
3
|
// folders, your SID, and whether a sandbox runs. Lives here (not in host/storage) so storage never imports the
|
|
3
4
|
// sandbox or sharing folders, which import storage. The self-test never reaches a disk through it: the disk port
|
|
4
5
|
// refuses there.
|
|
5
|
-
import { ScriptDiskPort } from '../storage/index.js';
|
|
6
|
+
import { HelperDiskPort, helperApplies, ScriptDiskPort } from '../storage/index.js';
|
|
6
7
|
import { currentUserSid, protectPrivateFolder } from '../sharing/index.js';
|
|
7
8
|
import { isSandboxRunning } from './platform.js';
|
|
8
|
-
|
|
9
|
+
/** `configOverridden`: the command runs with --config (the helper reads only your usual settings: it is not asked). */
|
|
10
|
+
export function realContainedDeps(storageRoot, maxGb, configOverridden = false) {
|
|
9
11
|
return {
|
|
10
12
|
storageRoot,
|
|
11
13
|
maxGb,
|
|
12
14
|
protect: (dir) => protectPrivateFolder(dir),
|
|
13
15
|
userSid: currentUserSid,
|
|
14
|
-
port: new ScriptDiskPort(),
|
|
16
|
+
port: new HelperDiskPort(new ScriptDiskPort(), helperApplies(process.env, configOverridden)),
|
|
15
17
|
sandboxRunning: isSandboxRunning,
|
|
16
18
|
};
|
|
17
19
|
}
|
|
@@ -13,7 +13,8 @@ export function writeSecrets(dataPath, id, secrets) {
|
|
|
13
13
|
!secrets.claudeCredentials &&
|
|
14
14
|
!secrets.vpn &&
|
|
15
15
|
!secrets.wireguardConfig &&
|
|
16
|
-
!secrets.githubToken
|
|
16
|
+
!secrets.githubToken &&
|
|
17
|
+
!secrets.liveFoldersKey;
|
|
17
18
|
if (empty)
|
|
18
19
|
return null;
|
|
19
20
|
const run = new RunDir(dataPath);
|
|
@@ -43,7 +43,15 @@ export function beginSharing(deps, start, now = new Date()) {
|
|
|
43
43
|
// The owner creates the run folder and every channel folder in it before others may write there: a folder another
|
|
44
44
|
// user created would stay theirs, and writable by them, after sharing is turned off.
|
|
45
45
|
const run = new RunDir(start.dataPath);
|
|
46
|
-
for (const dir of [
|
|
46
|
+
for (const dir of [
|
|
47
|
+
run.root,
|
|
48
|
+
run.requestsDir,
|
|
49
|
+
run.responsesDir,
|
|
50
|
+
run.sessionsDir,
|
|
51
|
+
run.projectsDir,
|
|
52
|
+
run.auditDir,
|
|
53
|
+
run.treesDir,
|
|
54
|
+
])
|
|
47
55
|
fs.mkdirSync(dir, { recursive: true });
|
|
48
56
|
const plan = sharingAclPlan({
|
|
49
57
|
storageUserDir: storageOf(start.dataPath),
|
|
@@ -76,6 +76,19 @@ export class ContainedStorage {
|
|
|
76
76
|
});
|
|
77
77
|
this.requireOk(outcome, 'mount');
|
|
78
78
|
}
|
|
79
|
+
else {
|
|
80
|
+
// Another holder vouches for the mount, but the folder could be a mount point of some other disk (one left after
|
|
81
|
+
// a restart, whose volume name an image of someone else's now has). A port that can check it without a prompt
|
|
82
|
+
// (the storage helper) is asked: it only says "mounted already" for a mount of this very image.
|
|
83
|
+
const confirmed = await this.deps.port.confirmMounted?.({
|
|
84
|
+
paths,
|
|
85
|
+
maxGb: this.deps.maxGb,
|
|
86
|
+
create: false,
|
|
87
|
+
userSid: this.deps.userSid(),
|
|
88
|
+
});
|
|
89
|
+
if (confirmed)
|
|
90
|
+
this.requireOk(confirmed, 'mount');
|
|
91
|
+
}
|
|
79
92
|
this.assertReallyMounted();
|
|
80
93
|
for (const name of CONTAINED_FOLDERS)
|
|
81
94
|
fs.mkdirSync(path.join(paths.mount, name), { recursive: true });
|
|
@@ -22,6 +22,11 @@ export interface DismountRequest {
|
|
|
22
22
|
}
|
|
23
23
|
export interface DiskPort {
|
|
24
24
|
mount(request: MountRequest): Promise<ScriptOutcome>;
|
|
25
|
+
/**
|
|
26
|
+
* A mount that should be there already, checked without a permission prompt: the outcome of a mount (which, for a
|
|
27
|
+
* mount of this image, changes nothing), or null when this port cannot check without a prompt.
|
|
28
|
+
*/
|
|
29
|
+
confirmMounted?(request: MountRequest): Promise<ScriptOutcome | null>;
|
|
25
30
|
dismount(request: DismountRequest): Promise<ScriptOutcome>;
|
|
26
31
|
reality(paths: ContainedPaths): Promise<MountReality>;
|
|
27
32
|
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import type { ContainedPaths } from './layout.js';
|
|
3
|
+
import type { DiskPort, DismountRequest, MountRequest, ScriptOutcome } from './disk-port.js';
|
|
4
|
+
import type { MountReality } from './state.js';
|
|
5
|
+
export declare const HELPER_PIPE = "\\\\.\\pipe\\AgentSandboxStorage";
|
|
6
|
+
export declare const HELPER_PROTOCOL = 1;
|
|
7
|
+
/** What goes to the service: exactly the verb and the data set (the service refuses anything else). */
|
|
8
|
+
export declare const HelperRequestSchema: z.ZodObject<{
|
|
9
|
+
verb: z.ZodEnum<{
|
|
10
|
+
mount: "mount";
|
|
11
|
+
dismount: "dismount";
|
|
12
|
+
}>;
|
|
13
|
+
set: z.ZodString;
|
|
14
|
+
}, z.core.$strict>;
|
|
15
|
+
export type HelperRequest = z.infer<typeof HelperRequestSchema>;
|
|
16
|
+
/** The service's answer (Protocol.Answer in Protocol.cs). */
|
|
17
|
+
export declare const HelperAnswerSchema: z.ZodObject<{
|
|
18
|
+
protocol: z.ZodNumber;
|
|
19
|
+
ok: z.ZodBoolean;
|
|
20
|
+
message: z.ZodString;
|
|
21
|
+
created: z.ZodBoolean;
|
|
22
|
+
alreadyMounted: z.ZodBoolean;
|
|
23
|
+
fallback: z.ZodBoolean;
|
|
24
|
+
image: z.ZodString;
|
|
25
|
+
mount: z.ZodString;
|
|
26
|
+
notes: z.ZodArray<z.ZodString>;
|
|
27
|
+
}, z.core.$strip>;
|
|
28
|
+
export type HelperAnswer = z.infer<typeof HelperAnswerSchema>;
|
|
29
|
+
/** One call: the text of the answer, or why there was none. `absent`: no helper here (or not for you). */
|
|
30
|
+
export type PipeResult = {
|
|
31
|
+
readonly kind: 'answer';
|
|
32
|
+
readonly text: string;
|
|
33
|
+
} | {
|
|
34
|
+
readonly kind: 'absent';
|
|
35
|
+
readonly reason: string;
|
|
36
|
+
} | {
|
|
37
|
+
readonly kind: 'failed';
|
|
38
|
+
readonly reason: string;
|
|
39
|
+
};
|
|
40
|
+
export type PipeConnector = (pipe: string, line: string, timeoutMs: number) => Promise<PipeResult>;
|
|
41
|
+
export declare const pipeConnector: PipeConnector;
|
|
42
|
+
/**
|
|
43
|
+
* Whether the helper may be asked at all. It reads your settings from the usual place (your profile's
|
|
44
|
+
* .agent-sandbox\config.json and the machine file), so a run with other settings (--config, AGENT_SANDBOX_CONFIG,
|
|
45
|
+
* AGENT_SANDBOX_HOME) would get other folders: those use the permission prompt.
|
|
46
|
+
*/
|
|
47
|
+
export declare function helperApplies(env: NodeJS.ProcessEnv, configOverridden: boolean): boolean;
|
|
48
|
+
/** The answer's paths are ours (Windows paths: case does not matter, a trailing backslash neither). */
|
|
49
|
+
export declare function samePaths(answer: Pick<HelperAnswer, 'image' | 'mount'>, paths: ContainedPaths): boolean;
|
|
50
|
+
/** What one answer means: an outcome, or "use the permission prompt instead". */
|
|
51
|
+
export declare function readAnswer(result: PipeResult, paths: ContainedPaths): {
|
|
52
|
+
readonly use: 'outcome';
|
|
53
|
+
readonly outcome: ScriptOutcome;
|
|
54
|
+
} | {
|
|
55
|
+
readonly use: 'fallback';
|
|
56
|
+
readonly why: string;
|
|
57
|
+
};
|
|
58
|
+
/** The helper when it is there and applies, else (and whenever it says so) the permission-prompt scripts. */
|
|
59
|
+
export declare class HelperDiskPort implements DiskPort {
|
|
60
|
+
private readonly fallback;
|
|
61
|
+
private readonly applies;
|
|
62
|
+
private readonly connect;
|
|
63
|
+
private readonly pipe;
|
|
64
|
+
constructor(fallback: DiskPort, applies: boolean, connect?: PipeConnector, pipe?: string);
|
|
65
|
+
mount(request: MountRequest): Promise<ScriptOutcome>;
|
|
66
|
+
/** The helper checks a mount without a prompt (it is "mounted already" only for this image); null without it. */
|
|
67
|
+
confirmMounted(request: MountRequest): Promise<ScriptOutcome | null>;
|
|
68
|
+
dismount(request: DismountRequest): Promise<ScriptOutcome>;
|
|
69
|
+
reality(paths: ContainedPaths): Promise<MountReality>;
|
|
70
|
+
/** The helper's outcome, or null to use the permission prompt. */
|
|
71
|
+
private ask;
|
|
72
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
// The storage helper (native\csharp\StorageHelper, the Windows service AgentSandboxStorage that an all-users install
|
|
2
|
+
// can add): mounting and dismounting go to its pipe instead of a Windows permission prompt. The request names only the
|
|
3
|
+
// data set; the service works out the folders itself from your settings, so the answer's paths must equal ours. When
|
|
4
|
+
// the helper is not installed, cannot be reached, or says to (an image it leaves alone), the permission-prompt scripts
|
|
5
|
+
// run as before. Looking (reality) never needs either.
|
|
6
|
+
import net from 'node:net';
|
|
7
|
+
import { z } from 'zod';
|
|
8
|
+
import { SET_NAME_RULE } from './layout.js';
|
|
9
|
+
import { refuseInSelfTest } from '../../test-guard.js';
|
|
10
|
+
export const HELPER_PIPE = '\\\\.\\pipe\\AgentSandboxStorage';
|
|
11
|
+
export const HELPER_PROTOCOL = 1;
|
|
12
|
+
const MOUNT_TIMEOUT_MS = 10 * 60_000;
|
|
13
|
+
const MAX_ANSWER_BYTES = 64 * 1024;
|
|
14
|
+
/** What goes to the service: exactly the verb and the data set (the service refuses anything else). */
|
|
15
|
+
export const HelperRequestSchema = z
|
|
16
|
+
.object({ verb: z.enum(['mount', 'dismount']), set: z.string().regex(SET_NAME_RULE) })
|
|
17
|
+
.strict();
|
|
18
|
+
/** The service's answer (Protocol.Answer in Protocol.cs). */
|
|
19
|
+
export const HelperAnswerSchema = z.object({
|
|
20
|
+
protocol: z.number().int(),
|
|
21
|
+
ok: z.boolean(),
|
|
22
|
+
message: z.string().max(4000),
|
|
23
|
+
created: z.boolean(),
|
|
24
|
+
alreadyMounted: z.boolean(),
|
|
25
|
+
fallback: z.boolean(),
|
|
26
|
+
image: z.string().max(1000),
|
|
27
|
+
mount: z.string().max(1000),
|
|
28
|
+
notes: z.array(z.string().max(1000)).max(50),
|
|
29
|
+
});
|
|
30
|
+
/** Errors that mean "no helper to talk to": not installed (or stopped), or this user may not open it. */
|
|
31
|
+
const ABSENT_CODES = new Set(['ENOENT', 'EACCES', 'EPERM', 'ECONNREFUSED']);
|
|
32
|
+
export const pipeConnector = (pipe, line, timeoutMs) => new Promise((resolve) => {
|
|
33
|
+
let text = '';
|
|
34
|
+
let connected = false;
|
|
35
|
+
let settled = false;
|
|
36
|
+
const done = (result) => {
|
|
37
|
+
if (settled)
|
|
38
|
+
return;
|
|
39
|
+
settled = true;
|
|
40
|
+
clearTimeout(timer);
|
|
41
|
+
socket.destroy();
|
|
42
|
+
resolve(result);
|
|
43
|
+
};
|
|
44
|
+
const socket = net.connect(pipe);
|
|
45
|
+
const timer = setTimeout(() => done({ kind: 'failed', reason: 'the storage helper did not answer in time' }), timeoutMs);
|
|
46
|
+
socket.setEncoding('utf8');
|
|
47
|
+
socket.on('connect', () => {
|
|
48
|
+
connected = true;
|
|
49
|
+
socket.write(line + '\n');
|
|
50
|
+
});
|
|
51
|
+
socket.on('data', (chunk) => {
|
|
52
|
+
text += chunk;
|
|
53
|
+
const end = text.indexOf('\n');
|
|
54
|
+
if (end >= 0)
|
|
55
|
+
done({ kind: 'answer', text: text.slice(0, end) });
|
|
56
|
+
else if (text.length > MAX_ANSWER_BYTES)
|
|
57
|
+
done({ kind: 'failed', reason: 'the storage helper answered too much' });
|
|
58
|
+
});
|
|
59
|
+
socket.on('error', (e) => {
|
|
60
|
+
if (!connected && ABSENT_CODES.has(e.code ?? ''))
|
|
61
|
+
done({ kind: 'absent', reason: e.code ?? 'no pipe' });
|
|
62
|
+
else
|
|
63
|
+
done({ kind: 'failed', reason: e.message });
|
|
64
|
+
});
|
|
65
|
+
socket.on('close', () => done({ kind: 'failed', reason: 'the storage helper closed the connection' }));
|
|
66
|
+
});
|
|
67
|
+
/**
|
|
68
|
+
* Whether the helper may be asked at all. It reads your settings from the usual place (your profile's
|
|
69
|
+
* .agent-sandbox\config.json and the machine file), so a run with other settings (--config, AGENT_SANDBOX_CONFIG,
|
|
70
|
+
* AGENT_SANDBOX_HOME) would get other folders: those use the permission prompt.
|
|
71
|
+
*/
|
|
72
|
+
export function helperApplies(env, configOverridden) {
|
|
73
|
+
if (configOverridden)
|
|
74
|
+
return false;
|
|
75
|
+
return !env.AGENT_SANDBOX_CONFIG && !env.AGENT_SANDBOX_HOME;
|
|
76
|
+
}
|
|
77
|
+
/** The answer's paths are ours (Windows paths: case does not matter, a trailing backslash neither). */
|
|
78
|
+
export function samePaths(answer, paths) {
|
|
79
|
+
const norm = (p) => p.replace(/\\+$/, '').toLowerCase();
|
|
80
|
+
return norm(answer.image) === norm(paths.image) && norm(answer.mount) === norm(paths.mount);
|
|
81
|
+
}
|
|
82
|
+
/** What one answer means: an outcome, or "use the permission prompt instead". */
|
|
83
|
+
export function readAnswer(result, paths) {
|
|
84
|
+
if (result.kind === 'absent')
|
|
85
|
+
return { use: 'fallback', why: 'no storage helper (' + result.reason + ')' };
|
|
86
|
+
if (result.kind === 'failed')
|
|
87
|
+
return { use: 'outcome', outcome: failed('The storage helper did not finish: ' + result.reason + '.') };
|
|
88
|
+
let raw;
|
|
89
|
+
try {
|
|
90
|
+
raw = JSON.parse(result.text);
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
return { use: 'fallback', why: 'the storage helper gave an answer that is not JSON' };
|
|
94
|
+
}
|
|
95
|
+
const parsed = HelperAnswerSchema.safeParse(raw);
|
|
96
|
+
if (!parsed.success || parsed.data.protocol !== HELPER_PROTOCOL)
|
|
97
|
+
return { use: 'fallback', why: 'the storage helper speaks another version' };
|
|
98
|
+
const answer = parsed.data;
|
|
99
|
+
if (answer.fallback)
|
|
100
|
+
return { use: 'fallback', why: answer.message };
|
|
101
|
+
if (answer.ok && !samePaths(answer, paths))
|
|
102
|
+
return {
|
|
103
|
+
use: 'outcome',
|
|
104
|
+
outcome: failed('The storage helper used other folders (' +
|
|
105
|
+
answer.mount +
|
|
106
|
+
') than this command (' +
|
|
107
|
+
paths.mount +
|
|
108
|
+
'): your settings differ from what it reads. Run sandbox doctor.'),
|
|
109
|
+
};
|
|
110
|
+
return {
|
|
111
|
+
use: 'outcome',
|
|
112
|
+
outcome: {
|
|
113
|
+
ok: answer.ok,
|
|
114
|
+
message: answer.message,
|
|
115
|
+
created: answer.created,
|
|
116
|
+
alreadyMounted: answer.alreadyMounted,
|
|
117
|
+
notes: answer.notes,
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
function failed(message) {
|
|
122
|
+
return { ok: false, message, created: false, alreadyMounted: false, notes: [] };
|
|
123
|
+
}
|
|
124
|
+
/** The helper when it is there and applies, else (and whenever it says so) the permission-prompt scripts. */
|
|
125
|
+
export class HelperDiskPort {
|
|
126
|
+
fallback;
|
|
127
|
+
applies;
|
|
128
|
+
connect;
|
|
129
|
+
pipe;
|
|
130
|
+
constructor(fallback, applies, connect = pipeConnector, pipe = HELPER_PIPE) {
|
|
131
|
+
this.fallback = fallback;
|
|
132
|
+
this.applies = applies;
|
|
133
|
+
this.connect = connect;
|
|
134
|
+
this.pipe = pipe;
|
|
135
|
+
}
|
|
136
|
+
async mount(request) {
|
|
137
|
+
const answer = await this.ask({ verb: 'mount', set: request.paths.set }, request.paths);
|
|
138
|
+
return answer ?? this.fallback.mount(request);
|
|
139
|
+
}
|
|
140
|
+
/** The helper checks a mount without a prompt (it is "mounted already" only for this image); null without it. */
|
|
141
|
+
confirmMounted(request) {
|
|
142
|
+
return this.ask({ verb: 'mount', set: request.paths.set }, request.paths);
|
|
143
|
+
}
|
|
144
|
+
async dismount(request) {
|
|
145
|
+
const answer = await this.ask({ verb: 'dismount', set: request.paths.set }, request.paths);
|
|
146
|
+
return answer ?? this.fallback.dismount(request);
|
|
147
|
+
}
|
|
148
|
+
reality(paths) {
|
|
149
|
+
return this.fallback.reality(paths);
|
|
150
|
+
}
|
|
151
|
+
/** The helper's outcome, or null to use the permission prompt. */
|
|
152
|
+
async ask(request, paths) {
|
|
153
|
+
if (!this.applies)
|
|
154
|
+
return null;
|
|
155
|
+
// A test talks to a helper of its own (another pipe name); the real one could mount a real disk.
|
|
156
|
+
if (this.pipe === HELPER_PIPE)
|
|
157
|
+
refuseInSelfTest('mount or dismount a disk image');
|
|
158
|
+
const line = JSON.stringify(HelperRequestSchema.parse(request));
|
|
159
|
+
const read = readAnswer(await this.connect(this.pipe, line, MOUNT_TIMEOUT_MS), paths);
|
|
160
|
+
return read.use === 'outcome' ? read.outcome : null;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
@@ -2,6 +2,7 @@ export { CONTAINED_FOLDERS, containedMappings, containedPaths, setNameOf, type C
|
|
|
2
2
|
export type { ContainedPaths } from './layout.js';
|
|
3
3
|
export { ContainedStorage, type ContainedDeps, type HolderId, type ReleaseResult } from './contained-storage.js';
|
|
4
4
|
export { ScriptDiskPort, spawnRunner, type DiskPort, type MountRequest, type ScriptOutcome } from './disk-port.js';
|
|
5
|
+
export { HELPER_PIPE, HelperAnswerSchema, HelperDiskPort, helperApplies, HelperRequestSchema, pipeConnector, readAnswer, samePaths, type PipeConnector, type PipeResult, } from './helper-port.js';
|
|
5
6
|
export { containedAvailability, containedFacts, type ContainedFacts } from './check.js';
|
|
6
7
|
export { hostDataFolder, hostDataRoot } from './resolve.js';
|
|
7
8
|
export { writeContainedSetting } from './setting.js';
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export { CONTAINED_FOLDERS, containedMappings, containedPaths, setNameOf } from './layout.js';
|
|
2
2
|
export { ContainedStorage } from './contained-storage.js';
|
|
3
3
|
export { ScriptDiskPort, spawnRunner } from './disk-port.js';
|
|
4
|
+
export { HELPER_PIPE, HelperAnswerSchema, HelperDiskPort, helperApplies, HelperRequestSchema, pipeConnector, readAnswer, samePaths, } from './helper-port.js';
|
|
4
5
|
export { containedAvailability, containedFacts } from './check.js';
|
|
5
6
|
export { hostDataFolder, hostDataRoot } from './resolve.js';
|
|
6
7
|
export { writeContainedSetting } from './setting.js';
|
|
@@ -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;
|
package/dist/host/web/bridge.js
CHANGED
|
@@ -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). */
|
package/dist/host/web/server.js
CHANGED
|
@@ -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.
|
|
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);
|
package/dist/runtime/agent.js
CHANGED
|
@@ -26645,6 +26645,9 @@ var ProjectsSchema = external_exports.strictObject({
|
|
|
26645
26645
|
),
|
|
26646
26646
|
wsbShare: external_exports.boolean().describe(
|
|
26647
26647
|
"On Windows 11 24H2+ with the Windows Sandbox command line (wsb), add other folders to the running sandbox with `wsb share` (untested). Otherwise starting a project outside every root offers a restart."
|
|
26648
|
+
),
|
|
26649
|
+
liveFolders: external_exports.enum(["off", "hcs"]).describe(
|
|
26650
|
+
'ALPHA, UNTESTED ON A REAL PC: "hcs" adds a project folder to the running sandbox through the Host Compute Service where `wsb share` is missing (one administrator prompt per add or remove; always read-only in this alpha). If it fails, the sandbox may close: save your work first. Turn it on before the sandbox starts. "off" (default) offers a restart instead. The real-PC tests still to pass are in docs/design/hcs-live-folders.md ("Test plan").'
|
|
26648
26651
|
)
|
|
26649
26652
|
});
|
|
26650
26653
|
|
|
@@ -27173,8 +27176,16 @@ var RunDir = class {
|
|
|
27173
27176
|
projectsDir;
|
|
27174
27177
|
/** audit.processes: programs the sandbox started, in small .jsonl files the host takes in and deletes. */
|
|
27175
27178
|
auditDir;
|
|
27179
|
+
/**
|
|
27180
|
+
* Each running session's process tree, recorded by its ptyhost (guest only: the daemon ends what a ptyhost that was
|
|
27181
|
+
* itself ended left running).
|
|
27182
|
+
*/
|
|
27183
|
+
treesDir;
|
|
27176
27184
|
/** One-shot secrets the host hands over (see secrets.ts). Next to run, not in it: shared-use users can change run. */
|
|
27177
27185
|
secretsDir;
|
|
27186
|
+
/** Live folders (live.ts): signed host requests, and the daemon's replies. */
|
|
27187
|
+
liveDir;
|
|
27188
|
+
liveRepliesDir;
|
|
27178
27189
|
constructor(dataDir2) {
|
|
27179
27190
|
this.root = path5.join(dataDir2, "run");
|
|
27180
27191
|
this.sandboxFile = path5.join(this.root, "sandbox.json");
|
|
@@ -27186,7 +27197,16 @@ var RunDir = class {
|
|
|
27186
27197
|
this.sessionsDir = path5.join(this.root, "sessions");
|
|
27187
27198
|
this.projectsDir = path5.join(this.root, "projects");
|
|
27188
27199
|
this.auditDir = path5.join(this.root, "audit");
|
|
27200
|
+
this.treesDir = path5.join(this.root, "trees");
|
|
27189
27201
|
this.secretsDir = path5.join(dataDir2, "secrets");
|
|
27202
|
+
this.liveDir = path5.join(this.root, "live");
|
|
27203
|
+
this.liveRepliesDir = path5.join(this.root, "live-replies");
|
|
27204
|
+
}
|
|
27205
|
+
liveFile(id) {
|
|
27206
|
+
return path5.join(this.liveDir, safeId(id) + ".json");
|
|
27207
|
+
}
|
|
27208
|
+
liveReplyFile(id) {
|
|
27209
|
+
return path5.join(this.liveRepliesDir, safeId(id) + ".json");
|
|
27190
27210
|
}
|
|
27191
27211
|
requestFile(id) {
|
|
27192
27212
|
return path5.join(this.requestsDir, safeId(id) + ".json");
|
|
@@ -27197,6 +27217,9 @@ var RunDir = class {
|
|
|
27197
27217
|
sessionFile(id) {
|
|
27198
27218
|
return path5.join(this.sessionsDir, safeId(id) + ".json");
|
|
27199
27219
|
}
|
|
27220
|
+
treeFile(id) {
|
|
27221
|
+
return path5.join(this.treesDir, safeId(id) + ".json");
|
|
27222
|
+
}
|
|
27200
27223
|
projectFile(name) {
|
|
27201
27224
|
return path5.join(this.projectsDir, safeId(name) + ".json");
|
|
27202
27225
|
}
|