windows-development-sandbox 1.27.3 → 1.29.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/README.md +5 -4
- package/dist/app/features/sandbox/sandbox.run.js +1 -1
- package/dist/bin/{chunk-FA5VWLP4.js → chunk-O4EL32TM.js} +1 -1
- package/dist/bin/{chunk-RU7XRN62.js → chunk-QYRRWGFY.js} +1637 -1354
- package/dist/bin/sandbox.js +10772 -3
- package/dist/cli/commands/launch/launch.js +1 -1
- package/dist/cli/commands/launch/running.js +2 -2
- package/dist/cli/index.js +1 -1
- package/dist/cli/menu/pages/agents-page.js +1 -1
- package/dist/cli/services/sandbox-service.js +1 -1
- package/dist/core/schema/sandbox.js +2 -2
- package/dist/guest/apps/app-installer.js +1 -1
- package/dist/guest/apps/store-check.d.ts +2 -0
- package/dist/guest/apps/store-check.js +3 -1
- package/dist/guest/boot/guide-lines.d.ts +2 -0
- package/dist/guest/boot/guide-lines.js +7 -0
- package/dist/guest/boot/guide.js +2 -1
- package/dist/guest/daemon/heartbeat.js +1 -0
- package/dist/guest/governor/core.d.ts +53 -1
- package/dist/guest/governor/core.js +95 -1
- package/dist/guest/governor/decide.d.ts +4 -0
- package/dist/guest/governor/decide.js +2 -0
- package/dist/guest/governor/governor.js +23 -9
- package/dist/guest/toolchains/runtimes.js +7 -6
- package/dist/host/accounts/deliver.js +16 -2
- package/dist/host/apps/store-package.d.ts +2 -0
- package/dist/host/apps/store-package.js +16 -15
- package/dist/host/channel/attach.js +1 -1
- package/dist/host/channel/channel.js +3 -3
- package/dist/host/guard/commands.js +1 -0
- package/dist/host/health/path-copies.d.ts +1 -1
- package/dist/host/health/path-copies.js +2 -2
- package/dist/host/install/index.d.ts +1 -1
- package/dist/host/install/index.js +1 -1
- package/dist/host/install/shims.d.ts +8 -1
- package/dist/host/install/shims.js +39 -1
- package/dist/host/launch/generate.js +4 -0
- package/dist/host/launch/index.d.ts +1 -0
- package/dist/host/launch/index.js +1 -0
- package/dist/host/launch/pc-logs.d.ts +6 -0
- package/dist/host/launch/pc-logs.js +129 -0
- package/dist/host/life/crash-report.d.ts +4 -0
- package/dist/host/life/crash-report.js +30 -1
- package/dist/host/life/last-end.js +1 -0
- package/dist/host/sandbox/platform.js +1 -1
- package/dist/host/sandbox/window.js +1 -1
- package/dist/host/tools/windows-terminal.d.ts +4 -1
- package/dist/host/tools/windows-terminal.js +25 -9
- package/dist/runtime/agent.js +6 -2
- package/dist/runtime/daemon.js +111 -16
- package/dist/runtime/db.js +6 -2
- package/dist/runtime/main.js +199 -35
- package/dist/runtime/netproxy.js +6 -2
- package/dist/runtime/tool.js +6 -2
- package/dist/runtime/view.js +126 -28
- package/dist/selftest/agent-paths.test.js +3 -1
- package/dist/selftest/channel.test.js +33 -0
- package/dist/selftest/cli.test.js +5 -5
- package/dist/selftest/config.test.js +1 -1
- package/dist/selftest/credentials.test.js +1 -1
- package/dist/selftest/engine.test.js +1 -1
- package/dist/selftest/generate.test.js +66 -1
- package/dist/selftest/hcs.test.js +1 -1
- package/dist/selftest/health.test.js +21 -10
- package/dist/selftest/heavy-load.test.js +112 -4
- package/dist/selftest/host-sim.test.js +13 -14
- package/dist/selftest/install.test.js +6 -2
- package/dist/selftest/live-terminal.test.js +103 -0
- package/dist/selftest/personalization.test.js +2 -1
- package/dist/selftest/store-app.test.js +62 -4
- package/dist/selftest/support/engine-golden.js +2 -0
- package/dist/selftest/support/web-preview.js +2 -2
- package/dist/shared/accounts/kinds.d.ts +5 -0
- package/dist/shared/accounts/kinds.js +8 -0
- package/dist/shared/apps/index.d.ts +1 -1
- package/dist/shared/apps/index.js +1 -1
- package/dist/shared/apps/selection.d.ts +1 -0
- package/dist/shared/apps/selection.js +4 -3
- package/dist/shared/apps/store-package.d.ts +13 -0
- package/dist/shared/apps/store-package.js +24 -0
- package/dist/shared/channel/schema.d.ts +2 -0
- package/dist/shared/channel/schema.js +4 -0
- package/dist/shared/memory/policy.d.ts +12 -0
- package/dist/shared/memory/policy.js +4 -0
- package/dist/shared/pty/clipboard-watch.d.ts +4 -0
- package/dist/shared/pty/clipboard-watch.js +40 -0
- package/dist/shared/pty/ctrl-c.d.ts +13 -7
- package/dist/shared/pty/ctrl-c.js +35 -19
- package/dist/shared/pty/index.d.ts +1 -0
- package/dist/shared/pty/index.js +1 -0
- package/dist/shared/pty/terminal.d.ts +1 -1
- package/dist/shared/pty/terminal.js +16 -13
- package/dist/shared/web/ctrl-c.d.ts +26 -0
- package/dist/shared/web/ctrl-c.js +64 -0
- package/dist/shared/web/index.d.ts +1 -0
- package/dist/shared/web/index.js +1 -0
- package/dist/web/index.html +1 -1
- package/dist/web/{main-NKM7ECSB.js → main-7NY4X6YU.js} +67 -67
- package/native/csharp/HcsHost/AssemblyInfo.cs +3 -3
- package/native/csharp/HostTools/AssemblyInfo.cs +3 -3
- package/native/csharp/HostTools/Window.cs +1 -1
- package/native/csharp/StorageHelper/AssemblyInfo.cs +3 -3
- package/native/scripts/get-store-package.ps1 +24 -13
- package/package.json +4 -14
- package/sandbox/csharp/ClipboardWatch.cs +59 -0
- package/sandbox/csharp/MemGov.cs +752 -7
- package/sandbox/scripts/install-store-package.ps1 +11 -4
- package/sandbox/scripts/watch-clipboard.ps1 +8 -0
- package/schemas/config.schema.json +2 -2
- package/schemas/sandbox-config.schema.json +1 -1
- package/dist/bin/agent-sandbox.d.ts +0 -2
- package/dist/bin/agent-sandbox.js +0 -9
- package/dist/bin/chunk-4M5SZZ5P.js +0 -10778
- package/dist/bin/claude-host.d.ts +0 -2
- package/dist/bin/claude-host.js +0 -11
- package/dist/bin/claude-sbx.d.ts +0 -2
- package/dist/bin/claude-sbx.js +0 -9
- package/dist/bin/codex-sbx.d.ts +0 -2
- package/dist/bin/codex-sbx.js +0 -9
- package/dist/bin/gemini-sbx.d.ts +0 -2
- package/dist/bin/gemini-sbx.js +0 -9
- package/dist/bin/sbx-hide.d.ts +0 -2
- package/dist/bin/sbx-hide.js +0 -9
- package/dist/bin/sbx-show.d.ts +0 -2
- package/dist/bin/sbx-show.js +0 -9
|
@@ -16,6 +16,7 @@ export const COMMANDS = new RegExp('^(' +
|
|
|
16
16
|
// this tool's own commands (package.json "bin")
|
|
17
17
|
'sandbox',
|
|
18
18
|
'windows-development-sandbox',
|
|
19
|
+
// Removed commands, still blocked: a planted claude-sbx.cmd would run on a PC where nothing else has that name.
|
|
19
20
|
'agent-sandbox',
|
|
20
21
|
'claude-sbx',
|
|
21
22
|
'claude-sandbox',
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export declare const COMMAND_NAMES: readonly ["sandbox"
|
|
1
|
+
export declare const COMMAND_NAMES: readonly ["sandbox"];
|
|
2
2
|
export type CopyKind = 'managed-machine' | 'managed-user' | 'npm' | 'old-scripts' | 'other';
|
|
3
3
|
/** One folder on the PATH that holds the commands. */
|
|
4
4
|
export interface CommandCopy {
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
// Every copy of the sandbox commands on the PATH (sandbox
|
|
1
|
+
// Every copy of the sandbox commands on the PATH (sandbox), in the order Windows finds them:
|
|
2
2
|
// the machine PATH, then yours, and in each folder PATHEXT order. Read only: nothing is run, and a file is only checked
|
|
3
3
|
// for existing. Used by doctor to say which copy runs and how to remove each of the others.
|
|
4
4
|
import fs from 'node:fs';
|
|
5
5
|
import path from 'node:path';
|
|
6
6
|
import { expandVariables } from '../../core/util/index.js';
|
|
7
|
-
export const COMMAND_NAMES = ['sandbox'
|
|
7
|
+
export const COMMAND_NAMES = ['sandbox'];
|
|
8
8
|
const normal = (p) => path
|
|
9
9
|
.normalize(expandVariables(p.trim().replace(/^"|"$/g, '')))
|
|
10
10
|
.replace(/[\\/]+$/, '')
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { type InstallScope, InstallLayout, compareVersions, VERSION_PATTERN, NODE_PATTERN } from './layout.js';
|
|
2
|
-
export { shimText, readBins, writeShims } from './shims.js';
|
|
2
|
+
export { shimText, readBins, writeShims, removeStaleShims, REMOVED_COMMANDS } from './shims.js';
|
|
3
3
|
export { InstallRecordFile, InstallRecordSchema, type InstallRecord } from './install-record.js';
|
|
4
4
|
export { UpdateTask } from './scheduled-task.js';
|
|
5
5
|
export { machineInstall, userInstall, managedInstalls, describeCopy, hiddenInstallLine, takeHiddenInstallNotice, } from './copies.js';
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { InstallLayout, compareVersions, VERSION_PATTERN, NODE_PATTERN } from './layout.js';
|
|
2
|
-
export { shimText, readBins, writeShims } from './shims.js';
|
|
2
|
+
export { shimText, readBins, writeShims, removeStaleShims, REMOVED_COMMANDS } from './shims.js';
|
|
3
3
|
export { InstallRecordFile, InstallRecordSchema } from './install-record.js';
|
|
4
4
|
export { UpdateTask } from './scheduled-task.js';
|
|
5
5
|
export { machineInstall, userInstall, managedInstalls, describeCopy, hiddenInstallLine, takeHiddenInstallNotice, } from './copies.js';
|
|
@@ -6,5 +6,12 @@ import type { InstallLayout } from './layout.js';
|
|
|
6
6
|
export declare function shimText(name: string, kind: 'cmd' | 'sh', target?: string): string;
|
|
7
7
|
/** The commands a package.json declares (name -> program path inside the package). Invalid names are refused. */
|
|
8
8
|
export declare function readBins(packageJson: unknown): Record<string, string>;
|
|
9
|
-
/**
|
|
9
|
+
/** Commands earlier versions installed (name -> program they ran). Their shims go when nothing declares them. */
|
|
10
|
+
export declare const REMOVED_COMMANDS: Readonly<Record<string, string>>;
|
|
11
|
+
/** Deletes the shim of a removed command, only when the file is exactly the text we wrote for it. */
|
|
12
|
+
export declare function removeStaleShims(layout: InstallLayout, keep?: Readonly<Record<string, string>>): void;
|
|
13
|
+
/**
|
|
14
|
+
* Writes <bin>.cmd and <bin> for every command in `bins` (name -> program path inside the package), and removes the
|
|
15
|
+
* shims of commands that no longer exist.
|
|
16
|
+
*/
|
|
10
17
|
export declare function writeShims(layout: InstallLayout, bins: Record<string, string>): void;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// The commands on PATH. Each one is a tiny script that reads the two pointer files and runs the version they name, so
|
|
2
2
|
// an update never has to rewrite a shim: it only moves the pointers. A .cmd for cmd.exe and PowerShell, and a POSIX
|
|
3
3
|
// script (same name, no extension) for Git Bash.
|
|
4
|
+
import fs from 'node:fs';
|
|
4
5
|
import path from 'node:path';
|
|
5
6
|
import { z } from 'zod';
|
|
6
7
|
import { fail } from '../../core/util/index.js';
|
|
@@ -46,10 +47,47 @@ export function readBins(packageJson) {
|
|
|
46
47
|
fail('package.json declares a command with an invalid name: ' + name);
|
|
47
48
|
return bins;
|
|
48
49
|
}
|
|
49
|
-
/**
|
|
50
|
+
/** Commands earlier versions installed (name -> program they ran). Their shims go when nothing declares them. */
|
|
51
|
+
export const REMOVED_COMMANDS = {
|
|
52
|
+
'agent-sandbox': 'dist/bin/agent-sandbox.js',
|
|
53
|
+
'claude-sbx': 'dist/bin/claude-sbx.js',
|
|
54
|
+
'claude-sandbox': 'dist/bin/claude-sbx.js',
|
|
55
|
+
'codex-sbx': 'dist/bin/codex-sbx.js',
|
|
56
|
+
'codex-sandbox': 'dist/bin/codex-sbx.js',
|
|
57
|
+
'gemini-sbx': 'dist/bin/gemini-sbx.js',
|
|
58
|
+
'gemini-sandbox': 'dist/bin/gemini-sbx.js',
|
|
59
|
+
'claude-host': 'dist/bin/claude-host.js',
|
|
60
|
+
'sbx-show': 'dist/bin/sbx-show.js',
|
|
61
|
+
'sbx-hide': 'dist/bin/sbx-hide.js',
|
|
62
|
+
};
|
|
63
|
+
/** Deletes the shim of a removed command, only when the file is exactly the text we wrote for it. */
|
|
64
|
+
export function removeStaleShims(layout, keep = {}) {
|
|
65
|
+
for (const [name, target] of Object.entries(REMOVED_COMMANDS)) {
|
|
66
|
+
if (name in keep)
|
|
67
|
+
continue;
|
|
68
|
+
for (const [file, kind] of [
|
|
69
|
+
[name + '.cmd', 'cmd'],
|
|
70
|
+
[name, 'sh'],
|
|
71
|
+
]) {
|
|
72
|
+
const full = path.join(layout.binDir, file);
|
|
73
|
+
try {
|
|
74
|
+
if (fs.readFileSync(full, 'utf8') === shimText(name, kind, target))
|
|
75
|
+
fs.rmSync(full, { force: true });
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
// Not there, or not readable: nothing of ours to remove.
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Writes <bin>.cmd and <bin> for every command in `bins` (name -> program path inside the package), and removes the
|
|
85
|
+
* shims of commands that no longer exist.
|
|
86
|
+
*/
|
|
50
87
|
export function writeShims(layout, bins) {
|
|
51
88
|
for (const [name, target] of Object.entries(bins)) {
|
|
52
89
|
writeFileAtomic(path.join(layout.binDir, name + '.cmd'), shimText(name, 'cmd', target));
|
|
53
90
|
writeFileAtomic(path.join(layout.binDir, name), shimText(name, 'sh', target));
|
|
54
91
|
}
|
|
92
|
+
removeStaleShims(layout, bins);
|
|
55
93
|
}
|
|
@@ -15,6 +15,7 @@ import { folderAttachment } from '../storage/providers/index.js';
|
|
|
15
15
|
import { assertTestWritable } from '../test-guard.js';
|
|
16
16
|
import { writeLoadSettings } from '../life/watchdog.js';
|
|
17
17
|
import { loadProfileOf } from './risk-notes.js';
|
|
18
|
+
import { launchHasSandboxSource, stagePcLogs } from './pc-logs.js';
|
|
18
19
|
import { planBootProjects } from './boot-projects.js';
|
|
19
20
|
import { planDatabases } from './database-plan.js';
|
|
20
21
|
import { collectMappings } from './mappings.js';
|
|
@@ -135,6 +136,9 @@ opened) {
|
|
|
135
136
|
fs.writeFileSync(path.join(launchPath, 'env.cmd'), logonEnv(session.workspace), 'utf8');
|
|
136
137
|
copyRuntime(o.packageRoot, launchPath);
|
|
137
138
|
stageHooks(o.packageRoot, launchPath, o.run.scripts);
|
|
139
|
+
// Developing the sandbox inside a sandbox: this PC's recent crash reports and logs (never for other projects).
|
|
140
|
+
if (launchHasSandboxSource([workspaceHost, ...boot.projects.map((p) => p.host)], o.packageRoot))
|
|
141
|
+
stagePcLogs(storageRoot, dataPath, launchPath);
|
|
138
142
|
const enginePlan = engine.plan(buildLaunchPlan({
|
|
139
143
|
config,
|
|
140
144
|
memoryMb,
|
|
@@ -10,3 +10,4 @@ export { msvcHint, projectEntries, readProjectFiles, uvHint } from './project-sc
|
|
|
10
10
|
export { hostDatabases, pcLanguages } from './pc-scan.js';
|
|
11
11
|
export { collectMappings } from './mappings.js';
|
|
12
12
|
export { RUNTIME_FILES, copyRuntime, gitIdentity, stageGitIdentity, stageHooks, stageScripts } from './stage.js';
|
|
13
|
+
export { isSandboxSourceRepo, launchHasSandboxSource, stagePcLogs } from './pc-logs.js';
|
|
@@ -10,3 +10,4 @@ export { msvcHint, projectEntries, readProjectFiles, uvHint } from './project-sc
|
|
|
10
10
|
export { hostDatabases, pcLanguages } from './pc-scan.js';
|
|
11
11
|
export { collectMappings } from './mappings.js';
|
|
12
12
|
export { RUNTIME_FILES, copyRuntime, gitIdentity, stageGitIdentity, stageHooks, stageScripts } from './stage.js';
|
|
13
|
+
export { isSandboxSourceRepo, launchHasSandboxSource, stagePcLogs } from './pc-logs.js';
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** Whether a folder is a checkout (or git worktree) of this sandbox's own source repo, judged by the installed copy. */
|
|
2
|
+
export declare function isSandboxSourceRepo(folder: string, packageRoot: string): boolean;
|
|
3
|
+
/** Whether this launch has the sandbox's own source repo among its projects (the launch folder or a mapped project). */
|
|
4
|
+
export declare function launchHasSandboxSource(folders: readonly string[], packageRoot: string): boolean;
|
|
5
|
+
/** Copies the PC's recent crash reports and log tails into <launchPath>\pc-logs. Returns the folder. */
|
|
6
|
+
export declare function stagePcLogs(storageRoot: string, dataPath: string, launchPath: string): string;
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
// PC logs for developing the sandbox inside a sandbox: when a launch includes this very source repo, the PC's recent
|
|
2
|
+
// crash reports and logs are copied into <launch>\pc-logs (C:\AgentLaunch\pc-logs, read-only, gone with the sandbox).
|
|
3
|
+
// The gate is the repository url: a repo that only claims the package name does not pass. If one ever did, it would
|
|
4
|
+
// see the owner's crash diagnostics (memory numbers, Windows events, process names), which hold no secrets.
|
|
5
|
+
// Nothing else from <storage> is copied: no state files, logins, secrets or session ledger.
|
|
6
|
+
import fs from 'node:fs';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import { z } from 'zod';
|
|
9
|
+
import { assertNoLinks, ensureDir } from '../../core/util/index.js';
|
|
10
|
+
const PACKAGE_NAME = 'windows-development-sandbox';
|
|
11
|
+
const MAX_PACKAGE_JSON_BYTES = 256 * 1024;
|
|
12
|
+
const MAX_REPORTS = 10;
|
|
13
|
+
const MAX_REPORT_BYTES = 64 * 1024;
|
|
14
|
+
const TAIL_BYTES = 256 * 1024;
|
|
15
|
+
const PackageSchema = z.object({
|
|
16
|
+
name: z.string(),
|
|
17
|
+
repository: z.union([z.string(), z.object({ url: z.string().optional() }).passthrough()]).optional(),
|
|
18
|
+
});
|
|
19
|
+
function repoUrl(pkg) {
|
|
20
|
+
const url = typeof pkg.repository === 'string' ? pkg.repository : pkg.repository?.url;
|
|
21
|
+
if (!url)
|
|
22
|
+
return null;
|
|
23
|
+
return url
|
|
24
|
+
.trim()
|
|
25
|
+
.toLowerCase()
|
|
26
|
+
.replace(/^git\+/, '')
|
|
27
|
+
.replace(/\.git$/, '')
|
|
28
|
+
.replace(/\/+$/, '');
|
|
29
|
+
}
|
|
30
|
+
/** The package.json in a folder, read with a size cap and no links; null when missing, too big or not valid. */
|
|
31
|
+
function readPackage(folder) {
|
|
32
|
+
try {
|
|
33
|
+
const file = path.join(folder, 'package.json');
|
|
34
|
+
assertNoLinks(folder, file);
|
|
35
|
+
const stat = fs.lstatSync(file);
|
|
36
|
+
if (!stat.isFile() || stat.size > MAX_PACKAGE_JSON_BYTES)
|
|
37
|
+
return null;
|
|
38
|
+
const parsed = PackageSchema.safeParse(JSON.parse(fs.readFileSync(file, 'utf8')));
|
|
39
|
+
return parsed.success ? parsed.data : null;
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
// No package.json, a link, or not JSON: not the sandbox's source.
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/** Whether a folder is a checkout (or git worktree) of this sandbox's own source repo, judged by the installed copy. */
|
|
47
|
+
export function isSandboxSourceRepo(folder, packageRoot) {
|
|
48
|
+
const own = readPackage(packageRoot);
|
|
49
|
+
const ownUrl = own ? repoUrl(own) : null;
|
|
50
|
+
if (!ownUrl)
|
|
51
|
+
return false;
|
|
52
|
+
const pkg = readPackage(folder);
|
|
53
|
+
return pkg !== null && pkg.name === PACKAGE_NAME && repoUrl(pkg) === ownUrl;
|
|
54
|
+
}
|
|
55
|
+
/** Plain file, no links on the way: its first `max` bytes (head) or last `max` bytes (tail); null when unreadable. */
|
|
56
|
+
function readBounded(root, file, max, from) {
|
|
57
|
+
try {
|
|
58
|
+
assertNoLinks(root, file);
|
|
59
|
+
const stat = fs.lstatSync(file);
|
|
60
|
+
if (!stat.isFile())
|
|
61
|
+
return null;
|
|
62
|
+
const length = Math.min(stat.size, max);
|
|
63
|
+
const buffer = Buffer.alloc(length);
|
|
64
|
+
const fd = fs.openSync(file, 'r');
|
|
65
|
+
try {
|
|
66
|
+
fs.readSync(fd, buffer, 0, length, from === 'tail' ? stat.size - length : 0);
|
|
67
|
+
}
|
|
68
|
+
finally {
|
|
69
|
+
fs.closeSync(fd);
|
|
70
|
+
}
|
|
71
|
+
return buffer;
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
// Missing or unreadable: left out.
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
const README = [
|
|
79
|
+
'These files come from the PC that started this sandbox: its recent crash reports and logs, for developing the',
|
|
80
|
+
'sandbox itself. They are read-only here and gone when the sandbox closes.',
|
|
81
|
+
'',
|
|
82
|
+
'They are not part of the repo: never copy them into the workspace and never commit them.',
|
|
83
|
+
'',
|
|
84
|
+
'crashes\\ the newest crash reports (what Windows logged when a sandbox ended, plus the memory log)',
|
|
85
|
+
'*.log the last 256 KB of the PC memory log, the host watcher log and the live-folders log',
|
|
86
|
+
'',
|
|
87
|
+
].join('\r\n');
|
|
88
|
+
/** Whether this launch has the sandbox's own source repo among its projects (the launch folder or a mapped project). */
|
|
89
|
+
export function launchHasSandboxSource(folders, packageRoot) {
|
|
90
|
+
return folders.some((f) => isSandboxSourceRepo(f, packageRoot));
|
|
91
|
+
}
|
|
92
|
+
/** Copies the PC's recent crash reports and log tails into <launchPath>\pc-logs. Returns the folder. */
|
|
93
|
+
export function stagePcLogs(storageRoot, dataPath, launchPath) {
|
|
94
|
+
const target = path.join(launchPath, 'pc-logs');
|
|
95
|
+
ensureDir(target);
|
|
96
|
+
fs.writeFileSync(path.join(target, 'README.txt'), README, 'utf8');
|
|
97
|
+
const crashes = path.join(storageRoot, 'logs', 'crashes');
|
|
98
|
+
let names = [];
|
|
99
|
+
try {
|
|
100
|
+
assertNoLinks(storageRoot, crashes);
|
|
101
|
+
names = fs
|
|
102
|
+
.readdirSync(crashes)
|
|
103
|
+
.filter((f) => /^crash-[\w.-]+\.txt$/.test(f))
|
|
104
|
+
.sort()
|
|
105
|
+
.slice(-MAX_REPORTS);
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
// No reports folder yet: none to copy.
|
|
109
|
+
}
|
|
110
|
+
for (const name of names) {
|
|
111
|
+
const text = readBounded(storageRoot, path.join(crashes, name), MAX_REPORT_BYTES, 'head');
|
|
112
|
+
if (!text)
|
|
113
|
+
continue;
|
|
114
|
+
ensureDir(path.join(target, 'crashes'));
|
|
115
|
+
fs.writeFileSync(path.join(target, 'crashes', name), text);
|
|
116
|
+
}
|
|
117
|
+
const data = path.basename(dataPath);
|
|
118
|
+
const tails = [
|
|
119
|
+
[path.join(storageRoot, 'logs', 'host-memory-' + data + '.log'), 'host-memory.log'],
|
|
120
|
+
[path.join(storageRoot, 'logs', 'host-sync-' + data + '.log'), 'host-sync.log'],
|
|
121
|
+
[path.join(storageRoot, 'state', 'live-folders.log'), 'live-folders.log'],
|
|
122
|
+
];
|
|
123
|
+
for (const [source, name] of tails) {
|
|
124
|
+
const text = readBounded(storageRoot, source, TAIL_BYTES, 'tail');
|
|
125
|
+
if (text)
|
|
126
|
+
fs.writeFileSync(path.join(target, name), text);
|
|
127
|
+
}
|
|
128
|
+
return target;
|
|
129
|
+
}
|
|
@@ -22,6 +22,10 @@ export declare function findingLines(events: readonly StopEvent[]): string[];
|
|
|
22
22
|
export declare function findDumps(dirs: readonly string[], since: Date): DumpFile[];
|
|
23
23
|
/** Whether the newest launch started at or before `lastSeen` had vGPU on (host-only .wsb in <storage>\launches). */
|
|
24
24
|
export declare function launchHadVgpu(storageRoot: string, lastSeen: string): boolean;
|
|
25
|
+
/**
|
|
26
|
+
* "This PC still had ..." from the last line of its memory log, when RAM and commit were both 4 GB or more free (pure).
|
|
27
|
+
*/
|
|
28
|
+
export declare function memoryWasFineLine(hostMemoryLog: string): string | null;
|
|
25
29
|
/**
|
|
26
30
|
* The report text (pure): what happened (the sandbox's or this PC's stop code, where the dump is) and the memory log's
|
|
27
31
|
* last lines. `memoryLog` is the sandbox's own file, so it is shown through safeText.
|
|
@@ -54,7 +54,10 @@ export function findingLines(events) {
|
|
|
54
54
|
lines.push('WHEA-Logger: Windows reported a hardware error (CPU, memory, a bus or a disk): heat or failing hardware.');
|
|
55
55
|
if (has((e) => lower(e).startsWith('hyper-v')))
|
|
56
56
|
lines.push("Hyper-V events: the sandbox's virtual machine (or the service that runs it) reported a problem.");
|
|
57
|
-
|
|
57
|
+
const isAppCrash = (e) => /^(application error|windows error reporting)$/i.test(e.provider);
|
|
58
|
+
if (has((e) => isAppCrash(e) && /vmwp\.exe/i.test(e.process)))
|
|
59
|
+
lines.push("vmwp.exe, the Windows program that runs the sandbox's VM, crashed. That is a fault in Windows' Hyper-V code or a driver it uses (with vGPU on, usually the graphics driver), not a program inside the sandbox running out of memory.");
|
|
60
|
+
else if (has(isAppCrash))
|
|
58
61
|
lines.push("A crash report names vmwp.exe or vmmem, the programs that hold the sandbox's VM.");
|
|
59
62
|
return lines;
|
|
60
63
|
}
|
|
@@ -122,6 +125,30 @@ function hostMemoryTail(storageRoot, dataPath) {
|
|
|
122
125
|
return '';
|
|
123
126
|
}
|
|
124
127
|
}
|
|
128
|
+
/**
|
|
129
|
+
* "This PC still had ..." from the last line of its memory log, when RAM and commit were both 4 GB or more free (pure).
|
|
130
|
+
*/
|
|
131
|
+
export function memoryWasFineLine(hostMemoryLog) {
|
|
132
|
+
const last = hostMemoryLog
|
|
133
|
+
.split(/\r?\n/)
|
|
134
|
+
.filter((l) => l.trim() !== '')
|
|
135
|
+
.pop();
|
|
136
|
+
if (!last)
|
|
137
|
+
return null;
|
|
138
|
+
const ram = /RAM free\/total MB (\d{1,9})\/(\d{1,9})/.exec(last);
|
|
139
|
+
const commit = /commit free\/limit MB (\d{1,9})\/(\d{1,9})/.exec(last);
|
|
140
|
+
if (!ram || !commit)
|
|
141
|
+
return null;
|
|
142
|
+
const freeRam = Number(ram[1]);
|
|
143
|
+
const freeCommit = Number(commit[1]);
|
|
144
|
+
if (freeRam < 4096 || freeCommit < 4096)
|
|
145
|
+
return null;
|
|
146
|
+
return ('This PC still had ' +
|
|
147
|
+
Math.floor(freeRam / 1024) +
|
|
148
|
+
' GB of RAM and ' +
|
|
149
|
+
Math.floor(freeCommit / 1024) +
|
|
150
|
+
' GB of memory (RAM plus paging file) free just before, so it did not run out of memory.');
|
|
151
|
+
}
|
|
125
152
|
/**
|
|
126
153
|
* The report text (pure): what happened (the sandbox's or this PC's stop code, where the dump is) and the memory log's
|
|
127
154
|
* last lines. `memoryLog` is the sandbox's own file, so it is shown through safeText.
|
|
@@ -152,6 +179,7 @@ export function crashReportText(cause, events, memoryLog, lastSeen, extra = {})
|
|
|
152
179
|
.map((l) => ' ' + safeText(l));
|
|
153
180
|
const freeze = freezeLine(cause, lastSeen);
|
|
154
181
|
const meaning = findingLines(unique);
|
|
182
|
+
const fine = memoryWasFineLine(extra.hostMemoryLog ?? '');
|
|
155
183
|
const dumps = extra.dumps ?? [];
|
|
156
184
|
const vgpu = extra.vgpu ? ['vGPU was on for this sandbox. ' + vgpuWarning('its launch settings'), ''] : [];
|
|
157
185
|
return [
|
|
@@ -165,6 +193,7 @@ export function crashReportText(cause, events, memoryLog, lastSeen, extra = {})
|
|
|
165
193
|
...vgpu,
|
|
166
194
|
'What that usually means:',
|
|
167
195
|
...(meaning.length ? meaning.map((l) => ' ' + l) : [' (no memory, blue screen or hardware event was logged)']),
|
|
196
|
+
...(fine ? [' ' + fine] : []),
|
|
168
197
|
'',
|
|
169
198
|
'Crash dumps Windows left (C:\\Windows\\Minidump, MEMORY.DMP):',
|
|
170
199
|
...(dumps.length ? dumps.map((d) => ' ' + d.name + ' ' + d.at) : [' (none from that time)']),
|
|
@@ -39,6 +39,7 @@ export function heartbeatLine(hb) {
|
|
|
39
39
|
hb.step +
|
|
40
40
|
(hb.hostStep > 0 ? ' (this PC asked for step ' + hb.hostStep + ')' : '') +
|
|
41
41
|
(hb.diskQueueReported !== undefined ? ', this PC disk queue ' + hb.diskQueueReported : '') +
|
|
42
|
+
(hb.held ? ', ' + hb.held + ' builds held by the memory gate' : '') +
|
|
42
43
|
(top ? '; biggest: ' + top : ''));
|
|
43
44
|
}
|
|
44
45
|
/** The likeliest reason, from the last heartbeat (pure). */
|
|
@@ -63,7 +63,7 @@ export function assertCanLaunch(owner) {
|
|
|
63
63
|
? other + ' (Windows allows one per PC).'
|
|
64
64
|
: 'A Windows Sandbox is already running (Windows allows one at a time).', other
|
|
65
65
|
? 'Use it with sandbox attach if it is shared with you, or start yours after it stops.'
|
|
66
|
-
: 'Attach to it with sandbox claude -t, show it with
|
|
66
|
+
: 'Attach to it with sandbox claude -t, show it with sandbox show, or shut it down with sandbox stop.');
|
|
67
67
|
}
|
|
68
68
|
}
|
|
69
69
|
/** cmd.exe expands %VAR% and ^ even inside quotes, and a quote ends them: such a path could run something else. */
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Headless mode: Windows Sandbox always opens its client window, so we hide it (ShowWindow SW_HIDE) as soon as it
|
|
2
|
-
// appears. The VM and the logged-in session keep running; `
|
|
2
|
+
// appears. The VM and the logged-in session keep running; `sandbox show` brings the window back. The work is done by
|
|
3
3
|
// HostTools.exe (native\csharp\HostTools\Window.cs), else by native\scripts\*-window.ps1 with the same C#.
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import { spawn, spawnSync } from 'node:child_process';
|
|
@@ -19,10 +19,13 @@ declare const TerminalSettingsSchema: z.ZodObject<{
|
|
|
19
19
|
export declare function withPwshProfile(settings: z.output<typeof TerminalSettingsSchema>, visible: boolean): z.output<typeof TerminalSettingsSchema> | null;
|
|
20
20
|
/** settings.json that makes new tabs read the current environment; null when it already does. Pure. */
|
|
21
21
|
export declare function withReloadEnvironment(settings: z.output<typeof TerminalSettingsSchema>): z.output<typeof TerminalSettingsSchema> | null;
|
|
22
|
+
/** settings.json that copies on select; null when the user already chose (true or false). Pure. */
|
|
23
|
+
export declare function withCopyOnSelect(settings: z.output<typeof TerminalSettingsSchema>): z.output<typeof TerminalSettingsSchema> | null;
|
|
22
24
|
/**
|
|
23
25
|
* Shows the PowerShell 7 profile once pwsh is installed in this data folder (and `allowed`, i.e. toolchains.pwsh is
|
|
24
26
|
* not off), hides it otherwise. settings.json is written by the sandbox too: read with a size cap, through no links,
|
|
25
|
-
* shape-checked, only rewritten when the profile changes
|
|
27
|
+
* shape-checked, only rewritten when the profile changes or a default switch is missing (reload the environment,
|
|
28
|
+
* copy on select; a value the user set stays); a file that does not parse is left alone.
|
|
26
29
|
*/
|
|
27
30
|
export declare function syncPwshProfile(dataPath: string, allowed: boolean): void;
|
|
28
31
|
export declare function ensureTerminal(dataPath: string, refresh: boolean, options?: {
|
|
@@ -24,6 +24,9 @@ const SETTINGS = {
|
|
|
24
24
|
theme: 'dark',
|
|
25
25
|
firstWindowPreference: 'defaultProfile',
|
|
26
26
|
confirmCloseAllTabs: false,
|
|
27
|
+
// Copied the moment the mouse is released, so an agent's redraw cannot clear the selection first.
|
|
28
|
+
copyOnSelect: true,
|
|
29
|
+
copyFormatting: 'none',
|
|
27
30
|
// New tabs pick up the PATH and variables the runner writes machine-wide after the window opened.
|
|
28
31
|
compatibility: { reloadEnvironmentVariables: true },
|
|
29
32
|
warning: { confirmCloseAllTabs: false, largePaste: false, multiLinePaste: false },
|
|
@@ -75,10 +78,17 @@ export function withReloadEnvironment(settings) {
|
|
|
75
78
|
return null;
|
|
76
79
|
return { ...settings, compatibility: { ...compatibility, reloadEnvironmentVariables: true } };
|
|
77
80
|
}
|
|
81
|
+
/** settings.json that copies on select; null when the user already chose (true or false). Pure. */
|
|
82
|
+
export function withCopyOnSelect(settings) {
|
|
83
|
+
if ('copyOnSelect' in settings)
|
|
84
|
+
return null;
|
|
85
|
+
return { ...settings, copyOnSelect: true };
|
|
86
|
+
}
|
|
78
87
|
/**
|
|
79
88
|
* Shows the PowerShell 7 profile once pwsh is installed in this data folder (and `allowed`, i.e. toolchains.pwsh is
|
|
80
89
|
* not off), hides it otherwise. settings.json is written by the sandbox too: read with a size cap, through no links,
|
|
81
|
-
* shape-checked, only rewritten when the profile changes
|
|
90
|
+
* shape-checked, only rewritten when the profile changes or a default switch is missing (reload the environment,
|
|
91
|
+
* copy on select; a value the user set stays); a file that does not parse is left alone.
|
|
82
92
|
*/
|
|
83
93
|
export function syncPwshProfile(dataPath, allowed) {
|
|
84
94
|
const file = path.join(terminalDir(dataPath), 'settings', 'settings.json');
|
|
@@ -100,16 +110,22 @@ export function syncPwshProfile(dataPath, allowed) {
|
|
|
100
110
|
}
|
|
101
111
|
const withProfile = withPwshProfile(parsed, visible);
|
|
102
112
|
if (withProfile) {
|
|
103
|
-
|
|
113
|
+
const reloaded = withReloadEnvironment(withProfile) ?? withProfile;
|
|
114
|
+
writeJson(file, withCopyOnSelect(reloaded) ?? reloaded);
|
|
104
115
|
return;
|
|
105
116
|
}
|
|
106
|
-
// Only
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
117
|
+
// Only default switches are missing: set those values in the text, so comments and the layout stay.
|
|
118
|
+
const missing = [];
|
|
119
|
+
if (withReloadEnvironment(parsed))
|
|
120
|
+
missing.push([['compatibility', 'reloadEnvironmentVariables'], true]);
|
|
121
|
+
if (withCopyOnSelect(parsed))
|
|
122
|
+
missing.push([['copyOnSelect'], true]);
|
|
123
|
+
if (missing.length === 0)
|
|
124
|
+
return;
|
|
125
|
+
let edited = text;
|
|
126
|
+
for (const [at, value] of missing)
|
|
127
|
+
edited = applyEdits(edited, modify(edited, at, value, { formattingOptions: { insertSpaces: true, tabSize: 4 } }));
|
|
128
|
+
writeFileAtomic(file, edited);
|
|
113
129
|
}
|
|
114
130
|
export async function ensureTerminal(dataPath, refresh, options = {}) {
|
|
115
131
|
const result = await installTerminal(dataPath, refresh);
|
package/dist/runtime/agent.js
CHANGED
|
@@ -25502,7 +25502,7 @@ var SandboxSchema = external_exports.strictObject({
|
|
|
25502
25502
|
'The last resort that keeps the sandbox up, in MB below its commit limit (RAM plus paging file): builds are slowed down long before (sandbox.pressureResponse); only if everything the agents start together would take the sandbox to the very edge of its memory, where Windows inside it would start failing, is a build refused memory instead of Windows. "auto" = the larger of 1536 MB and 3 % of the commit limit; "none" = no backstop. Yours only: a project file cannot change it.'
|
|
25503
25503
|
),
|
|
25504
25504
|
pressureResponse: external_exports.boolean().describe(
|
|
25505
|
-
"When the sandbox (or this PC) runs low on memory, slow builds down in steps, like a PC that gets slower instead of failing: build programs get lower CPU and memory priority, then less CPU, then compilers take turns (the waiting ones briefly suspended and trimmed to the paging file). Nothing is stopped or killed, and the agents themselves are never touched. Yours only: a project file cannot change it."
|
|
25505
|
+
"When the sandbox (or this PC) runs low on memory, slow builds down in steps, like a PC that gets slower instead of failing: build programs get lower CPU and memory priority, then less CPU, then compilers take turns (the waiting ones briefly suspended and trimmed to the paging file). Nothing is stopped or killed, and the agents themselves are never touched. A memory gate also holds new compilers and linkers for a moment when the memory of the build programs grows faster than 4 GB in 3 seconds (3 GB with vGPU), and lets them go one at a time. Yours only: a project file cannot change it."
|
|
25506
25506
|
),
|
|
25507
25507
|
vGpu: external_exports.boolean().describe(
|
|
25508
25508
|
"Virtual GPU. Off by default: Microsoft notes it widens the sandbox attack surface. The sandbox then uses this PC's graphics card, and with some graphics drivers heavy use can freeze the whole PC: turn it on only when a program in the sandbox needs the GPU (the mesa toolchain gives software OpenGL without it). Browsers in the sandbox never use it (they draw in software)."
|
|
@@ -25516,7 +25516,7 @@ var SandboxSchema = external_exports.strictObject({
|
|
|
25516
25516
|
printerRedirection: external_exports.boolean().describe("Printers from this PC."),
|
|
25517
25517
|
protectedClient: external_exports.boolean().describe("Run the sandbox window in Protected Client mode (recommended)."),
|
|
25518
25518
|
headless: external_exports.boolean().describe(
|
|
25519
|
-
|
|
25519
|
+
'Hide the sandbox window ("Show the window" brings it back). A headless sandbox shuts down when its last session ends.'
|
|
25520
25520
|
),
|
|
25521
25521
|
keepAwake: external_exports.boolean().describe(
|
|
25522
25522
|
"Keep this PC from sleeping while a sandbox runs, so agents left working overnight keep going (the screen may still turn off; closing a laptop lid still sleeps it)."
|
|
@@ -27700,6 +27700,8 @@ var PressureStatusSchema = external_exports.object({
|
|
|
27700
27700
|
lowered: external_exports.number().int().min(0).max(1024),
|
|
27701
27701
|
/** Compilers waiting their turn right now (name, process id, seconds until they may go on). */
|
|
27702
27702
|
paused: external_exports.array(external_exports.object({ name: Text(64), pid: external_exports.number().int(), seconds: external_exports.number().int().min(0).max(60) })).max(8),
|
|
27703
|
+
/** Build programs the memory gate holds right now (let go one at a time). */
|
|
27704
|
+
held: external_exports.number().int().min(0).max(1024).optional(),
|
|
27703
27705
|
/** The CPU share the compilers get together (percent; 100 = no cap). */
|
|
27704
27706
|
cpuPercent: external_exports.number().int().min(0).max(100).optional(),
|
|
27705
27707
|
/** The PC itself asked for slowing down (its memory or CPU is low). */
|
|
@@ -27720,6 +27722,8 @@ var HeartbeatSchema = external_exports.object({
|
|
|
27720
27722
|
topProcesses: external_exports.array(external_exports.object({ name: Text(64), privateMb: external_exports.number().int().min(0).max(1e7) })).max(5),
|
|
27721
27723
|
/** The disk queue this PC last reported while asking the sandbox to slow down. */
|
|
27722
27724
|
diskQueueReported: external_exports.number().min(0).max(1e5).optional(),
|
|
27725
|
+
/** Build programs the memory gate holds right now (let go one at a time). */
|
|
27726
|
+
held: external_exports.number().int().min(0).max(1024).optional(),
|
|
27723
27727
|
/** "ending": the sandbox is shutting down in order (idle shutdown, sandbox stop, Windows shutting down). */
|
|
27724
27728
|
phase: external_exports.enum(["ready", "working", "ending"])
|
|
27725
27729
|
});
|