windows-development-sandbox 1.1.0 → 1.4.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 +3 -2
- package/config.example.json +38 -5
- package/dist/cli/args.d.ts +3 -0
- package/dist/cli/args.js +3 -1
- package/dist/cli/commands/accounts.js +2 -2
- package/dist/cli/commands/agent-commands.js +1 -1
- package/dist/cli/commands/context.js +2 -1
- package/dist/cli/commands/data/clean.js +6 -1
- package/dist/cli/commands/data/collect.js +3 -3
- package/dist/cli/commands/index.d.ts +1 -1
- package/dist/cli/commands/index.js +1 -1
- package/dist/cli/commands/launch.js +36 -3
- package/dist/cli/commands/maintenance.d.ts +2 -0
- package/dist/cli/commands/maintenance.js +35 -2
- package/dist/cli/commands/sessions.js +3 -3
- package/dist/cli/commands/user-config.js +1 -1
- package/dist/cli/index.js +6 -4
- package/dist/cli/manifest.js +5 -0
- package/dist/cli/menu/apps.d.ts +21 -0
- package/dist/cli/menu/apps.js +122 -0
- package/dist/cli/menu/command-form.d.ts +28 -0
- package/dist/cli/menu/command-form.js +219 -0
- package/dist/cli/menu/index.d.ts +26 -0
- package/dist/cli/menu/index.js +180 -0
- package/dist/cli/menu/settings/categories.d.ts +9 -0
- package/dist/cli/menu/settings/categories.js +28 -0
- package/dist/cli/menu/settings/config-file.d.ts +40 -0
- package/dist/cli/menu/settings/config-file.js +71 -0
- package/dist/cli/menu/settings/editors.d.ts +30 -0
- package/dist/cli/menu/settings/editors.js +305 -0
- package/dist/cli/menu/settings/index.d.ts +32 -0
- package/dist/cli/menu/settings/index.js +136 -0
- package/dist/cli/menu/settings/schema-node.d.ts +62 -0
- package/dist/cli/menu/settings/schema-node.js +104 -0
- package/dist/cli/{menu-multi.d.ts → menu/several.d.ts} +10 -2
- package/dist/cli/menu/several.js +102 -0
- package/dist/cli/tui/app.d.ts +39 -0
- package/dist/cli/tui/app.js +52 -0
- package/dist/cli/tui/dialogs.d.ts +57 -0
- package/dist/cli/tui/dialogs.js +162 -0
- package/dist/cli/tui/frame.d.ts +18 -0
- package/dist/cli/tui/frame.js +27 -0
- package/dist/cli/tui/index.d.ts +7 -0
- package/dist/cli/tui/index.js +8 -0
- package/dist/cli/tui/list-screen.d.ts +101 -0
- package/dist/cli/tui/list-screen.js +299 -0
- package/dist/cli/tui/terminal.d.ts +37 -0
- package/dist/cli/tui/terminal.js +89 -0
- package/dist/cli/tui/text.d.ts +20 -0
- package/dist/cli/tui/text.js +105 -0
- package/dist/cli/tui/theme.d.ts +45 -0
- package/dist/cli/tui/theme.js +30 -0
- package/dist/core/config.js +9 -6
- package/dist/core/local-config.d.ts +15 -0
- package/dist/core/local-config.js +63 -4
- package/dist/core/schema/apps.d.ts +40 -0
- package/dist/core/schema/apps.js +27 -0
- package/dist/core/schema/config.d.ts +66 -1
- package/dist/core/schema/config.js +18 -6
- package/dist/core/schema/profile.d.ts +5 -0
- package/dist/core/schema/profile.js +4 -2
- package/dist/core/schema/sandbox.d.ts +28 -0
- package/dist/core/schema/sandbox.js +28 -0
- package/dist/core/schema/toolchains.d.ts +3 -0
- package/dist/core/schema/toolchains.js +6 -1
- package/dist/core/types.d.ts +6 -0
- package/dist/core/util.d.ts +2 -0
- package/dist/core/util.js +2 -0
- package/dist/core/workspace.js +22 -5
- package/dist/guest/apps/index.d.ts +10 -0
- package/dist/guest/apps/index.js +206 -0
- package/dist/guest/apps/resolvers.d.ts +8 -0
- package/dist/guest/apps/resolvers.js +90 -0
- package/dist/guest/audit-parse.d.ts +7 -0
- package/dist/guest/audit-parse.js +24 -0
- package/dist/guest/audit.d.ts +4 -0
- package/dist/guest/audit.js +47 -0
- package/dist/guest/daemon/relays.d.ts +1 -0
- package/dist/guest/daemon/relays.js +18 -0
- package/dist/guest/daemon.js +4 -0
- package/dist/guest/env.d.ts +1 -0
- package/dist/guest/env.js +3 -1
- package/dist/guest/helpers.d.ts +1 -1
- package/dist/guest/main.js +15 -2
- package/dist/guest/netproxy.d.ts +1 -0
- package/dist/guest/netproxy.js +100 -0
- package/dist/guest/ptyhost.js +14 -4
- package/dist/guest/setup/claude.d.ts +0 -4
- package/dist/guest/setup/claude.js +48 -2
- package/dist/guest/setup/network.d.ts +9 -0
- package/dist/guest/setup/network.js +97 -4
- package/dist/guest/setup/user.js +4 -0
- package/dist/guest/toolchains/native/mesa.js +13 -3
- package/dist/guest/toolchains/plan.d.ts +3 -1
- package/dist/guest/toolchains/plan.js +53 -9
- package/dist/guest/toolchains/python.js +26 -2
- package/dist/guest/toolchains/vscode-profile.d.ts +30 -0
- package/dist/guest/toolchains/vscode-profile.js +69 -0
- package/dist/guest/toolchains/vscode.d.ts +17 -0
- package/dist/guest/toolchains/vscode.js +87 -1
- package/dist/host/attach.js +2 -1
- package/dist/host/audit/collect.d.ts +34 -0
- package/dist/host/audit/collect.js +126 -0
- package/dist/host/audit/log.d.ts +46 -0
- package/dist/host/audit/log.js +143 -0
- package/dist/host/generate.d.ts +6 -0
- package/dist/host/generate.js +10 -0
- package/dist/host/launch/mappings.js +5 -1
- package/dist/host/launch/stage.js +1 -1
- package/dist/host/motw.d.ts +17 -0
- package/dist/host/motw.js +74 -0
- package/dist/host/platform.js +3 -0
- package/dist/host/port-forward.d.ts +8 -0
- package/dist/host/port-forward.js +37 -0
- package/dist/host/repo-guard.d.ts +10 -3
- package/dist/host/repo-guard.js +142 -14
- package/dist/host/stops.d.ts +67 -0
- package/dist/host/stops.js +126 -0
- package/dist/host/terminal.d.ts +8 -0
- package/dist/host/terminal.js +45 -1
- package/dist/host/workspace-view/index.d.ts +20 -0
- package/dist/host/workspace-view/index.js +53 -0
- package/dist/host/workspace-view/rules.d.ts +26 -0
- package/dist/host/workspace-view/rules.js +93 -0
- package/dist/host/workspace-view/view.d.ts +50 -0
- package/dist/host/workspace-view/view.js +281 -0
- package/dist/runtime/daemon.js +1319 -552
- package/dist/runtime/db.js +168 -13
- package/dist/runtime/main.js +2898 -608
- package/dist/runtime/netproxy.js +25936 -0
- package/dist/runtime/view.js +272 -23
- package/dist/selftest/audit.test.d.ts +2 -0
- package/dist/selftest/audit.test.js +57 -0
- package/dist/selftest/channel.test.js +7 -0
- package/dist/selftest/cli.test.js +47 -17
- package/dist/selftest/fixtures.d.ts +45 -0
- package/dist/selftest/fixtures.js +3 -1
- package/dist/selftest/generate.test.js +7 -2
- package/dist/selftest/index.js +5 -2
- package/dist/selftest/live-network.test.d.ts +2 -0
- package/dist/selftest/live-network.test.js +63 -0
- package/dist/selftest/local-config.test.js +16 -3
- package/dist/selftest/network.test.js +12 -0
- package/dist/selftest/projects.test.js +1 -1
- package/dist/selftest/repo-guard.test.js +59 -1
- package/dist/selftest/sync.test.js +50 -10
- package/dist/selftest/toolchains.test.js +30 -0
- package/dist/selftest/workspace-view.test.d.ts +2 -0
- package/dist/selftest/workspace-view.test.js +89 -0
- package/dist/shared/apps.d.ts +49 -0
- package/dist/shared/apps.js +114 -0
- package/dist/shared/netpolicy.d.ts +27 -0
- package/dist/shared/netpolicy.js +51 -0
- package/dist/shared/pty-client.d.ts +9 -4
- package/dist/shared/pty-client.js +139 -17
- package/dist/shared/rundir.d.ts +2 -0
- package/dist/shared/rundir.js +2 -0
- package/dist/shared/schema.d.ts +10 -0
- package/dist/shared/schema.js +2 -0
- package/dist/shared/system.d.ts +1 -0
- package/dist/shared/system.js +1 -0
- package/dist/sync/claude-mem.js +3 -2
- package/dist/sync/history.d.ts +6 -0
- package/dist/sync/history.js +26 -11
- package/dist/sync/watcher.js +78 -6
- package/package.json +2 -1
- package/sandbox/cargoshim.cs +56 -0
- package/sandbox/cpucap.cs +33 -0
- package/sandbox/cpucap.ps1 +4 -35
- package/sandbox/logon.cmd +6 -0
- package/sandbox/memlog.ps1 +34 -0
- package/sandbox/pagefile.cs +31 -0
- package/sandbox/pagefile.ps1 +21 -0
- package/sandbox/ptyhost.cs +105 -7
- package/schemas/config.schema.json +303 -2
- package/schemas/sandbox-config.schema.json +126 -12
- package/dist/cli/menu-multi.js +0 -82
- package/dist/cli/menu.d.ts +0 -14
- package/dist/cli/menu.js +0 -290
- package/dist/cli/settings/edit.d.ts +0 -13
- package/dist/cli/settings/edit.js +0 -19
- package/dist/cli/settings/index.d.ts +0 -8
- package/dist/cli/settings/index.js +0 -131
- package/dist/cli/settings/prompts.d.ts +0 -7
- package/dist/cli/settings/prompts.js +0 -48
- package/dist/cli/settings/schema.d.ts +0 -33
- package/dist/cli/settings/schema.js +0 -29
package/dist/shared/schema.d.ts
CHANGED
|
@@ -19,6 +19,7 @@ export declare const SandboxStatus: z.ZodObject<{
|
|
|
19
19
|
workspaceHost: z.ZodString;
|
|
20
20
|
guestWorkspace: z.ZodString;
|
|
21
21
|
headless: z.ZodBoolean;
|
|
22
|
+
addresses: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
22
23
|
}, z.core.$strip>;
|
|
23
24
|
/**
|
|
24
25
|
* A project the host registered after boot (sandbox claude from another folder): everything the guest needs to
|
|
@@ -118,6 +119,9 @@ export declare const ProjectSpec: z.ZodObject<{
|
|
|
118
119
|
}, z.core.$strict>;
|
|
119
120
|
vscode: z.ZodObject<{
|
|
120
121
|
enabled: z.ZodBoolean;
|
|
122
|
+
setup: z.ZodBoolean;
|
|
123
|
+
recommended: z.ZodBoolean;
|
|
124
|
+
extensions: z.ZodArray<z.ZodString>;
|
|
121
125
|
}, z.core.$strict>;
|
|
122
126
|
environment: z.ZodRecord<z.ZodString, z.ZodString>;
|
|
123
127
|
}, z.core.$strict>;
|
|
@@ -259,6 +263,9 @@ export declare const GuestRequest: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
|
259
263
|
}, z.core.$strict>;
|
|
260
264
|
vscode: z.ZodObject<{
|
|
261
265
|
enabled: z.ZodBoolean;
|
|
266
|
+
setup: z.ZodBoolean;
|
|
267
|
+
recommended: z.ZodBoolean;
|
|
268
|
+
extensions: z.ZodArray<z.ZodString>;
|
|
262
269
|
}, z.core.$strict>;
|
|
263
270
|
environment: z.ZodRecord<z.ZodString, z.ZodString>;
|
|
264
271
|
}, z.core.$strict>;
|
|
@@ -408,6 +415,9 @@ export declare const GuestRequest: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
|
408
415
|
}, z.core.$strict>;
|
|
409
416
|
vscode: z.ZodObject<{
|
|
410
417
|
enabled: z.ZodBoolean;
|
|
418
|
+
setup: z.ZodBoolean;
|
|
419
|
+
recommended: z.ZodBoolean;
|
|
420
|
+
extensions: z.ZodArray<z.ZodString>;
|
|
411
421
|
}, z.core.$strict>;
|
|
412
422
|
environment: z.ZodRecord<z.ZodString, z.ZodString>;
|
|
413
423
|
}, z.core.$strict>;
|
package/dist/shared/schema.js
CHANGED
|
@@ -19,6 +19,8 @@ export const SandboxStatus = z.object({
|
|
|
19
19
|
workspaceHost: Text(),
|
|
20
20
|
guestWorkspace: Text(),
|
|
21
21
|
headless: z.boolean(),
|
|
22
|
+
/** The sandbox's IPv4 addresses (for network.forwardPorts; the host only uses ones on its Hyper-V network). */
|
|
23
|
+
addresses: z.array(z.string().max(45)).max(8).optional(),
|
|
22
24
|
});
|
|
23
25
|
const Version = z.strictObject({ spec: Text(128), source: Text(256) });
|
|
24
26
|
const DatabasePlan = z.strictObject({
|
package/dist/shared/system.d.ts
CHANGED
package/dist/shared/system.js
CHANGED
package/dist/sync/claude-mem.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// claude-mem data (~/.claude-mem) between this PC and the sandbox's persistent store.
|
|
2
2
|
//
|
|
3
3
|
// IN (this PC -> sandbox): the whole folder is mirrored; nothing flows to your PC in that direction.
|
|
4
|
-
// OUT (sandbox -> this PC): ONLY the SQLite database files come back. Never settings, never the Chroma vector store:
|
|
4
|
+
// OUT (sandbox -> this PC), only with claudeProfile.syncClaudeMemBack: ONLY the SQLite database files come back. Never settings, never the Chroma vector store:
|
|
5
5
|
// Chroma keeps Python pickle files, and a crafted pickle runs code when loaded, so copying it back would let a
|
|
6
6
|
// compromised sandbox execute code on your PC. claude-mem rebuilds its vector index from the database by itself.
|
|
7
7
|
import fs from 'node:fs';
|
|
@@ -10,7 +10,8 @@ import _ from 'lodash';
|
|
|
10
10
|
import { dirExists, homeDir, isLink } from '../core/util.js';
|
|
11
11
|
import { safeRemove } from '../shared/safe-remove.js';
|
|
12
12
|
import { assertTestWritable } from '../host/test-guard.js';
|
|
13
|
-
|
|
13
|
+
// Never in: its settings (provider API keys), .env files and backups; nor run-time files that mean nothing elsewhere.
|
|
14
|
+
const SKIP_IN = /(^logs$|^backups?$|^settings\.json$|^\.env|\.pid$|\.port$|\.lock$|^worker.*\.json$|\.incoming$|\.conflict-\d+$)/i;
|
|
14
15
|
/** The only files that may come back from the sandbox. */
|
|
15
16
|
const DATABASE = /^claude-mem\.db(-wal|-shm)?$/i;
|
|
16
17
|
const MAX_DB_BYTES = 2 * 1024 * 1024 * 1024;
|
package/dist/sync/history.d.ts
CHANGED
|
@@ -7,6 +7,11 @@ declare const SyncStateSchema: z.ZodObject<{
|
|
|
7
7
|
guestWorkspace: z.ZodString;
|
|
8
8
|
syncHistory: z.ZodOptional<z.ZodBoolean>;
|
|
9
9
|
syncClaudeMem: z.ZodOptional<z.ZodBoolean>;
|
|
10
|
+
syncClaudeMemBack: z.ZodOptional<z.ZodBoolean>;
|
|
11
|
+
historyScope: z.ZodOptional<z.ZodEnum<{
|
|
12
|
+
project: "project";
|
|
13
|
+
all: "all";
|
|
14
|
+
}>>;
|
|
10
15
|
memHostStamp: z.ZodOptional<z.ZodNumber>;
|
|
11
16
|
memGuestStamp: z.ZodOptional<z.ZodNumber>;
|
|
12
17
|
projects: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
@@ -28,6 +33,7 @@ export interface SyncPaths extends WorkspacePair {
|
|
|
28
33
|
/** Brings this PC's history into the sandbox state (run before the sandbox starts). Returns files copied. */
|
|
29
34
|
export declare const syncHistoryIn: (p: SyncPaths, options?: {
|
|
30
35
|
promptHistory?: boolean;
|
|
36
|
+
scope?: "project" | "all";
|
|
31
37
|
}) => number;
|
|
32
38
|
/** Copies new sandbox history back to this PC (safe while the sandbox runs: transcripts are append-only). */
|
|
33
39
|
export declare const syncHistoryOut: (p: SyncPaths) => number;
|
package/dist/sync/history.js
CHANGED
|
@@ -7,8 +7,11 @@
|
|
|
7
7
|
//
|
|
8
8
|
// Safety (the sandbox side is untrusted):
|
|
9
9
|
// - links are never followed or written through, on either side;
|
|
10
|
-
// - what comes BACK is limited to transcripts (.jsonl)
|
|
11
|
-
// size, and never Claude's memory folders.
|
|
10
|
+
// - what comes BACK is limited to the sandbox's own projects: their transcripts (.jsonl) and prompt-history lines,
|
|
11
|
+
// plus to-do lists (.json), each capped in size, and never Claude's memory folders. A sandbox cannot plant a
|
|
12
|
+
// conversation for another project on this PC (claude --resume there would load it);
|
|
13
|
+
// - what goes IN is, by default (historyScope "project"), only the sandbox's own projects: other projects'
|
|
14
|
+
// conversations, file backups and prompts are not the agent's business.
|
|
12
15
|
import fs from 'node:fs';
|
|
13
16
|
import path from 'node:path';
|
|
14
17
|
import _ from 'lodash';
|
|
@@ -25,7 +28,7 @@ const MAX_FILE_BYTES = 100 * 1024 * 1024;
|
|
|
25
28
|
const AbsolutePath = z.string().max(1024).refine((p) => path.isAbsolute(p), 'must be an absolute path');
|
|
26
29
|
const SyncStateSchema = z.object({
|
|
27
30
|
hostClaudeDir: AbsolutePath, hostWorkspace: AbsolutePath, guestWorkspace: z.string().max(1024),
|
|
28
|
-
syncHistory: z.boolean().optional(), syncClaudeMem: z.boolean().optional(),
|
|
31
|
+
syncHistory: z.boolean().optional(), syncClaudeMem: z.boolean().optional(), syncClaudeMemBack: z.boolean().optional(), historyScope: z.enum(['project', 'all']).optional(),
|
|
29
32
|
/** Newest claude-mem database time on each side right after the last sync (detects edits on both sides). */
|
|
30
33
|
memHostStamp: z.number().optional(), memGuestStamp: z.number().optional(),
|
|
31
34
|
/** Projects registered while the sandbox ran (their history goes back to their own folders). */
|
|
@@ -91,6 +94,16 @@ const isJsonObject = (line) => { try {
|
|
|
91
94
|
catch {
|
|
92
95
|
return false;
|
|
93
96
|
} };
|
|
97
|
+
/** A prompt-history line whose project is one of `workspaces` (on this PC's side). */
|
|
98
|
+
const inProjects = (line, workspaces) => {
|
|
99
|
+
try {
|
|
100
|
+
const project = JSON.parse(line).project;
|
|
101
|
+
return typeof project === 'string' && workspaces.some((w) => path.resolve(w).toLowerCase() === path.resolve(project).toLowerCase());
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
};
|
|
94
107
|
const lineTime = (line) => { try {
|
|
95
108
|
return Number(JSON.parse(line).timestamp) || 0;
|
|
96
109
|
}
|
|
@@ -102,22 +115,24 @@ const pairsOf = (p) => _.sortBy([{ hostWorkspace: p.hostWorkspace, guestWorkspac
|
|
|
102
115
|
const translateAll = (text, pairs) => pairs.reduce((out, [from, to]) => translatePaths(out, from, to), text);
|
|
103
116
|
/** Brings this PC's history into the sandbox state (run before the sandbox starts). Returns files copied. */
|
|
104
117
|
export const syncHistoryIn = (p, options = {}) => {
|
|
118
|
+
const all = options.scope === 'all';
|
|
105
119
|
assertTestWritable(p.guestClaudeDir);
|
|
106
120
|
const pairs = pairsOf(p);
|
|
107
121
|
const byHost = new Map(pairs.map((x) => [projectFolderName(x.hostWorkspace), projectFolderName(x.guestWorkspace)]));
|
|
108
122
|
const guestNames = new Set(pairs.map((x) => projectFolderName(x.guestWorkspace)));
|
|
109
123
|
const transform = (t) => translateAll(t, pairs.map((x) => [x.hostWorkspace, x.guestWorkspace]));
|
|
110
124
|
const hostProjects = path.join(p.hostClaudeDir, 'projects');
|
|
111
|
-
const names = dirExists(hostProjects) ? fs.readdirSync(hostProjects).filter((name) => isSafeFileName(name) && (!guestNames.has(name) || byHost.has(name))) : [];
|
|
125
|
+
const names = dirExists(hostProjects) ? fs.readdirSync(hostProjects).filter((name) => isSafeFileName(name) && (all ? !guestNames.has(name) || byHost.has(name) : byHost.has(name))) : [];
|
|
112
126
|
const projects = _.sumBy(names, (name) => byHost.has(name)
|
|
113
127
|
? copyNewer(path.join(hostProjects, name), path.join(p.guestClaudeDir, 'projects', byHost.get(name)), { transform })
|
|
114
128
|
: copyNewer(path.join(hostProjects, name), path.join(p.guestClaudeDir, 'projects', name)));
|
|
115
|
-
|
|
129
|
+
// To-do lists, plans and file backups are kept by session, not by project: only with historyScope "all".
|
|
130
|
+
const sessions = all ? _.sumBy(SESSION_DIRS, (dir) => copyNewer(path.join(p.hostClaudeDir, dir), path.join(p.guestClaudeDir, dir))) : 0;
|
|
116
131
|
if (options.promptHistory === false)
|
|
117
132
|
return projects + sessions;
|
|
118
133
|
// Prompt history: rebuild the sandbox copy from this PC's file plus anything only the sandbox has.
|
|
119
134
|
const target = path.join(p.guestClaudeDir, 'history.jsonl');
|
|
120
|
-
const hostLines = readLines(path.join(p.hostClaudeDir, 'history.jsonl')).map(transform);
|
|
135
|
+
const hostLines = readLines(path.join(p.hostClaudeDir, 'history.jsonl')).filter((l) => all || inProjects(l, pairs.map((x) => x.hostWorkspace))).map(transform);
|
|
121
136
|
const known = new Set(hostLines);
|
|
122
137
|
const merged = _.sortBy([...hostLines, ...readLines(target).filter((l) => !known.has(l))], lineTime);
|
|
123
138
|
if (merged.length) {
|
|
@@ -136,15 +151,15 @@ export const syncHistoryOut = (p) => {
|
|
|
136
151
|
const transform = (t) => translateAll(t, pairs);
|
|
137
152
|
const guestProjectsDir = path.join(p.guestClaudeDir, 'projects');
|
|
138
153
|
const names = dirExists(guestProjectsDir) && !isLink(guestProjectsDir) ? fs.readdirSync(guestProjectsDir).filter(isSafeFileName) : [];
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
: copyNewer(path.join(guestProjectsDir, name), path.join(p.hostClaudeDir, 'projects', name), { accept: transcript, skipDirs: NO_MEMORY }));
|
|
154
|
+
// Only this sandbox's projects: a transcript under any other folder name would be planted into that project on this PC.
|
|
155
|
+
const projects = _.sumBy(names.filter((name) => byGuest.has(name)), (name) => copyNewer(path.join(guestProjectsDir, name), path.join(p.hostClaudeDir, 'projects', byGuest.get(name)), { accept: transcript, skipDirs: NO_MEMORY, transform }));
|
|
142
156
|
// Only to-do lists come back besides transcripts; plans and file backups stay in the sandbox.
|
|
143
157
|
const sessions = copyNewer(path.join(p.guestClaudeDir, 'todos'), path.join(p.hostClaudeDir, 'todos'), { accept: json });
|
|
144
158
|
const hostFile = path.join(p.hostClaudeDir, 'history.jsonl');
|
|
145
159
|
const known = new Set(readLines(hostFile));
|
|
146
|
-
// Only prompt-history entries (one JSON object per line)
|
|
147
|
-
const
|
|
160
|
+
// Only prompt-history entries (one JSON object per line) of this sandbox's projects come back.
|
|
161
|
+
const hostWorkspaces = pairsOf(p).map((x) => x.hostWorkspace);
|
|
162
|
+
const missing = readLines(path.join(p.guestClaudeDir, 'history.jsonl')).filter(isJsonObject).map(transform).filter((l) => inProjects(l, hostWorkspaces) && !known.has(l));
|
|
148
163
|
if (missing.length) {
|
|
149
164
|
ensureDir(p.hostClaudeDir);
|
|
150
165
|
fs.appendFileSync(hostFile, missing.join('\n') + '\n', 'utf8');
|
package/dist/sync/watcher.js
CHANGED
|
@@ -9,6 +9,12 @@ import { assertNoLinks, errMsg, sleep } from '../core/util.js';
|
|
|
9
9
|
import { hostClaudeDir } from '../host/claude-profile.js';
|
|
10
10
|
import { checkRepo } from '../host/repo-guard.js';
|
|
11
11
|
import { isSandboxRunning } from '../host/platform.js';
|
|
12
|
+
import { readActiveViews, WorkspaceView } from '../host/workspace-view/index.js';
|
|
13
|
+
import { readForwards, sandboxAddress, startPortForwards } from '../host/port-forward.js';
|
|
14
|
+
import { AuditCollector, readAuditSettings } from '../host/audit/collect.js';
|
|
15
|
+
import { readMarkedProjects, saveDataSize, watchAndMark } from '../host/motw.js';
|
|
16
|
+
import { sizeOf } from '../core/disk.js';
|
|
17
|
+
import { beatLife, causeSince, endLife, keepAwake, readLife } from '../host/stops.js';
|
|
12
18
|
import { hostClaudeMemDir, syncClaudeMem } from './claude-mem.js';
|
|
13
19
|
import { readSyncState, syncHistoryIn, syncHistoryOut, writeSyncState } from './history.js';
|
|
14
20
|
const WATCH_INTERVAL_MS = 60 * 1000;
|
|
@@ -32,7 +38,7 @@ export const syncProjectIn = (dataPath, project) => {
|
|
|
32
38
|
const next = { ...state, projects };
|
|
33
39
|
writeSyncState(dataPath, next);
|
|
34
40
|
// Prompt history is left alone: sessions are appending to the sandbox's copy right now.
|
|
35
|
-
return state.syncHistory ? syncHistoryIn(toPaths(next, dataPath), { promptHistory: false }) : 0;
|
|
41
|
+
return state.syncHistory ? syncHistoryIn(toPaths(next, dataPath), { promptHistory: false, scope: state.historyScope }) : 0;
|
|
36
42
|
};
|
|
37
43
|
const memSync = (dataPath, state, direction) => {
|
|
38
44
|
assertNoLinks(dataPath, guestMem(dataPath));
|
|
@@ -47,11 +53,11 @@ export const syncBeforeLaunch = (config, dataPath, hostWorkspace) => {
|
|
|
47
53
|
const previous = readSyncState(dataPath);
|
|
48
54
|
if (previous?.syncHistory)
|
|
49
55
|
syncHistoryOut(toPaths(previous, dataPath));
|
|
50
|
-
const state = { ...previous, hostClaudeDir: hostClaudeDir(profile), hostWorkspace, guestWorkspace: config.guestWorkspace, syncHistory: profile.syncHistory, syncClaudeMem: profile.syncClaudeMem, projects: [] };
|
|
56
|
+
const state = { ...previous, hostClaudeDir: hostClaudeDir(profile), hostWorkspace, guestWorkspace: config.guestWorkspace, syncHistory: profile.syncHistory, historyScope: profile.historyScope, syncClaudeMem: profile.syncClaudeMem, syncClaudeMemBack: profile.syncClaudeMemBack, projects: [] };
|
|
51
57
|
if (profile.syncHistory)
|
|
52
|
-
summary.history = syncHistoryIn(toPaths(state, dataPath));
|
|
58
|
+
summary.history = syncHistoryIn(toPaths(state, dataPath), { scope: profile.historyScope });
|
|
53
59
|
if (profile.syncClaudeMem) {
|
|
54
|
-
if (previous?.syncClaudeMem)
|
|
60
|
+
if (previous?.syncClaudeMem && previous.syncClaudeMemBack)
|
|
55
61
|
memSync(dataPath, state, 'out');
|
|
56
62
|
summary.claudeMem = memSync(dataPath, state, 'in');
|
|
57
63
|
}
|
|
@@ -66,7 +72,7 @@ export const syncBack = (dataPath, final) => {
|
|
|
66
72
|
return summary;
|
|
67
73
|
if (state.syncHistory)
|
|
68
74
|
summary.history = syncHistoryOut(toPaths(state, dataPath));
|
|
69
|
-
if (final && state.syncClaudeMem)
|
|
75
|
+
if (final && state.syncClaudeMem && state.syncClaudeMemBack)
|
|
70
76
|
summary.claudeMem = memSync(dataPath, state, 'out');
|
|
71
77
|
writeSyncState(dataPath, state);
|
|
72
78
|
return summary;
|
|
@@ -105,8 +111,10 @@ export const watch = async (dataPath) => {
|
|
|
105
111
|
catch { /* ignore */ } };
|
|
106
112
|
const sync = (final) => {
|
|
107
113
|
try {
|
|
108
|
-
for (const finding of checkRepo(dataPath))
|
|
114
|
+
for (const finding of checkRepo(dataPath)) {
|
|
109
115
|
note('SECURITY: neutralised a git ' + finding.kind + ' added inside the sandbox: ' + finding.detail);
|
|
116
|
+
audit?.guard(finding.kind + ': ' + finding.detail);
|
|
117
|
+
}
|
|
110
118
|
}
|
|
111
119
|
catch (e) {
|
|
112
120
|
note('repo guard failed: ' + errMsg(e));
|
|
@@ -120,16 +128,80 @@ export const watch = async (dataPath) => {
|
|
|
120
128
|
note('sync failed: ' + errMsg(e));
|
|
121
129
|
}
|
|
122
130
|
};
|
|
131
|
+
// workspaceFilter: each filtered project is kept in step with its view from now until the sandbox closes.
|
|
132
|
+
// audit.enabled: programs, files, sessions and guard findings into the host-only audit log.
|
|
133
|
+
const auditSettings = readAuditSettings(storageOf(dataPath), dataPath);
|
|
134
|
+
const audit = auditSettings?.enabled ? new AuditCollector(dataPath, storageOf(dataPath), auditSettings) : null;
|
|
135
|
+
audit?.start();
|
|
136
|
+
const auditTimer = audit ? setInterval(() => { try {
|
|
137
|
+
audit.flush();
|
|
138
|
+
}
|
|
139
|
+
catch (e) {
|
|
140
|
+
note('audit: ' + errMsg(e));
|
|
141
|
+
} }, 10000) : null;
|
|
142
|
+
// storage.markSandboxFiles: programs and scripts that appear in the projects get the "from the internet" mark.
|
|
143
|
+
const stopMarking = watchAndMark(readMarkedProjects(storageOf(dataPath), dataPath), (m) => note(m));
|
|
144
|
+
// network.forwardPorts: localhost on this PC reaches servers in the sandbox while it runs.
|
|
145
|
+
const stopForwards = startPortForwards(readForwards(storageOf(dataPath), dataPath), () => sandboxAddress(dataPath), (m) => note('port forward: ' + m));
|
|
146
|
+
const views = readActiveViews(storageOf(dataPath), dataPath);
|
|
147
|
+
const stops = views.flatMap((v) => {
|
|
148
|
+
try {
|
|
149
|
+
return [new WorkspaceView({ ...v, note: (m) => note('workspace ' + v.host + ': ' + m) }).watch()];
|
|
150
|
+
}
|
|
151
|
+
catch (e) {
|
|
152
|
+
note('workspace ' + v.host + ': could not watch: ' + errMsg(e));
|
|
153
|
+
return [];
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
// sandbox.keepAwake, and the life record that tells the next launch how this run ended (host/stops.ts).
|
|
157
|
+
const awake = readLife(storageOf(dataPath), dataPath)?.keepAwake ? keepAwake(process.pid) : null;
|
|
123
158
|
try {
|
|
124
159
|
await sleep(30000);
|
|
125
160
|
while (isSandboxRunning()) {
|
|
161
|
+
beatLife(storageOf(dataPath), dataPath);
|
|
126
162
|
sync(false);
|
|
127
163
|
await sleep(WATCH_INTERVAL_MS);
|
|
128
164
|
}
|
|
165
|
+
try {
|
|
166
|
+
const life = readLife(storageOf(dataPath), dataPath);
|
|
167
|
+
const cause = life ? causeSince(life.lastSeen) : null;
|
|
168
|
+
endLife(storageOf(dataPath), dataPath, cause);
|
|
169
|
+
if (cause)
|
|
170
|
+
note('sandbox ended: ' + cause.text);
|
|
171
|
+
}
|
|
172
|
+
catch (e) {
|
|
173
|
+
note('could not record how the sandbox ended: ' + errMsg(e));
|
|
174
|
+
}
|
|
129
175
|
await sleep(3000);
|
|
130
176
|
sync(true);
|
|
131
177
|
}
|
|
132
178
|
finally {
|
|
179
|
+
awake?.kill();
|
|
180
|
+
stops.forEach((stop) => stop());
|
|
181
|
+
stopForwards();
|
|
182
|
+
stopMarking();
|
|
183
|
+
if (auditTimer)
|
|
184
|
+
clearInterval(auditTimer);
|
|
185
|
+
try {
|
|
186
|
+
audit?.stop();
|
|
187
|
+
}
|
|
188
|
+
catch (e) {
|
|
189
|
+
note('audit: ' + errMsg(e));
|
|
190
|
+
}
|
|
191
|
+
for (const v of views) {
|
|
192
|
+
try {
|
|
193
|
+
const s = new WorkspaceView(v).syncAll();
|
|
194
|
+
note('workspace ' + v.host + ': final sync ' + s.toHost + ' out, ' + s.toView + ' in');
|
|
195
|
+
}
|
|
196
|
+
catch (e) {
|
|
197
|
+
note('workspace ' + v.host + ': final sync failed: ' + errMsg(e));
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
// For the next launch's storage.warnDataGb (measuring then would be slow).
|
|
201
|
+
try {
|
|
202
|
+
saveDataSize(storageOf(dataPath), dataPath, sizeOf(dataPath));
|
|
203
|
+
}
|
|
204
|
+
catch { /* best effort */ }
|
|
133
205
|
fs.rmSync(watcherPidFile(dataPath), { force: true });
|
|
134
206
|
}
|
|
135
207
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "windows-development-sandbox",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"description": "A disposable Windows Sandbox for development, with or without an AI agent (Claude Code, Codex, Gemini, opencode, ...): the folder you run it from is the workspace, toolchains and databases for your project install themselves, and nothing else on your PC is reachable.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -72,6 +72,7 @@
|
|
|
72
72
|
"dependencies": {
|
|
73
73
|
"@inquirer/prompts": "^7.10.1",
|
|
74
74
|
"commander": "^14.0.3",
|
|
75
|
+
"ignore": "^7.0.12",
|
|
75
76
|
"jsonc-parser": "^3.3.1",
|
|
76
77
|
"lodash": "^4.18.1",
|
|
77
78
|
"zod": "^4.6.5"
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// cargo.exe stand-in for toolchains.rust.targetDir = "separate": every checkout builds into ITS OWN target\sandbox.
|
|
2
|
+
// One CARGO_TARGET_DIR for the whole sandbox made agents working in different git worktrees overwrite each other's
|
|
3
|
+
// builds (and the Claude Code Bash tool does not keep a per-call variable). The checkout is the nearest folder with a
|
|
4
|
+
// .git (a folder, or the file a worktree has); without one, the nearest folder with a Cargo.toml. A CARGO_TARGET_DIR
|
|
5
|
+
// you set yourself always wins. Compiled inside the sandbox with the .NET Framework csc.exe (C# 5).
|
|
6
|
+
using System;
|
|
7
|
+
using System.Diagnostics;
|
|
8
|
+
using System.IO;
|
|
9
|
+
|
|
10
|
+
static class CargoShim
|
|
11
|
+
{
|
|
12
|
+
static int Main()
|
|
13
|
+
{
|
|
14
|
+
string real = Environment.GetEnvironmentVariable("AGENT_REAL_CARGO");
|
|
15
|
+
if (string.IsNullOrEmpty(real) || !File.Exists(real))
|
|
16
|
+
{
|
|
17
|
+
Console.Error.WriteLine("error: the real cargo was not found (AGENT_REAL_CARGO).");
|
|
18
|
+
return 101;
|
|
19
|
+
}
|
|
20
|
+
var start = new ProcessStartInfo(real, ArgumentsOf(Environment.CommandLine)) { UseShellExecute = false };
|
|
21
|
+
if (string.IsNullOrEmpty(Environment.GetEnvironmentVariable("CARGO_TARGET_DIR")))
|
|
22
|
+
{
|
|
23
|
+
string root = CheckoutOf(Environment.CurrentDirectory);
|
|
24
|
+
if (root != null) start.EnvironmentVariables["CARGO_TARGET_DIR"] = Path.Combine(root, "target", "sandbox");
|
|
25
|
+
}
|
|
26
|
+
// Ctrl+C reaches cargo too (same console); the shim only waits for it to finish.
|
|
27
|
+
Console.CancelKeyPress += delegate(object sender, ConsoleCancelEventArgs e) { e.Cancel = true; };
|
|
28
|
+
using (Process p = Process.Start(start))
|
|
29
|
+
{
|
|
30
|
+
p.WaitForExit();
|
|
31
|
+
return p.ExitCode;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
static string CheckoutOf(string dir)
|
|
36
|
+
{
|
|
37
|
+
string cargoRoot = null;
|
|
38
|
+
for (var d = new DirectoryInfo(dir); d != null; d = d.Parent)
|
|
39
|
+
{
|
|
40
|
+
string git = Path.Combine(d.FullName, ".git");
|
|
41
|
+
if (Directory.Exists(git) || File.Exists(git)) return d.FullName;
|
|
42
|
+
if (cargoRoot == null && File.Exists(Path.Combine(d.FullName, "Cargo.toml"))) cargoRoot = d.FullName;
|
|
43
|
+
}
|
|
44
|
+
return cargoRoot;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Everything after the program name, untouched, so quoting survives exactly as the caller wrote it.
|
|
48
|
+
static string ArgumentsOf(string commandLine)
|
|
49
|
+
{
|
|
50
|
+
string s = commandLine.TrimStart();
|
|
51
|
+
int end;
|
|
52
|
+
if (s.StartsWith("\"")) { end = s.IndexOf('"', 1); end = end < 0 ? s.Length : end + 1; }
|
|
53
|
+
else { end = s.IndexOf(' '); if (end < 0) end = s.Length; }
|
|
54
|
+
return s.Substring(end).TrimStart();
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// Caps a process and everything it starts at a share of the PC's CPU. Loaded by cpucap.ps1 (Add-Type). C# 5.
|
|
2
|
+
using System;
|
|
3
|
+
using System.ComponentModel;
|
|
4
|
+
using System.Runtime.InteropServices;
|
|
5
|
+
|
|
6
|
+
public static class SandboxCpuCap {
|
|
7
|
+
// JOBOBJECT_CPU_RATE_CONTROL_INFORMATION.
|
|
8
|
+
[StructLayout(LayoutKind.Sequential)]
|
|
9
|
+
private struct CpuRate { public uint ControlFlags; public uint Rate; }
|
|
10
|
+
|
|
11
|
+
[DllImport("kernel32.dll", SetLastError = true)] private static extern IntPtr CreateJobObject(IntPtr attributes, string name);
|
|
12
|
+
[DllImport("kernel32.dll", SetLastError = true)] private static extern bool SetInformationJobObject(IntPtr job, int infoClass, ref CpuRate info, int length);
|
|
13
|
+
[DllImport("kernel32.dll", SetLastError = true)] private static extern IntPtr OpenProcess(uint access, bool inherit, int pid);
|
|
14
|
+
[DllImport("kernel32.dll", SetLastError = true)] private static extern bool AssignProcessToJobObject(IntPtr job, IntPtr process);
|
|
15
|
+
[DllImport("kernel32.dll")] private static extern bool CloseHandle(IntPtr handle);
|
|
16
|
+
|
|
17
|
+
private const int JobObjectCpuRateControlInformation = 15;
|
|
18
|
+
private const uint CpuRateEnable = 0x1, CpuRateHardCap = 0x4;
|
|
19
|
+
private const uint ProcessTerminate = 0x0001, ProcessSetQuota = 0x0100; // what AssignProcessToJobObject needs
|
|
20
|
+
|
|
21
|
+
/// <summary>Puts process pid in a new job with a hard cap of percent % of all processors. Throws on failure.</summary>
|
|
22
|
+
public static void Apply(int pid, int percent) {
|
|
23
|
+
IntPtr job = CreateJobObject(IntPtr.Zero, null);
|
|
24
|
+
if (job == IntPtr.Zero) throw new Win32Exception();
|
|
25
|
+
// The rate is in 1/100 % of all processors.
|
|
26
|
+
CpuRate rate = new CpuRate { ControlFlags = CpuRateEnable | CpuRateHardCap, Rate = (uint)(percent * 100) };
|
|
27
|
+
if (!SetInformationJobObject(job, JobObjectCpuRateControlInformation, ref rate, Marshal.SizeOf(typeof(CpuRate)))) throw new Win32Exception();
|
|
28
|
+
IntPtr process = OpenProcess(ProcessSetQuota | ProcessTerminate, false, pid);
|
|
29
|
+
if (process == IntPtr.Zero) throw new Win32Exception();
|
|
30
|
+
try { if (!AssignProcessToJobObject(job, process)) throw new Win32Exception(); } finally { CloseHandle(process); }
|
|
31
|
+
// The job's handle is left to close with this process: the job lives on while any process in it runs.
|
|
32
|
+
}
|
|
33
|
+
}
|
package/sandbox/cpucap.ps1
CHANGED
|
@@ -3,46 +3,15 @@
|
|
|
3
3
|
# every processor, so heavy builds inside can take the whole PC. This puts logon.cmd (the parent of this script),
|
|
4
4
|
# before it starts anything, into a Windows job object with a hard CPU-rate cap; everything it starts inherits it:
|
|
5
5
|
# Windows Terminal and its tabs, the boot, the agents, installs and builds. Programs started from the sandbox's Start
|
|
6
|
-
# menu are not covered. Never stops the boot:
|
|
7
|
-
param([int]$ProcessId = 0, [int]$Percent = 0)
|
|
6
|
+
# menu are not covered. Never stops the boot: it logs one line either way and the sandbox starts uncapped on failure.
|
|
7
|
+
param([int]$ProcessId = 0, [int]$Percent = 0) # 0 = the parent process / cpuLimitPercent from session.json
|
|
8
8
|
$ErrorActionPreference = 'Stop'
|
|
9
9
|
$log = 'C:\AgentData\logs\logon-hit.txt'
|
|
10
10
|
try {
|
|
11
|
-
if ($Percent -eq 0) {
|
|
12
|
-
$session = Get-Content -LiteralPath 'C:\AgentLaunch\session.json' -Raw -Encoding UTF8 | ConvertFrom-Json
|
|
13
|
-
if ($null -eq $session.cpuLimitPercent) { exit 0 }
|
|
14
|
-
$Percent = [int]$session.cpuLimitPercent
|
|
15
|
-
}
|
|
11
|
+
if ($Percent -eq 0) { $Percent = [int](Get-Content -LiteralPath 'C:\AgentLaunch\session.json' -Raw -Encoding UTF8 | ConvertFrom-Json).cpuLimitPercent }
|
|
16
12
|
if ($Percent -lt 1 -or $Percent -ge 100) { exit 0 }
|
|
17
13
|
if ($ProcessId -eq 0) { $ProcessId = [int](Get-CimInstance Win32_Process -Filter "ProcessId=$PID").ParentProcessId }
|
|
18
|
-
Add-Type -
|
|
19
|
-
using System;
|
|
20
|
-
using System.ComponentModel;
|
|
21
|
-
using System.Runtime.InteropServices;
|
|
22
|
-
public static class SandboxCpuCap {
|
|
23
|
-
[StructLayout(LayoutKind.Sequential)]
|
|
24
|
-
private struct CpuRate { public uint ControlFlags; public uint Rate; }
|
|
25
|
-
[DllImport("kernel32.dll", SetLastError = true)] private static extern IntPtr CreateJobObject(IntPtr attributes, string name);
|
|
26
|
-
[DllImport("kernel32.dll", SetLastError = true)] private static extern bool SetInformationJobObject(IntPtr job, int infoClass, ref CpuRate info, int length);
|
|
27
|
-
[DllImport("kernel32.dll", SetLastError = true)] private static extern IntPtr OpenProcess(uint access, bool inherit, int pid);
|
|
28
|
-
[DllImport("kernel32.dll", SetLastError = true)] private static extern bool AssignProcessToJobObject(IntPtr job, IntPtr process);
|
|
29
|
-
[DllImport("kernel32.dll")] private static extern bool CloseHandle(IntPtr handle);
|
|
30
|
-
// JobObjectCpuRateControlInformation; ENABLE | HARD_CAP; the rate is in 1/100 % of all processors.
|
|
31
|
-
public static void Apply(int pid, int percent) {
|
|
32
|
-
IntPtr job = CreateJobObject(IntPtr.Zero, null);
|
|
33
|
-
if (job == IntPtr.Zero) throw new Win32Exception();
|
|
34
|
-
CpuRate rate = new CpuRate();
|
|
35
|
-
rate.ControlFlags = 0x1 | 0x4;
|
|
36
|
-
rate.Rate = (uint)(percent * 100);
|
|
37
|
-
if (!SetInformationJobObject(job, 15, ref rate, Marshal.SizeOf(typeof(CpuRate)))) throw new Win32Exception();
|
|
38
|
-
// PROCESS_SET_QUOTA | PROCESS_TERMINATE: what AssignProcessToJobObject needs.
|
|
39
|
-
IntPtr process = OpenProcess(0x0100 | 0x0001, false, pid);
|
|
40
|
-
if (process == IntPtr.Zero) throw new Win32Exception();
|
|
41
|
-
try { if (!AssignProcessToJobObject(job, process)) throw new Win32Exception(); } finally { CloseHandle(process); }
|
|
42
|
-
// The job's handle may close: it lives on while any process in it runs.
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
'@
|
|
14
|
+
Add-Type -Path (Join-Path $PSScriptRoot 'cpucap.cs')
|
|
46
15
|
[SandboxCpuCap]::Apply($ProcessId, $Percent)
|
|
47
16
|
"CPU capped at $Percent% of the PC (sandbox.cpuLimit)." | Add-Content -LiteralPath $log
|
|
48
17
|
} catch {
|
package/sandbox/logon.cmd
CHANGED
|
@@ -4,9 +4,15 @@ rem (downloaded once by the host into persistent storage), or a plain PowerShell
|
|
|
4
4
|
if not exist C:\AgentData\logs mkdir C:\AgentData\logs
|
|
5
5
|
echo logon.cmd ran at %date% %time% > C:\AgentData\logs\logon-hit.txt
|
|
6
6
|
call C:\AgentLaunch\sandbox\trust.cmd
|
|
7
|
+
rem New Terminal tabs start in %AGENT_WORKSPACE%: Terminal must have it before the runtime sets it machine-wide.
|
|
8
|
+
if exist C:\AgentLaunch\env.cmd call C:\AgentLaunch\env.cmd
|
|
7
9
|
set "PS=%SystemRoot%\System32\WindowsPowerShell\v1.0\powershell.exe"
|
|
8
10
|
rem sandbox.cpuLimit: this window joins a CPU-capped job first, so everything started below inherits the cap.
|
|
9
11
|
"%PS%" -NoLogo -NoProfile -ExecutionPolicy Bypass -File C:\AgentLaunch\sandbox\cpucap.ps1
|
|
12
|
+
rem sandbox.pageFileMb: room for many parallel builds (the sandbox starts with about 1 GB of paging file).
|
|
13
|
+
"%PS%" -NoLogo -NoProfile -ExecutionPolicy Bypass -File C:\AgentLaunch\sandbox\pagefile.ps1
|
|
14
|
+
rem The memory state every 30 s into the persistent logs, so a crash leaves evidence.
|
|
15
|
+
start "" /min "%PS%" -NoLogo -NoProfile -WindowStyle Hidden -ExecutionPolicy Bypass -File C:\AgentLaunch\sandbox\memlog.ps1
|
|
10
16
|
set "BOOT=%PS% -NoLogo -NoProfile -ExecutionPolicy Bypass -NoExit -File C:\AgentLaunch\sandbox\bootstrap.ps1"
|
|
11
17
|
if not exist C:\AgentData\tools\terminal\wt.exe goto plain
|
|
12
18
|
start "" C:\AgentData\tools\terminal\wt.exe --maximized new-tab --title "Agent Sandbox" -d C:\AgentLaunch %BOOT%
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
#Requires -Version 5.1
|
|
2
|
+
# Writes the sandbox's memory state to C:\AgentData\logs\memory-<time>.log every 30 seconds, while it runs. That folder
|
|
3
|
+
# outlives the sandbox, so if Windows inside it ever crashes again, the last lines show what was using memory just
|
|
4
|
+
# before. Bounded: a file is closed at 1 MB (about two days) and a new one started; only the newest 10 are kept, so
|
|
5
|
+
# the logs never take more than about 10 MB. Started hidden by logon.cmd. Never stops the boot.
|
|
6
|
+
$ErrorActionPreference = 'SilentlyContinue'
|
|
7
|
+
$logFolder = 'C:\AgentData\logs'
|
|
8
|
+
$maxFileBytes = 1MB
|
|
9
|
+
$keepFiles = 10
|
|
10
|
+
|
|
11
|
+
# A new log file, and the oldest ones beyond $keepFiles removed. Returns its path.
|
|
12
|
+
function New-MemoryLog {
|
|
13
|
+
$path = Join-Path $logFolder ('memory-{0:yyyyMMdd-HHmmss}.log' -f (Get-Date))
|
|
14
|
+
'time | RAM used/total MB | commit used/limit MB | biggest processes (private MB)' | Set-Content -LiteralPath $path
|
|
15
|
+
Get-ChildItem -LiteralPath $logFolder -Filter 'memory-*.log' | Sort-Object LastWriteTime -Descending |
|
|
16
|
+
Select-Object -Skip $keepFiles | Remove-Item -Force
|
|
17
|
+
return $path
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
$logPath = New-MemoryLog
|
|
21
|
+
while ($true) {
|
|
22
|
+
$os = Get-CimInstance Win32_OperatingSystem
|
|
23
|
+
$ramTotalMb = [math]::Round($os.TotalVisibleMemorySize / 1KB)
|
|
24
|
+
$ramUsedMb = $ramTotalMb - [math]::Round($os.FreePhysicalMemory / 1KB)
|
|
25
|
+
$commitLimitMb = [math]::Round($os.TotalVirtualMemorySize / 1KB)
|
|
26
|
+
$commitUsedMb = $commitLimitMb - [math]::Round($os.FreeVirtualMemory / 1KB)
|
|
27
|
+
$processes = @(Get-Process)
|
|
28
|
+
$biggest = $processes | Sort-Object PrivateMemorySize64 -Descending | Select-Object -First 5 |
|
|
29
|
+
ForEach-Object { '{0} {1}' -f $_.ProcessName, [math]::Round($_.PrivateMemorySize64 / 1MB) }
|
|
30
|
+
'{0:HH:mm:ss} | {1}/{2} | {3}/{4} | {5} processes; {6}' -f (Get-Date), $ramUsedMb, $ramTotalMb, $commitUsedMb, $commitLimitMb, $processes.Count, ($biggest -join ', ') |
|
|
31
|
+
Add-Content -LiteralPath $logPath
|
|
32
|
+
if ((Get-Item -LiteralPath $logPath).Length -ge $maxFileBytes) { $logPath = New-MemoryLog }
|
|
33
|
+
Start-Sleep -Seconds 30
|
|
34
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// Sets the size of the sandbox's paging file at once, with no restart. Loaded by pagefile.ps1 (Add-Type). C# 5.
|
|
2
|
+
using System;
|
|
3
|
+
using System.ComponentModel;
|
|
4
|
+
using System.Runtime.InteropServices;
|
|
5
|
+
|
|
6
|
+
public static class SandboxPageFile {
|
|
7
|
+
// UNICODE_STRING. The marshaller copies Buffer to native memory for the length of the call.
|
|
8
|
+
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
|
|
9
|
+
private struct NtString { public ushort Length; public ushort MaximumLength; public string Buffer; }
|
|
10
|
+
|
|
11
|
+
[DllImport("ntdll.dll")] private static extern int RtlAdjustPrivilege(int privilege, bool enable, bool currentThread, out bool wasEnabled);
|
|
12
|
+
[DllImport("ntdll.dll")] private static extern int NtCreatePagingFile(ref NtString path, ref long minimumBytes, ref long maximumBytes, uint priority);
|
|
13
|
+
[DllImport("ntdll.dll")] private static extern int RtlNtStatusToDosError(int status);
|
|
14
|
+
|
|
15
|
+
private const int SeCreatePagefilePrivilege = 15;
|
|
16
|
+
|
|
17
|
+
/// <summary>Makes C:\pagefile.sys sizeMb, fixed (it never grows). Throws with Windows' reason on failure.</summary>
|
|
18
|
+
public static void Set(int sizeMb) {
|
|
19
|
+
bool wasEnabled;
|
|
20
|
+
Check(RtlAdjustPrivilege(SeCreatePagefilePrivilege, true, false, out wasEnabled));
|
|
21
|
+
// The same call Windows makes at startup; for the existing file it changes the size in place.
|
|
22
|
+
const string path = @"\??\C:\pagefile.sys";
|
|
23
|
+
NtString name = new NtString { Length = (ushort)(path.Length * 2), MaximumLength = (ushort)(path.Length * 2 + 2), Buffer = path };
|
|
24
|
+
long minimum = sizeMb * 1048576L, maximum = minimum;
|
|
25
|
+
Check(NtCreatePagingFile(ref name, ref minimum, ref maximum, 0));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
private static void Check(int status) {
|
|
29
|
+
if (status != 0) throw new Win32Exception(RtlNtStatusToDosError(status));
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
#Requires -Version 5.1
|
|
2
|
+
# Gives the sandbox a paging file of sandbox.pageFileMb, created at full size now, before anything runs. Windows Sandbox
|
|
3
|
+
# starts with about 1 GB, so parallel builds run out of memory ("The paging file is too small", os error 1455), and
|
|
4
|
+
# then Windows grows the file while they run: both crashes of Windows inside the sandbox (13 Oct 2026, stop code 0x1A
|
|
5
|
+
# MEMORY_MANAGEMENT) happened under that load. A fixed size never grows. Never stops the boot: it logs one line.
|
|
6
|
+
param([int]$SizeMb = 0) # 0 = read sandbox.pageFileMb from session.json
|
|
7
|
+
$ErrorActionPreference = 'Stop'
|
|
8
|
+
$log = 'C:\AgentData\logs\logon-hit.txt'
|
|
9
|
+
try {
|
|
10
|
+
if ($SizeMb -eq 0) { $SizeMb = [int](Get-Content -LiteralPath 'C:\AgentLaunch\session.json' -Raw -Encoding UTF8 | ConvertFrom-Json).pageFileMb }
|
|
11
|
+
if ($SizeMb -le 0) { exit 0 }
|
|
12
|
+
# Keep 8 GB of the sandbox's disk free for everything else.
|
|
13
|
+
$SizeMb = [math]::Min($SizeMb, [math]::Floor((Get-PSDrive -Name C).Free / 1MB) - 8192)
|
|
14
|
+
if ($SizeMb -lt 1024) { throw 'too little free disk.' }
|
|
15
|
+
Add-Type -Path (Join-Path $PSScriptRoot 'pagefile.cs')
|
|
16
|
+
[SandboxPageFile]::Set($SizeMb)
|
|
17
|
+
"Paging file set to $SizeMb MB, fixed (sandbox.pageFileMb)." | Add-Content -LiteralPath $log
|
|
18
|
+
} catch {
|
|
19
|
+
"Paging file left as it is: $($_.Exception.Message)" | Add-Content -LiteralPath $log
|
|
20
|
+
}
|
|
21
|
+
exit 0
|