windows-development-sandbox 0.0.0-stage → 1.0.5
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/.sandbox-config.example.json +14 -0
- package/README.md +78 -2
- package/config.example.json +332 -0
- package/dist/api.d.ts +31 -0
- package/dist/api.js +30 -0
- package/dist/bin/agent-sandbox.d.ts +2 -0
- package/dist/bin/agent-sandbox.js +3 -0
- package/dist/bin/claude-host.d.ts +2 -0
- package/dist/bin/claude-host.js +5 -0
- package/dist/bin/claude-sbx.d.ts +2 -0
- package/dist/bin/claude-sbx.js +3 -0
- package/dist/bin/codex-sbx.d.ts +2 -0
- package/dist/bin/codex-sbx.js +3 -0
- package/dist/bin/gemini-sbx.d.ts +2 -0
- package/dist/bin/gemini-sbx.js +3 -0
- package/dist/bin/sandbox.d.ts +2 -0
- package/dist/bin/sandbox.js +4 -0
- package/dist/bin/sbx-hide.d.ts +2 -0
- package/dist/bin/sbx-hide.js +3 -0
- package/dist/bin/sbx-show.d.ts +2 -0
- package/dist/bin/sbx-show.js +3 -0
- package/dist/cli/args.d.ts +32 -0
- package/dist/cli/args.js +111 -0
- package/dist/cli/commands/accounts.d.ts +2 -0
- package/dist/cli/commands/accounts.js +52 -0
- package/dist/cli/commands/context.d.ts +21 -0
- package/dist/cli/commands/context.js +54 -0
- package/dist/cli/commands/data.d.ts +46 -0
- package/dist/cli/commands/data.js +249 -0
- package/dist/cli/commands/index.d.ts +8 -0
- package/dist/cli/commands/index.js +8 -0
- package/dist/cli/commands/launch.d.ts +5 -0
- package/dist/cli/commands/launch.js +152 -0
- package/dist/cli/commands/maintenance.d.ts +8 -0
- package/dist/cli/commands/maintenance.js +125 -0
- package/dist/cli/commands/repair.d.ts +59 -0
- package/dist/cli/commands/repair.js +332 -0
- package/dist/cli/commands/sessions.d.ts +11 -0
- package/dist/cli/commands/sessions.js +113 -0
- package/dist/cli/commands/setup.d.ts +45 -0
- package/dist/cli/commands/setup.js +152 -0
- package/dist/cli/index.d.ts +14 -0
- package/dist/cli/index.js +96 -0
- package/dist/cli/manifest.d.ts +95 -0
- package/dist/cli/manifest.js +87 -0
- package/dist/cli/menu.d.ts +22 -0
- package/dist/cli/menu.js +187 -0
- package/dist/cli/settings.d.ts +11 -0
- package/dist/cli/settings.js +156 -0
- package/dist/core/config.d.ts +17 -0
- package/dist/core/config.js +103 -0
- package/dist/core/detect-databases.d.ts +15 -0
- package/dist/core/detect-databases.js +147 -0
- package/dist/core/detect-pwsh.d.ts +18 -0
- package/dist/core/detect-pwsh.js +111 -0
- package/dist/core/detect-toolchains.d.ts +30 -0
- package/dist/core/detect-toolchains.js +58 -0
- package/dist/core/local-config.d.ts +12 -0
- package/dist/core/local-config.js +64 -0
- package/dist/core/schema.d.ts +604 -0
- package/dist/core/schema.js +264 -0
- package/dist/core/types.d.ts +162 -0
- package/dist/core/types.js +1 -0
- package/dist/core/util.d.ts +53 -0
- package/dist/core/util.js +145 -0
- package/dist/core/workspace.d.ts +23 -0
- package/dist/core/workspace.js +162 -0
- package/dist/guest/daemon.d.ts +1 -0
- package/dist/guest/daemon.js +186 -0
- package/dist/guest/db.d.ts +1 -0
- package/dist/guest/db.js +60 -0
- package/dist/guest/env.d.ts +49 -0
- package/dist/guest/env.js +99 -0
- package/dist/guest/gitguard.d.ts +7 -0
- package/dist/guest/gitguard.js +60 -0
- package/dist/guest/gitrules.d.ts +2 -0
- package/dist/guest/gitrules.js +11 -0
- package/dist/guest/guide.d.ts +12 -0
- package/dist/guest/guide.js +68 -0
- package/dist/guest/main.d.ts +1 -0
- package/dist/guest/main.js +120 -0
- package/dist/guest/ptyhost.d.ts +40 -0
- package/dist/guest/ptyhost.js +94 -0
- package/dist/guest/setup/agents.d.ts +8 -0
- package/dist/guest/setup/agents.js +73 -0
- package/dist/guest/setup/claude.d.ts +21 -0
- package/dist/guest/setup/claude.js +138 -0
- package/dist/guest/setup/environment.d.ts +6 -0
- package/dist/guest/setup/environment.js +65 -0
- package/dist/guest/setup/hooks.d.ts +1 -0
- package/dist/guest/setup/hooks.js +36 -0
- package/dist/guest/setup/network.d.ts +10 -0
- package/dist/guest/setup/network.js +99 -0
- package/dist/guest/setup/policy.d.ts +3 -0
- package/dist/guest/setup/policy.js +18 -0
- package/dist/guest/setup/user.d.ts +7 -0
- package/dist/guest/setup/user.js +47 -0
- package/dist/guest/toolchains/binstall.d.ts +7 -0
- package/dist/guest/toolchains/binstall.js +55 -0
- package/dist/guest/toolchains/catalogue.d.ts +51 -0
- package/dist/guest/toolchains/catalogue.js +31 -0
- package/dist/guest/toolchains/cli.d.ts +10 -0
- package/dist/guest/toolchains/cli.js +64 -0
- package/dist/guest/toolchains/common.d.ts +37 -0
- package/dist/guest/toolchains/common.js +124 -0
- package/dist/guest/toolchains/databases.d.ts +7 -0
- package/dist/guest/toolchains/databases.js +226 -0
- package/dist/guest/toolchains/git.d.ts +3 -0
- package/dist/guest/toolchains/git.js +112 -0
- package/dist/guest/toolchains/go.d.ts +3 -0
- package/dist/guest/toolchains/go.js +33 -0
- package/dist/guest/toolchains/index.d.ts +13 -0
- package/dist/guest/toolchains/index.js +122 -0
- package/dist/guest/toolchains/java.d.ts +8 -0
- package/dist/guest/toolchains/java.js +96 -0
- package/dist/guest/toolchains/managers.d.ts +16 -0
- package/dist/guest/toolchains/managers.js +82 -0
- package/dist/guest/toolchains/msvc.d.ts +10 -0
- package/dist/guest/toolchains/msvc.js +216 -0
- package/dist/guest/toolchains/native.d.ts +12 -0
- package/dist/guest/toolchains/native.js +220 -0
- package/dist/guest/toolchains/node.d.ts +3 -0
- package/dist/guest/toolchains/node.js +132 -0
- package/dist/guest/toolchains/pwsh.d.ts +4 -0
- package/dist/guest/toolchains/pwsh.js +14 -0
- package/dist/guest/toolchains/python.d.ts +3 -0
- package/dist/guest/toolchains/python.js +47 -0
- package/dist/guest/toolchains/runtimes.d.ts +17 -0
- package/dist/guest/toolchains/runtimes.js +40 -0
- package/dist/guest/toolchains/scripting.d.ts +10 -0
- package/dist/guest/toolchains/scripting.js +137 -0
- package/dist/guest/toolchains/versions.d.ts +47 -0
- package/dist/guest/toolchains/versions.js +77 -0
- package/dist/guest/toolchains/vscode.d.ts +2 -0
- package/dist/guest/toolchains/vscode.js +53 -0
- package/dist/guest/view.d.ts +1 -0
- package/dist/guest/view.js +22 -0
- package/dist/host/accounts.d.ts +32 -0
- package/dist/host/accounts.js +119 -0
- package/dist/host/channel.d.ts +37 -0
- package/dist/host/channel.js +180 -0
- package/dist/host/claude-profile.d.ts +15 -0
- package/dist/host/claude-profile.js +103 -0
- package/dist/host/cleanup.d.ts +23 -0
- package/dist/host/cleanup.js +66 -0
- package/dist/host/generate.d.ts +15 -0
- package/dist/host/generate.js +185 -0
- package/dist/host/personalize.d.ts +10 -0
- package/dist/host/personalize.js +97 -0
- package/dist/host/platform.d.ts +17 -0
- package/dist/host/platform.js +114 -0
- package/dist/host/project-scan.d.ts +23 -0
- package/dist/host/project-scan.js +173 -0
- package/dist/host/repo-guard.d.ts +23 -0
- package/dist/host/repo-guard.js +128 -0
- package/dist/host/secrets.d.ts +14 -0
- package/dist/host/secrets.js +133 -0
- package/dist/host/terminal.d.ts +32 -0
- package/dist/host/terminal.js +127 -0
- package/dist/host/window.d.ts +4 -0
- package/dist/host/window.js +68 -0
- package/dist/runtime/daemon.js +20291 -0
- package/dist/runtime/db.js +20423 -0
- package/dist/runtime/gitguard.js +84 -0
- package/dist/runtime/main.js +22398 -0
- package/dist/runtime/view.js +19966 -0
- package/dist/selftest/cli.test.d.ts +2 -0
- package/dist/selftest/cli.test.js +30 -0
- package/dist/selftest/config.test.d.ts +3 -0
- package/dist/selftest/config.test.js +74 -0
- package/dist/selftest/data.test.d.ts +2 -0
- package/dist/selftest/data.test.js +73 -0
- package/dist/selftest/detect.test.d.ts +2 -0
- package/dist/selftest/detect.test.js +42 -0
- package/dist/selftest/fixtures.d.ts +232 -0
- package/dist/selftest/fixtures.js +63 -0
- package/dist/selftest/guest.test.d.ts +3 -0
- package/dist/selftest/guest.test.js +61 -0
- package/dist/selftest/harness.d.ts +14 -0
- package/dist/selftest/harness.js +36 -0
- package/dist/selftest/helpers.test.d.ts +2 -0
- package/dist/selftest/helpers.test.js +125 -0
- package/dist/selftest/host.test.d.ts +2 -0
- package/dist/selftest/host.test.js +71 -0
- package/dist/selftest/index.d.ts +1 -0
- package/dist/selftest/index.js +56 -0
- package/dist/selftest/live.test.d.ts +7 -0
- package/dist/selftest/live.test.js +127 -0
- package/dist/selftest/mirror.test.d.ts +3 -0
- package/dist/selftest/mirror.test.js +74 -0
- package/dist/selftest/security.test.d.ts +3 -0
- package/dist/selftest/security.test.js +143 -0
- package/dist/selftest/setup.test.d.ts +2 -0
- package/dist/selftest/setup.test.js +23 -0
- package/dist/selftest/sync.test.d.ts +3 -0
- package/dist/selftest/sync.test.js +82 -0
- package/dist/shared/download.d.ts +58 -0
- package/dist/shared/download.js +200 -0
- package/dist/shared/guest-paths.d.ts +30 -0
- package/dist/shared/guest-paths.js +61 -0
- package/dist/shared/netrules.d.ts +18 -0
- package/dist/shared/netrules.js +98 -0
- package/dist/shared/paths.d.ts +9 -0
- package/dist/shared/paths.js +22 -0
- package/dist/shared/pty-client.d.ts +86 -0
- package/dist/shared/pty-client.js +227 -0
- package/dist/shared/rundir.d.ts +30 -0
- package/dist/shared/rundir.js +86 -0
- package/dist/shared/safe-remove.d.ts +10 -0
- package/dist/shared/safe-remove.js +55 -0
- package/dist/shared/schema.d.ts +84 -0
- package/dist/shared/schema.js +51 -0
- package/dist/shared/system.d.ts +15 -0
- package/dist/shared/system.js +19 -0
- package/dist/sync/claude-mem.d.ts +8 -0
- package/dist/sync/claude-mem.js +77 -0
- package/dist/sync/history.d.ts +30 -0
- package/dist/sync/history.js +129 -0
- package/dist/sync/watcher.d.ts +13 -0
- package/dist/sync/watcher.js +116 -0
- package/hooks/post-agent/README.txt +1 -0
- package/hooks/pre-provision/README.txt +1 -0
- package/package.json +91 -4
- package/sandbox/bootstrap.ps1 +258 -0
- package/sandbox/gitshim.cs +35 -0
- package/sandbox/logon.cmd +16 -0
- package/sandbox/ptyhost.cs +603 -0
- package/sandbox/trust.cmd +35 -0
- package/sandbox/vswhere.cs +83 -0
- package/schemas/config.schema.json +1062 -0
- package/schemas/sandbox-config.schema.json +952 -0
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
// Claude settings from this PC, plugin path fix-up, and the claude-mem working copy.
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { translatePaths } from '../../shared/paths.js';
|
|
6
|
+
import { claudeDir, dryRun, exists, launchDir, log, mkdirp, robocopy, session, stateDir, warn, workspace } from '../env.js';
|
|
7
|
+
/** The sandbox keeps its own login and global state; everything else is refreshed from this PC each boot. */
|
|
8
|
+
const KEEP_SANDBOX_COPY = ['.claude.json', '.credentials.json'];
|
|
9
|
+
/** Plugin manifests store absolute paths to the plugin cache; point them at the sandbox's Claude folder. */
|
|
10
|
+
const fixPluginPaths = (hostConfigDir) => {
|
|
11
|
+
const plugins = path.join(claudeDir, 'plugins');
|
|
12
|
+
if (!hostConfigDir || !exists(plugins))
|
|
13
|
+
return;
|
|
14
|
+
for (const name of fs.readdirSync(plugins).filter((f) => f.endsWith('.json'))) {
|
|
15
|
+
const file = path.join(plugins, name);
|
|
16
|
+
const text = fs.readFileSync(file, 'utf8');
|
|
17
|
+
const fixed = translatePaths(text, hostConfigDir, claudeDir);
|
|
18
|
+
if (fixed !== text)
|
|
19
|
+
fs.writeFileSync(file, fixed, 'utf8');
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
const readState = (file) => { try {
|
|
23
|
+
return JSON.parse(fs.readFileSync(file, 'utf8').replace(/^/, ''));
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return null;
|
|
27
|
+
} };
|
|
28
|
+
const sameFolder = (a, b) => a.replace(/\\/g, '/').replace(/\/+$/, '').toLowerCase() === b.replace(/\\/g, '/').replace(/\/+$/, '').toLowerCase();
|
|
29
|
+
/**
|
|
30
|
+
* The sandbox keeps its own .claude.json (login, onboarding, per-project state), so MCP servers from this PC are
|
|
31
|
+
* merged into it each boot: user-scope `mcpServers`, and this project's local-scope servers under the sandbox path.
|
|
32
|
+
* Servers with the same name are replaced by this PC's definition; servers added inside the sandbox are kept.
|
|
33
|
+
*/
|
|
34
|
+
export const mergeMcpServers = (hostFile, guestFile, hostWorkspace, guestWorkspace) => {
|
|
35
|
+
const host = readState(hostFile);
|
|
36
|
+
const guest = readState(guestFile);
|
|
37
|
+
if (!host || !guest)
|
|
38
|
+
return 0;
|
|
39
|
+
const hostProject = Object.entries(host.projects ?? {}).find(([key]) => sameFolder(key, hostWorkspace))?.[1]?.mcpServers ?? {};
|
|
40
|
+
const projects = { ...(guest.projects ?? {}) };
|
|
41
|
+
const guestKey = Object.keys(projects).find((key) => sameFolder(key, guestWorkspace)) ?? guestWorkspace.replace(/\\/g, '/');
|
|
42
|
+
projects[guestKey] = { ...(projects[guestKey] ?? {}), mcpServers: { ...(projects[guestKey]?.mcpServers ?? {}), ...hostProject } };
|
|
43
|
+
const merged = { ...guest, mcpServers: { ...(guest.mcpServers ?? {}), ...(host.mcpServers ?? {}) }, projects };
|
|
44
|
+
fs.writeFileSync(guestFile, JSON.stringify(merged, null, 2) + '\n', 'utf8');
|
|
45
|
+
return Object.keys(host.mcpServers ?? {}).length + Object.keys(hostProject).length;
|
|
46
|
+
};
|
|
47
|
+
export const syncClaudeProfile = () => {
|
|
48
|
+
const profile = session.claudeProfile;
|
|
49
|
+
if (profile.items.length === 0) {
|
|
50
|
+
log('No Claude settings were staged from this PC.');
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
const source = path.join(launchDir, 'claude-profile');
|
|
54
|
+
mkdirp(claudeDir);
|
|
55
|
+
for (const name of profile.items) {
|
|
56
|
+
const from = path.join(source, name);
|
|
57
|
+
const to = path.join(claudeDir, name);
|
|
58
|
+
if (!exists(from))
|
|
59
|
+
continue;
|
|
60
|
+
if ((KEEP_SANDBOX_COPY.includes(name) || !profile.overwrite) && exists(to)) {
|
|
61
|
+
log('Claude settings: kept the sandbox\'s ' + name);
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
// Folders (skills, commands, plugins) are mirrored: what was removed on this PC goes here too.
|
|
65
|
+
if (fs.statSync(from).isDirectory())
|
|
66
|
+
fs.rmSync(to, { recursive: true, force: true });
|
|
67
|
+
fs.cpSync(from, to, { recursive: true, force: true });
|
|
68
|
+
log('Claude settings: copied ' + name + ' from this PC');
|
|
69
|
+
}
|
|
70
|
+
fixPluginPaths(profile.hostConfigDir);
|
|
71
|
+
const mcp = mergeMcpServers(path.join(source, '.claude.json'), path.join(claudeDir, '.claude.json'), session.workspaceHost, workspace);
|
|
72
|
+
if (mcp)
|
|
73
|
+
log('Claude settings: ' + mcp + ' MCP server(s) from this PC merged in');
|
|
74
|
+
if (profile.credentials)
|
|
75
|
+
warn('Your host Claude LOGIN was copied into the sandbox (copyCredentials is true).');
|
|
76
|
+
};
|
|
77
|
+
/**
|
|
78
|
+
* claude-mem keeps SQLite + a vector store. They run from the sandbox's own disk (SQLite and mapped folders do not
|
|
79
|
+
* mix) and are mirrored to persistent storage, from where the host syncs them back to your PC.
|
|
80
|
+
*/
|
|
81
|
+
export const claudeMemPaths = { store: path.join(stateDir, 'claude-mem'), local: path.join(os.homedir(), '.claude-mem') };
|
|
82
|
+
const claudeMemEnabled = () => session.claudeProfile.syncClaudeMem && !dryRun;
|
|
83
|
+
export const restoreClaudeMem = () => {
|
|
84
|
+
if (!claudeMemEnabled() || !exists(claudeMemPaths.store))
|
|
85
|
+
return;
|
|
86
|
+
robocopy(claudeMemPaths.store, claudeMemPaths.local);
|
|
87
|
+
log('claude-mem data restored from persistent storage.');
|
|
88
|
+
};
|
|
89
|
+
/** Size + mtime of every SQLite database and -wal file under `dir`: when these have not moved, nothing was written. */
|
|
90
|
+
const databaseFingerprint = (dir) => {
|
|
91
|
+
const parts = [];
|
|
92
|
+
const walk = (folder) => {
|
|
93
|
+
for (const entry of fs.readdirSync(folder, { withFileTypes: true })) {
|
|
94
|
+
const full = path.join(folder, entry.name);
|
|
95
|
+
if (entry.isDirectory())
|
|
96
|
+
walk(full);
|
|
97
|
+
else if (entry.isFile() && /\.(db|sqlite3?)(-wal)?$/i.test(entry.name)) {
|
|
98
|
+
const s = fs.statSync(full);
|
|
99
|
+
parts.push(full + ':' + s.size + ':' + s.mtimeMs);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
walk(dir);
|
|
104
|
+
return parts.sort().join('|');
|
|
105
|
+
};
|
|
106
|
+
/** The databases as of the last save by this process (null: not saved yet, so the first save always copies). */
|
|
107
|
+
let lastSaved = null;
|
|
108
|
+
/**
|
|
109
|
+
* Copies the live claude-mem folder back to persistent storage (staging folder + swap, never a half copy). Called from
|
|
110
|
+
* the daemon tick: when no database changed since the last save it does nothing (no copy of the whole folder).
|
|
111
|
+
*/
|
|
112
|
+
export const saveClaudeMem = () => {
|
|
113
|
+
if (!claudeMemEnabled() || !exists(claudeMemPaths.local))
|
|
114
|
+
return;
|
|
115
|
+
const { store, local } = claudeMemPaths;
|
|
116
|
+
const fingerprint = databaseFingerprint(local);
|
|
117
|
+
if (fingerprint === lastSaved && exists(store))
|
|
118
|
+
return;
|
|
119
|
+
const incoming = store + '.incoming';
|
|
120
|
+
const previous = store + '.previous';
|
|
121
|
+
fs.rmSync(incoming, { recursive: true, force: true });
|
|
122
|
+
// The database, its -wal and -shm files are copied together, so the copy is consistent with itself.
|
|
123
|
+
fs.cpSync(local, incoming, { recursive: true, filter: (src) => !/\.(pid|lock)$/i.test(src) });
|
|
124
|
+
// Swap without a window where no copy exists: old aside, new in, then drop the old one.
|
|
125
|
+
fs.rmSync(previous, { recursive: true, force: true });
|
|
126
|
+
if (exists(store))
|
|
127
|
+
fs.renameSync(store, previous);
|
|
128
|
+
try {
|
|
129
|
+
fs.renameSync(incoming, store);
|
|
130
|
+
}
|
|
131
|
+
catch (e) {
|
|
132
|
+
if (exists(previous))
|
|
133
|
+
fs.renameSync(previous, store);
|
|
134
|
+
throw e;
|
|
135
|
+
}
|
|
136
|
+
fs.rmSync(previous, { recursive: true, force: true });
|
|
137
|
+
lastSaved = fingerprint;
|
|
138
|
+
};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { ToolEnv } from '../toolchains/index.js';
|
|
2
|
+
export declare const agentEnv: (tools: Pick<ToolEnv, "vars" | "prepend" | "append">) => NodeJS.ProcessEnv;
|
|
3
|
+
/** Makes the agent environment machine-wide, so new terminal tabs and anything the agent starts see the same tools. */
|
|
4
|
+
export declare const persistEnvironment: (env: NodeJS.ProcessEnv, toolVars: string[]) => void;
|
|
5
|
+
/** Adds variables and PATH folders machine-wide after boot (`sbx-db start`): new terminal tabs see them. */
|
|
6
|
+
export declare const addMachineEnvironment: (vars: Record<string, string>, dirs: string[]) => void;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// The environment every agent session, hook and terminal tab in the sandbox gets.
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { claudeDir, dataDir, dryRun, errMsg, isWin, testMode, mkdirp, powershell, session, stateDir, toolsDir, warn, workspace } from '../env.js';
|
|
4
|
+
/** Variables agent-sandbox itself sets (besides the toolchains' own). */
|
|
5
|
+
const AGENT_VARS = ['CLAUDE_CONFIG_DIR', 'CODEX_HOME', 'GEMINI_CLI_HOME', 'npm_config_cache', 'npm_config_update_notifier', 'AGENT_WORKSPACE', 'AGENT_DATA', 'AGENT_TOOLS', 'AGENT_PROJECT', 'AGENT_SANDBOX', 'AGENT_SANDBOX_WORKSPACE'];
|
|
6
|
+
export const agentEnv = (tools) => {
|
|
7
|
+
const env = {
|
|
8
|
+
...process.env,
|
|
9
|
+
...tools.vars,
|
|
10
|
+
CLAUDE_CONFIG_DIR: claudeDir,
|
|
11
|
+
CODEX_HOME: path.join(stateDir, 'codex'),
|
|
12
|
+
GEMINI_CLI_HOME: path.join(stateDir, 'gemini'),
|
|
13
|
+
npm_config_cache: path.join(toolsDir, 'npm-cache'),
|
|
14
|
+
npm_config_update_notifier: 'false',
|
|
15
|
+
AGENT_WORKSPACE: workspace,
|
|
16
|
+
AGENT_DATA: dataDir,
|
|
17
|
+
AGENT_TOOLS: toolsDir,
|
|
18
|
+
AGENT_PROJECT: session.project,
|
|
19
|
+
// For project scripts: "am I in the sandbox, and where is the project?" (stable names; documented).
|
|
20
|
+
AGENT_SANDBOX: '1',
|
|
21
|
+
AGENT_SANDBOX_WORKSPACE: workspace,
|
|
22
|
+
};
|
|
23
|
+
for (const dir of [env.CLAUDE_CONFIG_DIR, env.CODEX_HOME, env.GEMINI_CLI_HOME, env.npm_config_cache])
|
|
24
|
+
mkdirp(dir);
|
|
25
|
+
// The runner's own Node (bootstrap.ps1 puts it first on PATH) is not exposed: `node` is whatever nvm selected.
|
|
26
|
+
const runners = [path.join(toolsDir, 'node'), path.dirname(process.execPath)].map((d) => d.toLowerCase());
|
|
27
|
+
const base = (process.env.PATH ?? '').split(path.delimiter).filter((d) => d && !runners.includes(d.toLowerCase()));
|
|
28
|
+
env.PATH = [...new Set([...tools.prepend, ...base, ...tools.append])].join(path.delimiter);
|
|
29
|
+
return env;
|
|
30
|
+
};
|
|
31
|
+
/** Makes the agent environment machine-wide, so new terminal tabs and anything the agent starts see the same tools. */
|
|
32
|
+
export const persistEnvironment = (env, toolVars) => {
|
|
33
|
+
if (!isWin || dryRun || testMode)
|
|
34
|
+
return;
|
|
35
|
+
const vars = Object.fromEntries([...toolVars, ...AGENT_VARS].filter((n) => env[n] !== undefined).map((n) => [n, env[n]]));
|
|
36
|
+
const script = [
|
|
37
|
+
"$vars = '" + JSON.stringify(vars).replace(/'/g, "''") + "' | ConvertFrom-Json",
|
|
38
|
+
"foreach ($p in $vars.PSObject.Properties) { [Environment]::SetEnvironmentVariable($p.Name, [string]$p.Value, 'Machine') }",
|
|
39
|
+
"[Environment]::SetEnvironmentVariable('Path', '" + (env.PATH ?? '').replace(/'/g, "''") + "', 'Machine')",
|
|
40
|
+
].join('\n');
|
|
41
|
+
try {
|
|
42
|
+
powershell(script);
|
|
43
|
+
}
|
|
44
|
+
catch (e) {
|
|
45
|
+
warn('Could not persist the environment machine-wide: ' + errMsg(e));
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
/** Adds variables and PATH folders machine-wide after boot (`sbx-db start`): new terminal tabs see them. */
|
|
49
|
+
export const addMachineEnvironment = (vars, dirs) => {
|
|
50
|
+
if (!isWin || dryRun || testMode)
|
|
51
|
+
return;
|
|
52
|
+
const script = [
|
|
53
|
+
"$vars = '" + JSON.stringify(vars).replace(/'/g, "''") + "' | ConvertFrom-Json",
|
|
54
|
+
"foreach ($p in $vars.PSObject.Properties) { [Environment]::SetEnvironmentVariable($p.Name, [string]$p.Value, 'Machine') }",
|
|
55
|
+
"$path = [Environment]::GetEnvironmentVariable('Path', 'Machine') -split ';'",
|
|
56
|
+
"$add = '" + JSON.stringify(dirs).replace(/'/g, "''") + "' | ConvertFrom-Json | Where-Object { $path -notcontains $_ }",
|
|
57
|
+
"if ($add) { [Environment]::SetEnvironmentVariable('Path', ((@($add) + $path) -join ';'), 'Machine') }",
|
|
58
|
+
].join('\n');
|
|
59
|
+
try {
|
|
60
|
+
powershell(script);
|
|
61
|
+
}
|
|
62
|
+
catch (e) {
|
|
63
|
+
warn('Could not add to the environment: ' + errMsg(e));
|
|
64
|
+
}
|
|
65
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const runHooks: (stage: string, env: NodeJS.ProcessEnv) => void;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// User hook scripts (.ps1 / .mjs / .js / .cmd / .bat) from hooks\<stage>, run in name order in the workspace.
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { spawnSync } from 'node:child_process';
|
|
5
|
+
import { SYSTEM } from '../../shared/system.js';
|
|
6
|
+
import { exists, isWin, launchDir, log, session, warn, workspace } from '../env.js';
|
|
7
|
+
const commandFor = (file) => {
|
|
8
|
+
const ext = path.extname(file).toLowerCase();
|
|
9
|
+
if (ext === '.mjs' || ext === '.js')
|
|
10
|
+
return [process.execPath, [file]];
|
|
11
|
+
if (!isWin)
|
|
12
|
+
return null;
|
|
13
|
+
if (ext === '.ps1')
|
|
14
|
+
return [SYSTEM.powershell(), ['-NoLogo', '-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', file]];
|
|
15
|
+
if (ext === '.cmd' || ext === '.bat')
|
|
16
|
+
return [SYSTEM.cmd(), ['/d', '/c', file]];
|
|
17
|
+
return null;
|
|
18
|
+
};
|
|
19
|
+
export const runHooks = (stage, env) => {
|
|
20
|
+
const dir = path.join(launchDir, 'hooks', stage);
|
|
21
|
+
if (!exists(dir))
|
|
22
|
+
return;
|
|
23
|
+
for (const name of fs.readdirSync(dir).sort()) {
|
|
24
|
+
const command = commandFor(path.join(dir, name));
|
|
25
|
+
if (!command)
|
|
26
|
+
continue;
|
|
27
|
+
log('Hook [' + stage + ']: ' + name);
|
|
28
|
+
const result = spawnSync(command[0], command[1], { stdio: 'inherit', cwd: workspace, env });
|
|
29
|
+
if (result.status === 0)
|
|
30
|
+
continue;
|
|
31
|
+
const message = 'Hook failed (' + stage + '/' + name + ', exit ' + result.status + ')';
|
|
32
|
+
if (session.hooks.failFast)
|
|
33
|
+
throw new Error(message);
|
|
34
|
+
warn(message);
|
|
35
|
+
}
|
|
36
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** What the agent's manifest says about the LAN. */
|
|
2
|
+
export type LanState = 'blocked' | 'open' | 'offline';
|
|
3
|
+
/**
|
|
4
|
+
* Applies the LAN block. Internet is checked first; if it works before the rules but not after, the rules (not the
|
|
5
|
+
* internet) are the problem, so they are removed and the agent is told the LAN is open. If there is no internet at
|
|
6
|
+
* all, the block stays in place: being offline is never a reason to open the LAN.
|
|
7
|
+
*/
|
|
8
|
+
export declare const applyNetworkPolicy: (ptyHostExe: string | null) => Promise<LanState>;
|
|
9
|
+
/** Guest IPv4 addresses the host can use to reach session ports. */
|
|
10
|
+
export declare const guestAddresses: () => string[];
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
// Guest firewall: block the host's LAN (keeping DNS/DHCP to the gateway) and let the host reach session ports.
|
|
2
|
+
import dns from 'node:dns';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import { spawnSync } from 'node:child_process';
|
|
5
|
+
import { buildFirewallCommands, ipv4DnsServers, parseDefaultGateways, parseDnsServers } from '../../shared/netrules.js';
|
|
6
|
+
import { SYSTEM } from '../../shared/system.js';
|
|
7
|
+
import { dryRun, isWin, log, session, warn } from '../env.js';
|
|
8
|
+
const ATTACH_RULE = 'AgentSandbox-Attach';
|
|
9
|
+
const netsh = (args) => {
|
|
10
|
+
const r = spawnSync(SYSTEM.netsh(), args, { encoding: 'utf8', windowsHide: true });
|
|
11
|
+
return { status: r.status, out: (r.stdout ?? '') + (r.stderr ?? '') };
|
|
12
|
+
};
|
|
13
|
+
const discoverExemptIps = () => {
|
|
14
|
+
if (process.env.AGENT_TEST_EXEMPT)
|
|
15
|
+
return process.env.AGENT_TEST_EXEMPT.split(',');
|
|
16
|
+
if (!isWin)
|
|
17
|
+
return [];
|
|
18
|
+
const route = spawnSync(SYSTEM.route(), ['print', '-4'], { encoding: 'utf8', windowsHide: true });
|
|
19
|
+
// The resolver's own list works on every Windows language; `ipconfig /all` (English labels only) is the fallback.
|
|
20
|
+
let servers = [];
|
|
21
|
+
try {
|
|
22
|
+
servers = ipv4DnsServers(dns.getServers());
|
|
23
|
+
}
|
|
24
|
+
catch { /* fall back below */ }
|
|
25
|
+
if (!servers.length)
|
|
26
|
+
servers = parseDnsServers(spawnSync(SYSTEM.ipconfig(), ['/all'], { encoding: 'utf8', windowsHide: true }).stdout ?? '');
|
|
27
|
+
return [...parseDefaultGateways(route.stdout ?? ''), ...servers];
|
|
28
|
+
};
|
|
29
|
+
const internetWorks = async () => {
|
|
30
|
+
try {
|
|
31
|
+
return (await fetch('https://nodejs.org', { method: 'HEAD', signal: AbortSignal.timeout(10000) })).status > 0;
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
/** The sandbox network can take a few seconds to come up after logon. */
|
|
38
|
+
const waitForInternet = async (seconds) => {
|
|
39
|
+
for (const end = Date.now() + seconds * 1000; Date.now() < end;) {
|
|
40
|
+
if (await internetWorks())
|
|
41
|
+
return true;
|
|
42
|
+
await new Promise((r) => setTimeout(r, 2000));
|
|
43
|
+
}
|
|
44
|
+
return false;
|
|
45
|
+
};
|
|
46
|
+
/** Inbound rule for ptyhost only, and only from the sandbox's own subnet (where the host's Hyper-V adapter is). */
|
|
47
|
+
const attachRule = (ptyHostExe) => [
|
|
48
|
+
['advfirewall', 'firewall', 'delete', 'rule', 'name=' + ATTACH_RULE],
|
|
49
|
+
['advfirewall', 'firewall', 'add', 'rule', 'name=' + ATTACH_RULE, 'dir=in', 'action=allow', 'enable=yes', 'profile=any', 'protocol=tcp', 'remoteip=LocalSubnet', 'program=' + ptyHostExe],
|
|
50
|
+
];
|
|
51
|
+
const run = (commands) => {
|
|
52
|
+
for (const c of commands) {
|
|
53
|
+
const r = netsh(c);
|
|
54
|
+
if (r.status !== 0 && c[2] !== 'delete')
|
|
55
|
+
throw new Error('netsh ' + c.slice(0, 5).join(' ') + ' failed (exit ' + r.status + '): ' + r.out.trim());
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
/**
|
|
59
|
+
* Applies the LAN block. Internet is checked first; if it works before the rules but not after, the rules (not the
|
|
60
|
+
* internet) are the problem, so they are removed and the agent is told the LAN is open. If there is no internet at
|
|
61
|
+
* all, the block stays in place: being offline is never a reason to open the LAN.
|
|
62
|
+
*/
|
|
63
|
+
export const applyNetworkPolicy = async (ptyHostExe) => {
|
|
64
|
+
if (!session.networking) {
|
|
65
|
+
log('Networking is disabled for this sandbox.');
|
|
66
|
+
return 'offline';
|
|
67
|
+
}
|
|
68
|
+
const block = session.network.blockPrivateRanges;
|
|
69
|
+
const exempt = block ? discoverExemptIps() : [];
|
|
70
|
+
const lanRules = block ? buildFirewallCommands(exempt) : [];
|
|
71
|
+
const commands = [...lanRules, ...(ptyHostExe && session.attach.enabled ? attachRule(ptyHostExe) : [])];
|
|
72
|
+
if (!block)
|
|
73
|
+
log('Private-range blocking disabled by config.');
|
|
74
|
+
if (block)
|
|
75
|
+
log('Gateway/DNS addresses kept reachable for DNS only: ' + (exempt.length ? [...new Set(exempt)].join(', ') : '(none found)'));
|
|
76
|
+
if (!isWin || dryRun) {
|
|
77
|
+
log('(dry run) firewall plan:');
|
|
78
|
+
for (const c of commands)
|
|
79
|
+
log(' netsh ' + c.join(' '));
|
|
80
|
+
return block ? 'blocked' : 'open';
|
|
81
|
+
}
|
|
82
|
+
if (commands.length === 0)
|
|
83
|
+
return 'open';
|
|
84
|
+
const online = block ? await waitForInternet(30) : true;
|
|
85
|
+
run(commands);
|
|
86
|
+
if (!block)
|
|
87
|
+
return 'open';
|
|
88
|
+
if (online && !(await waitForInternet(15))) {
|
|
89
|
+
run(lanRules.filter((c) => c[2] === 'delete'));
|
|
90
|
+
warn('The LAN block broke internet access in this sandbox, so it was removed: the agent CAN reach your local network this time. Set sandbox.networking to false for hard isolation.');
|
|
91
|
+
return 'open';
|
|
92
|
+
}
|
|
93
|
+
if (!online)
|
|
94
|
+
warn('No internet detected yet; the LAN block is in place anyway.');
|
|
95
|
+
log('LAN blocked in the guest firewall (10/8, 172.16/12, 192.168/16, 100.64/10, link-local, multicast, IPv6 local, SMB); DNS/DHCP to the gateway allowed.');
|
|
96
|
+
return 'blocked';
|
|
97
|
+
};
|
|
98
|
+
/** Guest IPv4 addresses the host can use to reach session ports. */
|
|
99
|
+
export const guestAddresses = () => Object.values(os.networkInterfaces()).flat().filter((i) => Boolean(i) && i.family === 'IPv4' && !i.internal).map((i) => i.address);
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// Claude Code managed settings for the sandbox (C:\Program Files\ClaudeCode\managed-settings.json). Managed settings
|
|
2
|
+
// have the highest precedence and cannot be overridden by user or project settings, so they carry the hard rules:
|
|
3
|
+
// today, the git access mode. Rewritten every boot; removed when there is nothing to enforce.
|
|
4
|
+
import fs from 'node:fs';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { gitDenyRules } from '../gitrules.js';
|
|
7
|
+
import { managedClaudeMd } from '../guide.js';
|
|
8
|
+
export const managedSettingsFile = () => path.join(path.dirname(managedClaudeMd()), 'managed-settings.json');
|
|
9
|
+
export const writeManagedSettings = (access) => {
|
|
10
|
+
const file = managedSettingsFile();
|
|
11
|
+
const deny = gitDenyRules(access);
|
|
12
|
+
if (deny.length === 0) {
|
|
13
|
+
fs.rmSync(file, { force: true });
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
17
|
+
fs.writeFileSync(file, JSON.stringify({ permissions: { deny } }, null, 2) + '\n', 'utf8');
|
|
18
|
+
};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** Imports the host's look (registry values + wallpaper) and restarts Explorer so it applies. */
|
|
2
|
+
export declare const applyPersonalization: () => void;
|
|
3
|
+
/**
|
|
4
|
+
* Your Git identity from this PC (name/email only), plus safe.directory for the mapped workspace (its owner SID
|
|
5
|
+
* differs in the sandbox). Uses the real git.exe directly: the read-only guard would rightly refuse `config --add`.
|
|
6
|
+
*/
|
|
7
|
+
export declare const setupGitConfig: (env: NodeJS.ProcessEnv) => void;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// Making the sandbox feel like your PC: desktop look and Git identity.
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { spawnSync } from 'node:child_process';
|
|
6
|
+
import { SYSTEM } from '../../shared/system.js';
|
|
7
|
+
import { dryRun, exec, exists, isWin, launchDir, log, mkdirp, powershell, session, toolsDir } from '../env.js';
|
|
8
|
+
const psQuote = (s) => "'" + s.replace(/'/g, "''") + "'";
|
|
9
|
+
/** Imports the host's look (registry values + wallpaper) and restarts Explorer so it applies. */
|
|
10
|
+
export const applyPersonalization = () => {
|
|
11
|
+
const dir = path.join(launchDir, 'personalize');
|
|
12
|
+
if (!session.personalization || !exists(dir) || dryRun || !isWin)
|
|
13
|
+
return;
|
|
14
|
+
exec(SYSTEM.reg(), ['import', path.join(dir, 'personalize.reg')], { stdio: 'ignore' });
|
|
15
|
+
const wallpaper = fs.readdirSync(dir).find((f) => f.startsWith('wallpaper'));
|
|
16
|
+
const local = wallpaper ? path.join(mkdirp(path.join(os.homedir(), 'Pictures')), wallpaper) : null;
|
|
17
|
+
if (wallpaper && local)
|
|
18
|
+
fs.copyFileSync(path.join(dir, wallpaper), local);
|
|
19
|
+
powershell([
|
|
20
|
+
local ? "Set-ItemProperty -Path 'HKCU:\\Control Panel\\Desktop' -Name WallPaper -Value " + psQuote(local) : '',
|
|
21
|
+
"Add-Type -Namespace AgentSandbox -Name Desk -MemberDefinition '[System.Runtime.InteropServices.DllImport(\"user32.dll\", CharSet = System.Runtime.InteropServices.CharSet.Unicode)] public static extern int SystemParametersInfo(int a, int b, string c, int d);'",
|
|
22
|
+
local ? '[void][AgentSandbox.Desk]::SystemParametersInfo(20, 0, ' + psQuote(local) + ', 3)' : '',
|
|
23
|
+
// Explorer restarts by itself and picks up taskbar/theme changes.
|
|
24
|
+
'Stop-Process -Name explorer -Force -ErrorAction SilentlyContinue',
|
|
25
|
+
].filter(Boolean).join('\n'));
|
|
26
|
+
log('Desktop personalisation applied from this PC.');
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* Your Git identity from this PC (name/email only), plus safe.directory for the mapped workspace (its owner SID
|
|
30
|
+
* differs in the sandbox). Uses the real git.exe directly: the read-only guard would rightly refuse `config --add`.
|
|
31
|
+
*/
|
|
32
|
+
export const setupGitConfig = (env) => {
|
|
33
|
+
// A dry run (the self-test) must not touch the real ~/.gitconfig: it once replaced the sandbox's own, safe.directory
|
|
34
|
+
// included, so git refused the workspace until the next boot.
|
|
35
|
+
if (dryRun)
|
|
36
|
+
return;
|
|
37
|
+
const source = path.join(launchDir, 'home', '.gitconfig');
|
|
38
|
+
if (exists(source))
|
|
39
|
+
fs.copyFileSync(source, path.join(os.homedir(), '.gitconfig'));
|
|
40
|
+
const git = path.join(toolsDir, 'git', 'cmd', 'git.exe');
|
|
41
|
+
if (!exists(git))
|
|
42
|
+
return;
|
|
43
|
+
const { GIT_DIR: _gitDir, ...rest } = env;
|
|
44
|
+
spawnSync(git, ['config', '--global', '--add', 'safe.directory', '*'], { env: rest, windowsHide: true, stdio: 'ignore' });
|
|
45
|
+
// Windows long paths are on in the sandbox, but git skips files over 260 characters unless told.
|
|
46
|
+
spawnSync(git, ['config', '--global', 'core.longpaths', 'true'], { env: rest, windowsHide: true, stdio: 'ignore' });
|
|
47
|
+
};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { SpawnOptions } from 'node:child_process';
|
|
2
|
+
/**
|
|
3
|
+
* Returns the installer for one crate: `cargo binstall` (prebuilt only: its own compile fallback is off, so a crate
|
|
4
|
+
* without a Windows build is compiled by our `cargo install` with the linker settings in `cargoEnv`), then
|
|
5
|
+
* `cargo install --locked`. cargo-binstall is fetched on first use, so boots with every crate installed download nothing.
|
|
6
|
+
*/
|
|
7
|
+
export declare const crateInstaller: (cargoHome: string, cargoEnv: SpawnOptions) => ((crate: string) => Promise<void>);
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// toolchains.rust.crates with prebuilt binaries: cargo-binstall (from its own GitHub release) fetches a crate's
|
|
2
|
+
// published Windows build in seconds; only crates without one are compiled with `cargo install --locked`.
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { githubAsset, installArchive } from '../../shared/download.js';
|
|
6
|
+
import { errMsg, execAsync, exists, log, warn } from '../env.js';
|
|
7
|
+
import { binstallAsset } from './catalogue.js';
|
|
8
|
+
import { arch, tool } from './common.js';
|
|
9
|
+
/**
|
|
10
|
+
* cargo-binstall.exe into CARGO_HOME\bin (so `cargo binstall` works for the agent too). Downloaded into its own
|
|
11
|
+
* tools\cargo-binstall folder first: installArchive replaces its destination, which must never be cargo's bin.
|
|
12
|
+
*/
|
|
13
|
+
const ensureBinstall = async (cargoBin) => {
|
|
14
|
+
const target = path.join(cargoBin, 'cargo-binstall.exe');
|
|
15
|
+
if (exists(target))
|
|
16
|
+
return true;
|
|
17
|
+
try {
|
|
18
|
+
const dir = tool('cargo-binstall');
|
|
19
|
+
if (!exists(path.join(dir, 'cargo-binstall.exe'))) {
|
|
20
|
+
const asset = await githubAsset('cargo-bins/cargo-binstall', binstallAsset(arch));
|
|
21
|
+
log('Downloading ' + asset.name + ' (prebuilt Rust tools) ...');
|
|
22
|
+
await installArchive(asset.url, dir, { sha256: asset.sha256, locate: 'cargo-binstall.exe', fileName: asset.name });
|
|
23
|
+
}
|
|
24
|
+
fs.copyFileSync(path.join(dir, 'cargo-binstall.exe'), target);
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
catch (e) {
|
|
28
|
+
warn('cargo-binstall unavailable, compiling crates with cargo install instead: ' + errMsg(e));
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* Returns the installer for one crate: `cargo binstall` (prebuilt only: its own compile fallback is off, so a crate
|
|
34
|
+
* without a Windows build is compiled by our `cargo install` with the linker settings in `cargoEnv`), then
|
|
35
|
+
* `cargo install --locked`. cargo-binstall is fetched on first use, so boots with every crate installed download nothing.
|
|
36
|
+
*/
|
|
37
|
+
export const crateInstaller = (cargoHome, cargoEnv) => {
|
|
38
|
+
const cargo = path.join(cargoHome, 'bin', 'cargo.exe');
|
|
39
|
+
let binstall = null;
|
|
40
|
+
// quick-install's install statistics are opt-out; the sandbox opts out.
|
|
41
|
+
const binstallEnv = { ...cargoEnv, env: { ...(cargoEnv.env ?? process.env), BINSTALL_DISABLE_TELEMETRY: 'true' } };
|
|
42
|
+
return async (crate) => {
|
|
43
|
+
binstall ??= ensureBinstall(path.join(cargoHome, 'bin'));
|
|
44
|
+
if (await binstall) {
|
|
45
|
+
try {
|
|
46
|
+
await execAsync(cargo, ['binstall', '--no-confirm', '--locked', '--disable-strategies', 'compile', crate], binstallEnv);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
catch (e) {
|
|
50
|
+
log('No prebuilt ' + crate + ' for Windows (' + errMsg(e) + '); compiling it with cargo install ...');
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
await execAsync(cargo, ['install', '--locked', crate], cargoEnv);
|
|
54
|
+
};
|
|
55
|
+
};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { GitAccess } from '../../core/types.js';
|
|
2
|
+
export type Arch = 'x64' | 'arm64';
|
|
3
|
+
/** PowerShell/PowerShell: the portable zip (PowerShell-7.6.6-win-x64.zip), never the MSI or the fxdependent builds. */
|
|
4
|
+
export declare const pwshAsset: (arch: Arch) => RegExp;
|
|
5
|
+
/** cargo-bins/cargo-binstall: the plain zip (the .full build adds nothing we use; .sig files are signatures). */
|
|
6
|
+
export declare const binstallAsset: (arch: Arch) => RegExp;
|
|
7
|
+
/**
|
|
8
|
+
* One portable command-line tool from a GitHub release. The archive is unpacked into tools\cli\<name>; without
|
|
9
|
+
* `subdir` only the folder holding `exe` is kept (and goes on PATH), with `subdir` the whole archive stays and that
|
|
10
|
+
* folder inside it goes on PATH (for tools whose exe needs files next to it).
|
|
11
|
+
*/
|
|
12
|
+
export interface CliTool {
|
|
13
|
+
name: string;
|
|
14
|
+
repo: string;
|
|
15
|
+
asset: (arch: Arch) => RegExp;
|
|
16
|
+
exe: string;
|
|
17
|
+
subdir?: string;
|
|
18
|
+
/** What the tool is for (summary and docs). */
|
|
19
|
+
about: string;
|
|
20
|
+
}
|
|
21
|
+
export declare const CLI_TOOLS: readonly [{
|
|
22
|
+
readonly name: "gh";
|
|
23
|
+
readonly repo: "cli/cli";
|
|
24
|
+
readonly asset: (arch: Arch) => RegExp;
|
|
25
|
+
readonly exe: "gh.exe";
|
|
26
|
+
readonly about: "GitHub CLI";
|
|
27
|
+
}, {
|
|
28
|
+
readonly name: "zstd";
|
|
29
|
+
readonly repo: "facebook/zstd";
|
|
30
|
+
readonly asset: () => RegExp;
|
|
31
|
+
readonly exe: "zstd.exe";
|
|
32
|
+
readonly about: "Zstandard compression";
|
|
33
|
+
}];
|
|
34
|
+
export type CliName = (typeof CLI_TOOLS)[number]['name'] | 'openssl';
|
|
35
|
+
export declare const CLI_NAMES: readonly CliName[];
|
|
36
|
+
/** toolchains.cli: each tool true, false or "auto" (gh: when the project has a .github folder; zstd: never by itself). */
|
|
37
|
+
export type CliSettings = Partial<Record<CliName, boolean | 'auto'>>;
|
|
38
|
+
export interface CliContext {
|
|
39
|
+
gitAccess: GitAccess;
|
|
40
|
+
/** The workspace has a .github folder (workflows, CODEOWNERS ...). */
|
|
41
|
+
hasGithubDir: boolean;
|
|
42
|
+
}
|
|
43
|
+
export interface CliDecision {
|
|
44
|
+
on: boolean;
|
|
45
|
+
reason: string;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Which catalogue tools a sandbox gets. gh is never installed unless git access is "full": gh pushes, merges and edits
|
|
49
|
+
* branches through the GitHub API, so it would walk straight past a "readonly" (or "none") git guard.
|
|
50
|
+
*/
|
|
51
|
+
export declare const decideCli: (settings: CliSettings, ctx: CliContext) => Record<CliName, CliDecision>;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/** PowerShell/PowerShell: the portable zip (PowerShell-7.6.6-win-x64.zip), never the MSI or the fxdependent builds. */
|
|
2
|
+
export const pwshAsset = (arch) => (arch === 'arm64' ? /^PowerShell-\d+\.\d+\.\d+-win-arm64\.zip$/ : /^PowerShell-\d+\.\d+\.\d+-win-x64\.zip$/);
|
|
3
|
+
/** cargo-bins/cargo-binstall: the plain zip (the .full build adds nothing we use; .sig files are signatures). */
|
|
4
|
+
export const binstallAsset = (arch) => (arch === 'arm64' ? /^cargo-binstall-aarch64-pc-windows-msvc\.zip$/ : /^cargo-binstall-x86_64-pc-windows-msvc\.zip$/);
|
|
5
|
+
export const CLI_TOOLS = [
|
|
6
|
+
// gh_2.102.0_windows_amd64.zip holds bin\gh.exe.
|
|
7
|
+
{ name: 'gh', repo: 'cli/cli', asset: (arch) => (arch === 'arm64' ? /^gh_\d+\.\d+\.\d+_windows_arm64\.zip$/ : /^gh_\d+\.\d+\.\d+_windows_amd64\.zip$/), exe: 'gh.exe', about: 'GitHub CLI' },
|
|
8
|
+
// zstd-v1.5.7-win64.zip; there is no arm64 build, and Windows on Arm runs the x64 one.
|
|
9
|
+
{ name: 'zstd', repo: 'facebook/zstd', asset: () => /^zstd-v\d+\.\d+\.\d+-win64\.zip$/, exe: 'zstd.exe', about: 'Zstandard compression' },
|
|
10
|
+
];
|
|
11
|
+
export const CLI_NAMES = [...CLI_TOOLS.map((t) => t.name), 'openssl'];
|
|
12
|
+
/**
|
|
13
|
+
* Which catalogue tools a sandbox gets. gh is never installed unless git access is "full": gh pushes, merges and edits
|
|
14
|
+
* branches through the GitHub API, so it would walk straight past a "readonly" (or "none") git guard.
|
|
15
|
+
*/
|
|
16
|
+
export const decideCli = (settings, ctx) => {
|
|
17
|
+
const decide = (name) => {
|
|
18
|
+
const setting = settings[name] ?? false;
|
|
19
|
+
if (name === 'gh' && setting !== false && ctx.gitAccess !== 'full')
|
|
20
|
+
return { on: false, reason: 'needs provisioning.git.access "full" (gh can push and merge through the GitHub API)' };
|
|
21
|
+
if (setting !== 'auto')
|
|
22
|
+
return { on: setting, reason: setting ? 'enabled in config' : 'disabled in config' };
|
|
23
|
+
if (name === 'gh' && ctx.hasGithubDir)
|
|
24
|
+
return { on: true, reason: '.github' };
|
|
25
|
+
// openssl is only a shim onto Git's own copy: free, so "auto" means on.
|
|
26
|
+
if (name === 'openssl')
|
|
27
|
+
return { on: true, reason: 'Git for Windows ships it' };
|
|
28
|
+
return { on: false, reason: 'not listed in toolchains.cli' };
|
|
29
|
+
};
|
|
30
|
+
return Object.fromEntries(CLI_NAMES.map((name) => [name, decide(name)]));
|
|
31
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { GitAccess } from '../../core/types.js';
|
|
2
|
+
import { type CliSettings } from './catalogue.js';
|
|
3
|
+
import { type ToolEnv } from './common.js';
|
|
4
|
+
/** gh keeps `gh auth login` here, under the agent state (persists like Claude's own login; never copied from the PC). */
|
|
5
|
+
export declare const ghConfigDir: () => string;
|
|
6
|
+
/**
|
|
7
|
+
* Installs the tools toolchains.cli asks for. Settings still "auto" here (normally the host decides) use the same
|
|
8
|
+
* rules on the workspace as the host would. The gh rule is enforced here again, whatever the host sent.
|
|
9
|
+
*/
|
|
10
|
+
export declare const setupCli: (env: ToolEnv, settings: CliSettings, gitAccess: GitAccess, refresh?: boolean) => Promise<void>;
|