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
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// workspaceFilter: the sandbox works on a copy of the project without what git ignores, kept in step both ways.
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { IgnoreRules } from '../host/workspace-view/rules.js';
|
|
5
|
+
import { WorkspaceView } from '../host/workspace-view/view.js';
|
|
6
|
+
export const workspaceViewSuite = {
|
|
7
|
+
name: 'workspace-view',
|
|
8
|
+
title: 'git-ignored files kept out of the sandbox (filtered view, two-way sync)',
|
|
9
|
+
run: async (t, f) => {
|
|
10
|
+
t.section('a filtered launch');
|
|
11
|
+
const project = t.dir('filtered-app');
|
|
12
|
+
fs.mkdirSync(path.join(project, '.git'));
|
|
13
|
+
const filtered = { ...f.baseConfig, workspaceFilter: { enabled: true, exclude: [], include: [] } };
|
|
14
|
+
const gen = await f.launch({ cwd: project }, { config: filtered });
|
|
15
|
+
const workspace = gen.mappings.find((m) => m.guest.toLowerCase().endsWith('\\filtered-app'));
|
|
16
|
+
const git = gen.mappings.find((m) => m.guest.toLowerCase().endsWith('\\filtered-app.git'));
|
|
17
|
+
t.check(Boolean(workspace) && !workspace.host.toLowerCase().startsWith(project.toLowerCase()) && workspace.host.includes('views'), 'the sandbox maps the filtered view, not the project folder');
|
|
18
|
+
t.check(git?.host === path.join(project, '.git') && !git.readOnly, '... and the project\'s .git beside it, so git works');
|
|
19
|
+
const noGit = await f.launch({ cwd: project }, { config: { ...filtered, provisioning: { ...filtered.provisioning, git: { ...filtered.provisioning.git, access: 'none' } } } });
|
|
20
|
+
t.check(!noGit.mappings.some((m) => m.guest.toLowerCase().endsWith('.git')), 'with git access "none" no .git is mapped at all');
|
|
21
|
+
t.section('which files git ignores');
|
|
22
|
+
const rulesRoot = t.at('rules');
|
|
23
|
+
t.write(path.join(rulesRoot, '.gitignore'), 'node_modules/\n*.log\n!keep.log\n.env*\n!.env.example\ndist\n');
|
|
24
|
+
t.write(path.join(rulesRoot, 'sub', '.gitignore'), 'secret.txt\n!*.log\n');
|
|
25
|
+
t.write(path.join(rulesRoot, '.git', 'info', 'exclude'), 'local-notes.md\n');
|
|
26
|
+
const rules = new IgnoreRules(rulesRoot, ['*.pem'], ['dist/keep-me.js']);
|
|
27
|
+
const verdicts = Object.fromEntries(['src/a.ts', 'node_modules/x/index.js', 'a.log', 'keep.log', '.env', '.env.local', '.env.example', 'dist/out.js', 'sub/secret.txt', 'sub/b.log', 'local-notes.md', 'key.pem', 'dist/keep-me.js', '.git/config'].map((p) => [p, rules.isIgnored(p)]));
|
|
28
|
+
t.equal(verdicts, {
|
|
29
|
+
'src/a.ts': false, 'node_modules/x/index.js': true, 'a.log': true, 'keep.log': false, '.env': true, '.env.local': true, '.env.example': false,
|
|
30
|
+
'dist/out.js': true, 'sub/secret.txt': true, 'sub/b.log': false, 'local-notes.md': true, 'key.pem': true, 'dist/keep-me.js': false, '.git/config': true,
|
|
31
|
+
}, 'rules: nested .gitignore files, ! exceptions, .git/info/exclude, your own exclude and include patterns');
|
|
32
|
+
t.section('the view');
|
|
33
|
+
const host = t.at('project');
|
|
34
|
+
t.write(path.join(host, '.gitignore'), 'node_modules/\n.env\ndist/\n');
|
|
35
|
+
t.write(path.join(host, 'src', 'main.ts'), 'console.log(1);\n');
|
|
36
|
+
t.write(path.join(host, 'README.md'), '# hi\n');
|
|
37
|
+
t.write(path.join(host, '.env'), 'API_KEY=secret\n');
|
|
38
|
+
t.write(path.join(host, 'node_modules', 'pkg', 'index.js'), 'host build');
|
|
39
|
+
fs.mkdirSync(path.join(host, '.git'), { recursive: true });
|
|
40
|
+
const paths = WorkspaceView.pathsFor(t.at('storage'), host);
|
|
41
|
+
const view = new WorkspaceView({ ...paths, guestGitDir: 'C:\\Workspace\\project.git' });
|
|
42
|
+
view.prepare();
|
|
43
|
+
const inView = (rel) => fs.existsSync(path.join(paths.view, ...rel.split('/')));
|
|
44
|
+
t.check(inView('src/main.ts') && inView('README.md') && inView('.gitignore'), 'view: what git tracks is there');
|
|
45
|
+
t.check(!inView('.env') && !inView('node_modules'), 'view: ignored files (.env, node_modules) are not');
|
|
46
|
+
t.equal(fs.readFileSync(path.join(paths.view, '.git'), 'utf8'), 'gitdir: C:/Workspace/project.git\n', 'view: its .git points at the project\'s real .git (mapped separately)');
|
|
47
|
+
t.check(!paths.view.toLowerCase().startsWith(host.toLowerCase()), 'view: it lives outside the project');
|
|
48
|
+
t.section('both ways');
|
|
49
|
+
t.write(path.join(host, 'src', 'main.ts'), 'console.log(2);\n');
|
|
50
|
+
t.touch(path.join(host, 'src', 'main.ts'), 5);
|
|
51
|
+
view.syncChanged(['src/main.ts']);
|
|
52
|
+
t.equal(fs.readFileSync(path.join(paths.view, 'src', 'main.ts'), 'utf8'), 'console.log(2);\n', 'your edit reaches the sandbox');
|
|
53
|
+
t.write(path.join(paths.view, 'src', 'new.ts'), 'export {};\n');
|
|
54
|
+
t.write(path.join(paths.view, 'node_modules', 'pkg', 'index.js'), 'sandbox build');
|
|
55
|
+
t.write(path.join(paths.view, 'dist', 'bundle.js'), 'built');
|
|
56
|
+
view.syncAll();
|
|
57
|
+
t.check(fs.existsSync(path.join(host, 'src', 'new.ts')), 'a file the agent creates reaches your project');
|
|
58
|
+
t.equal(fs.readFileSync(path.join(host, 'node_modules', 'pkg', 'index.js'), 'utf8'), 'host build', 'the sandbox\'s own node_modules never overwrite yours');
|
|
59
|
+
t.check(!fs.existsSync(path.join(host, 'dist')), 'its build output stays in the sandbox');
|
|
60
|
+
fs.rmSync(path.join(paths.view, 'README.md'));
|
|
61
|
+
view.syncAll();
|
|
62
|
+
t.check(!fs.existsSync(path.join(host, 'README.md')), 'a deletion in the sandbox is repeated in your project');
|
|
63
|
+
fs.rmSync(path.join(host, 'src', 'new.ts'));
|
|
64
|
+
view.syncAll();
|
|
65
|
+
t.check(!inView('src/new.ts'), 'a deletion in your project is repeated in the sandbox');
|
|
66
|
+
t.section('conflicts and changing rules');
|
|
67
|
+
t.write(path.join(host, 'src', 'main.ts'), 'yours\n');
|
|
68
|
+
t.touch(path.join(host, 'src', 'main.ts'), 20);
|
|
69
|
+
t.write(path.join(paths.view, 'src', 'main.ts'), 'sandbox\n');
|
|
70
|
+
t.touch(path.join(paths.view, 'src', 'main.ts'), 30);
|
|
71
|
+
const stats = view.syncAll();
|
|
72
|
+
const conflicts = fs.readdirSync(path.join(host, 'src')).filter((n) => n.includes('.sandbox-conflict-'));
|
|
73
|
+
t.check(stats.conflicts === 1 && fs.readFileSync(path.join(host, 'src', 'main.ts'), 'utf8') === 'yours\n' && conflicts.length === 1 && fs.readFileSync(path.join(host, 'src', conflicts[0]), 'utf8') === 'sandbox\n', 'changed on both sides: yours stays, the sandbox\'s version is kept next to it');
|
|
74
|
+
t.equal(fs.readFileSync(path.join(paths.view, 'src', 'main.ts'), 'utf8'), 'yours\n', '... and the sandbox sees yours');
|
|
75
|
+
t.write(path.join(host, '.gitignore'), 'node_modules/\ndist/\n');
|
|
76
|
+
view.syncChanged(['.gitignore']);
|
|
77
|
+
t.check(inView('.env'), 'a file no longer ignored comes in when .gitignore changes');
|
|
78
|
+
t.equal(view.syncAll(), { toView: 0, toHost: 0, deleted: 0, conflicts: 0, skipped: 0 }, 'a pass with nothing changed copies nothing');
|
|
79
|
+
t.check(view.syncChanged(['../outside.txt', 'C:/Windows/x', '']).toHost === 0, 'paths from outside the project are ignored');
|
|
80
|
+
t.section('live');
|
|
81
|
+
const stop = view.watch(60000);
|
|
82
|
+
t.write(path.join(host, 'live.txt'), 'from you');
|
|
83
|
+
const arrived = await t.waitFor(() => inView('live.txt'), 5000);
|
|
84
|
+
t.write(path.join(paths.view, 'live-back.txt'), 'from the sandbox');
|
|
85
|
+
const back = await t.waitFor(() => fs.existsSync(path.join(host, 'live-back.txt')), 5000);
|
|
86
|
+
stop();
|
|
87
|
+
t.check(Boolean(arrived) && Boolean(back), 'changes on either side arrive within a second or two while it watches');
|
|
88
|
+
},
|
|
89
|
+
};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { Config, GitAccess } from '../core/types.js';
|
|
2
|
+
export type AppCategory = 'editors' | 'databases' | 'api' | 'git' | 'utilities' | 'browsers' | 'gamedev' | 'cloud' | 'diagrams' | 'network' | 'cli';
|
|
3
|
+
export declare const APP_CATEGORIES: {
|
|
4
|
+
id: AppCategory;
|
|
5
|
+
label: string;
|
|
6
|
+
description: string;
|
|
7
|
+
}[];
|
|
8
|
+
/** Where a download comes from. Resolvers (sublime, kubectl ...) are code in guest/apps, named here by id. */
|
|
9
|
+
export type AppSource = {
|
|
10
|
+
github: string;
|
|
11
|
+
asset: string;
|
|
12
|
+
} | {
|
|
13
|
+
url: string;
|
|
14
|
+
} | {
|
|
15
|
+
resolve: string;
|
|
16
|
+
} | {
|
|
17
|
+
npm: string;
|
|
18
|
+
};
|
|
19
|
+
export interface AppEntry {
|
|
20
|
+
id: string;
|
|
21
|
+
name: string;
|
|
22
|
+
category: AppCategory;
|
|
23
|
+
about: string;
|
|
24
|
+
size: string;
|
|
25
|
+
source: AppSource;
|
|
26
|
+
/** How the download is unpacked: an archive (zip/7z/tar, the default), the file itself, an NSIS installer or an MSI. */
|
|
27
|
+
unpack?: 'archive' | 'file' | 'nsis' | 'msi';
|
|
28
|
+
/** The program to start (or the tool on PATH): a file name, or a regex (/.../) for names with a version in them. */
|
|
29
|
+
exe: string;
|
|
30
|
+
/** A command-line tool: its folder goes on PATH (no desktop shortcut). */
|
|
31
|
+
cli?: boolean;
|
|
32
|
+
/** Needs these first: 7-Zip to unpack an installer, Mesa for OpenGL without a GPU. */
|
|
33
|
+
needs?: ('7zip' | 'mesa')[];
|
|
34
|
+
/** Settings elsewhere that this entry stands for (VS Code, Windows Terminal): shown in the catalogue, not installed by it. */
|
|
35
|
+
setting?: string;
|
|
36
|
+
/** Only with full git access (it pushes on its own, past the sandbox's git guard). */
|
|
37
|
+
fullGitOnly?: boolean;
|
|
38
|
+
}
|
|
39
|
+
export declare const APPS: AppEntry[];
|
|
40
|
+
export declare const appById: (id: string) => AppEntry | undefined;
|
|
41
|
+
/** A catalogue `exe` that is a regex ("/.../") matches names with a version in them. */
|
|
42
|
+
export declare const exeMatcher: (exe: string) => ((name: string) => boolean);
|
|
43
|
+
/** The apps to install, in order: 7-Zip first when anything needs it to unpack. */
|
|
44
|
+
export declare const appsToInstall: (apps: Config["apps"], gitAccess: GitAccess) => {
|
|
45
|
+
entries: AppEntry[];
|
|
46
|
+
skipped: string[];
|
|
47
|
+
};
|
|
48
|
+
/** Does any chosen app draw with OpenGL (so the Mesa step must run first)? */
|
|
49
|
+
export declare const appsNeedMesa: (apps: Config["apps"] | undefined) => boolean;
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
export const APP_CATEGORIES = [
|
|
2
|
+
{ id: 'editors', label: 'Editors & IDEs', description: 'Code and text editors (VS Code is a setting of its own and on by default).' },
|
|
3
|
+
{ id: 'databases', label: 'Database clients', description: 'GUIs for the databases the sandbox runs (PostgreSQL, MySQL, MongoDB, Redis, SQLite, DynamoDB).' },
|
|
4
|
+
{ id: 'api', label: 'API clients', description: 'Send HTTP, REST and GraphQL requests to what you are building.' },
|
|
5
|
+
{ id: 'git', label: 'Git tools', description: 'Git GUIs and terminal UIs. They use the sandbox\'s git, so its git access rules apply.' },
|
|
6
|
+
{ id: 'utilities', label: 'Utilities', description: 'Archives, comparing files, finding files, process tools, hex editing.' },
|
|
7
|
+
{ id: 'browsers', label: 'Browsers', description: 'Browsers for testing web apps (Edge is already in Windows).' },
|
|
8
|
+
{ id: 'gamedev', label: 'Game development', description: 'Engines and editors. They draw with OpenGL on the CPU (Mesa) when the sandbox has no GPU.' },
|
|
9
|
+
{ id: 'cloud', label: 'Cloud & Kubernetes', description: 'Command-line tools for clouds, Kubernetes and infrastructure as code.' },
|
|
10
|
+
{ id: 'diagrams', label: 'Diagrams & docs', description: 'Diagrams and document conversion.' },
|
|
11
|
+
{ id: 'network', label: 'Network & HTTP', description: 'Inspect and test HTTP traffic and APIs.' },
|
|
12
|
+
{ id: 'cli', label: 'Command-line tools', description: 'Small tools developers reach for: JSON and YAML, search, diff, builds, media, load testing ...' },
|
|
13
|
+
];
|
|
14
|
+
const gh = (github, asset) => ({ github, asset });
|
|
15
|
+
export const APPS = [
|
|
16
|
+
// ---- editors
|
|
17
|
+
{ id: 'vscode', name: 'Visual Studio Code', category: 'editors', about: 'Code editor with debugging, set up for the sandbox\'s languages.', size: '150 MB', source: { resolve: 'setting' }, exe: 'Code.exe', setting: 'toolchains.vscode.enabled' },
|
|
18
|
+
{ id: 'windows-terminal', name: 'Windows Terminal', category: 'utilities', about: 'The sandbox window: tabs, panes, PowerShell 7 profile.', size: '40 MB', source: { resolve: 'setting' }, exe: 'WindowsTerminal.exe', setting: 'provisioning.terminal.enabled' },
|
|
19
|
+
{ id: 'notepadpp', name: 'Notepad++', category: 'editors', about: 'Fast text and code editor.', size: '8 MB', source: gh('notepad-plus-plus/notepad-plus-plus', '^npp\\.[\\d.]+\\.portable\\.x64\\.zip$'), exe: 'notepad++.exe' },
|
|
20
|
+
{ id: 'sublime', name: 'Sublime Text', category: 'editors', about: 'Fast code editor (paid licence, free to evaluate).', size: '28 MB', source: { resolve: 'sublime' }, exe: 'sublime_text.exe' },
|
|
21
|
+
{ id: 'neovim', name: 'Neovim', category: 'editors', about: 'Terminal text editor (nvim).', size: '12 MB', source: gh('neovim/neovim', '^nvim-win64\\.zip$'), exe: 'nvim.exe', cli: true },
|
|
22
|
+
// ---- database clients
|
|
23
|
+
{ id: 'dbeaver', name: 'DBeaver Community', category: 'databases', about: 'Universal database client: PostgreSQL, MySQL, SQLite, SQL Server and more.', size: '120 MB', source: gh('dbeaver/dbeaver', '^dbeaver-ce-[\\d.]+-windows-x86_64\\.zip$'), exe: 'dbeaver.exe' },
|
|
24
|
+
{ id: 'pgadmin', name: 'pgAdmin 4', category: 'databases', about: 'The PostgreSQL admin GUI (from the PostgreSQL binaries zip).', size: '330 MB', source: { resolve: 'pgadmin' }, exe: 'pgAdmin4.exe' },
|
|
25
|
+
{ id: 'heidisql', name: 'HeidiSQL', category: 'databases', about: 'Light client for MySQL, MariaDB, PostgreSQL and SQL Server.', size: '28 MB', source: gh('HeidiSQL/HeidiSQL', '^HeidiSQL_[\\d.]+_64_Portable\\.zip$'), exe: 'heidisql.exe' },
|
|
26
|
+
{ id: 'sqlitebrowser', name: 'DB Browser for SQLite', category: 'databases', about: 'Open, browse and edit SQLite files.', size: '21 MB', source: gh('sqlitebrowser/sqlitebrowser', '^DB\\.Browser\\.for\\.SQLite-v?[\\d.]+-win64\\.zip$'), exe: 'DB Browser for SQLite.exe' },
|
|
27
|
+
{ id: 'compass', name: 'MongoDB Compass', category: 'databases', about: 'The MongoDB GUI.', size: '220 MB', source: gh('mongodb-js/compass', '^mongodb-compass-[\\d.]+-win32-x64\\.zip$'), exe: 'MongoDBCompass.exe' },
|
|
28
|
+
{ id: 'redisinsight', name: 'Redis Insight', category: 'databases', about: 'The Redis GUI.', size: '110 MB', source: gh('redis/RedisInsight', '^Redis-Insight-win-installer\\.exe$'), unpack: 'nsis', exe: 'Redis Insight.exe', needs: ['7zip'] },
|
|
29
|
+
{ id: 'dbgate', name: 'DbGate', category: 'databases', about: 'Database manager for SQL and NoSQL databases.', size: '180 MB', source: gh('dbgate/dbgate', '^dbgate-[\\d.]+-win_x64\\.zip$'), exe: 'DbGate.exe' },
|
|
30
|
+
{ id: 'beekeeper', name: 'Beekeeper Studio', category: 'databases', about: 'SQL editor and database manager.', size: '245 MB', source: gh('beekeeper-studio/beekeeper-studio', '^Beekeeper-Studio-[\\d.]+-portable\\.exe$'), unpack: 'nsis', exe: 'Beekeeper Studio.exe', needs: ['7zip'] },
|
|
31
|
+
{ id: 'dynamodb-admin', name: 'dynamodb-admin', category: 'databases', about: 'Web GUI for DynamoDB Local: run dynamodb-admin, then open http://localhost:8001.', size: '5 MB', source: { npm: 'dynamodb-admin' }, exe: 'dynamodb-admin.cmd', cli: true },
|
|
32
|
+
// ---- API clients
|
|
33
|
+
{ id: 'bruno', name: 'Bruno', category: 'api', about: 'Offline, git-friendly API client (collections are files in your repo).', size: '175 MB', source: gh('usebruno/bruno', '^bruno_[\\d.]+_x64_win\\.zip$'), exe: 'Bruno.exe' },
|
|
34
|
+
{ id: 'insomnia', name: 'Insomnia', category: 'api', about: 'REST, GraphQL and gRPC client.', size: '590 MB', source: gh('Kong/insomnia', '^insomnia-[\\d.]+-full\\.nupkg$'), exe: 'Insomnia.exe' },
|
|
35
|
+
// ---- git
|
|
36
|
+
{ id: 'sourcegit', name: 'SourceGit', category: 'git', about: 'Fast open-source Git GUI.', size: '26 MB', source: gh('sourcegit-scm/sourcegit', '^sourcegit_[\\d.]+\\.win-x64\\.zip$'), exe: 'SourceGit.exe' },
|
|
37
|
+
{ id: 'githubdesktop', name: 'GitHub Desktop', category: 'git', about: 'Simple Git GUI from GitHub. It has its own git, so it needs provisioning.git.access "full".', size: '300 MB', source: gh('desktop/desktop', '^GitHubDesktop-[\\d.]+-x64-full\\.nupkg$'), exe: 'GitHubDesktop.exe', fullGitOnly: true },
|
|
38
|
+
{ id: 'lazygit', name: 'lazygit', category: 'git', about: 'Git in the terminal.', size: '7 MB', source: gh('jesseduffield/lazygit', '^lazygit_[\\d.]+_windows_x86_64\\.zip$'), exe: 'lazygit.exe', cli: true },
|
|
39
|
+
// ---- utilities
|
|
40
|
+
{ id: '7zip', name: '7-Zip', category: 'utilities', about: 'Archive manager (7z, zip, rar, tar ...). Also unpacks installers for other apps.', size: '2 MB', source: { resolve: '7zip' }, exe: '7zFM.exe' },
|
|
41
|
+
{ id: 'winmerge', name: 'WinMerge', category: 'utilities', about: 'Compare and merge files and folders.', size: '23 MB', source: gh('WinMerge/winmerge', '^winmerge-[\\d.]+-x64-exe\\.zip$'), exe: 'WinMergeU.exe' },
|
|
42
|
+
{ id: 'everything', name: 'Everything', category: 'utilities', about: 'Finds files by name instantly.', size: '2 MB', source: { resolve: 'everything' }, exe: 'Everything.exe' },
|
|
43
|
+
{ id: 'sysinternals', name: 'Sysinternals Suite', category: 'utilities', about: 'Process Explorer, Process Monitor, Autoruns, TCPView and more (on PATH).', size: '200 MB', source: { url: 'https://download.sysinternals.com/files/SysinternalsSuite.zip' }, exe: 'procexp64.exe' },
|
|
44
|
+
{ id: 'imhex', name: 'ImHex', category: 'utilities', about: 'Hex editor for reverse engineering and binary formats.', size: '80 MB', source: gh('WerWolv/ImHex', '^imhex-[\\d.]+-Windows-Portable-NoGPU-x86_64\\.zip$'), exe: 'imhex-gui.exe' },
|
|
45
|
+
// ---- browsers
|
|
46
|
+
{ id: 'chrome', name: 'Chrome for Testing', category: 'browsers', about: 'Chrome build made for automated tests (with chromedriver on PATH).', size: '170 MB', source: { resolve: 'chrome' }, exe: 'chrome.exe' },
|
|
47
|
+
{ id: 'firefox', name: 'Firefox', category: 'browsers', about: 'Firefox for testing.', size: '75 MB', source: { resolve: 'firefox' }, unpack: 'nsis', exe: 'firefox.exe', needs: ['7zip'] },
|
|
48
|
+
// ---- game development
|
|
49
|
+
{ id: 'godot', name: 'Godot', category: 'gamedev', about: 'Open-source 2D and 3D game engine.', size: '80 MB', source: gh('godotengine/godot', '^Godot_v[\\d.]+-stable_win64\\.exe\\.zip$'), exe: '/^Godot_v[\\d.]+-stable_win64\\.exe$/', needs: ['mesa'] },
|
|
50
|
+
{ id: 'blender', name: 'Blender', category: 'gamedev', about: '3D modelling, animation and rendering.', size: '400 MB', source: { resolve: 'blender' }, exe: 'blender.exe', needs: ['mesa'] },
|
|
51
|
+
{ id: 'love', name: 'LÖVE', category: 'gamedev', about: 'Lua framework for 2D games.', size: '4 MB', source: gh('love2d/love', '^love-[\\d.]+-win64\\.zip$'), exe: 'love.exe', needs: ['mesa'] },
|
|
52
|
+
{ id: 'defold', name: 'Defold', category: 'gamedev', about: 'Free 2D/3D game engine and editor.', size: '330 MB', source: gh('defold/defold', '^Defold-x86_64-win32\\.zip$'), exe: 'Defold.exe', needs: ['mesa'] },
|
|
53
|
+
{ id: 'pixelorama', name: 'Pixelorama', category: 'gamedev', about: 'Pixel-art and sprite editor.', size: '40 MB', source: gh('Orama-Interactive/Pixelorama', '^Pixelorama-Windows-64bit\\.zip$'), exe: 'Pixelorama.exe', needs: ['mesa'] },
|
|
54
|
+
{ id: 'tiled', name: 'Tiled', category: 'gamedev', about: 'Tile map editor.', size: '22 MB', source: gh('mapeditor/tiled', '^Tiled-[\\d.]+_Windows-10\\+_x86_64\\.msi$'), unpack: 'msi', exe: 'tiled.exe' },
|
|
55
|
+
// ---- cloud
|
|
56
|
+
{ id: 'kubectl', name: 'kubectl', category: 'cloud', about: 'Kubernetes command line.', size: '60 MB', source: { resolve: 'kubectl' }, unpack: 'file', exe: 'kubectl.exe', cli: true },
|
|
57
|
+
{ id: 'helm', name: 'Helm', category: 'cloud', about: 'Kubernetes package manager.', size: '20 MB', source: { resolve: 'helm' }, exe: 'helm.exe', cli: true },
|
|
58
|
+
{ id: 'k9s', name: 'k9s', category: 'cloud', about: 'Kubernetes cluster UI in the terminal.', size: '40 MB', source: gh('derailed/k9s', '^k9s_Windows_amd64\\.zip$'), exe: 'k9s.exe', cli: true },
|
|
59
|
+
{ id: 'terraform', name: 'Terraform', category: 'cloud', about: 'Infrastructure as code.', size: '30 MB', source: { resolve: 'terraform' }, exe: 'terraform.exe', cli: true },
|
|
60
|
+
{ id: 'azure-cli', name: 'Azure CLI', category: 'cloud', about: 'Manage Azure (az).', size: '90 MB', source: { url: 'https://aka.ms/installazurecliwindowszipx64' }, exe: 'az.cmd', cli: true },
|
|
61
|
+
{ id: 'gcloud', name: 'Google Cloud CLI', category: 'cloud', about: 'Manage Google Cloud (gcloud, gsutil, bq).', size: '120 MB', source: { url: 'https://dl.google.com/dl/cloudsdk/channels/rapid/downloads/google-cloud-cli-windows-x86_64-bundled-python.zip' }, exe: 'gcloud.cmd', cli: true },
|
|
62
|
+
{ id: 'aws-cli', name: 'AWS CLI', category: 'cloud', about: 'Manage AWS (aws).', size: '50 MB', source: { url: 'https://awscli.amazonaws.com/AWSCLIV2.msi' }, unpack: 'msi', exe: 'aws.exe', cli: true },
|
|
63
|
+
// ---- diagrams and docs
|
|
64
|
+
{ id: 'drawio', name: 'draw.io', category: 'diagrams', about: 'Diagram editor (flowcharts, architecture, UML).', size: '170 MB', source: gh('jgraph/drawio-desktop', '^draw\\.io-[\\d.]+-windows\\.zip$'), exe: 'draw.io.exe' },
|
|
65
|
+
{ id: 'pandoc', name: 'Pandoc', category: 'diagrams', about: 'Convert documents between Markdown, HTML, Word, PDF ...', size: '35 MB', source: gh('jgm/pandoc', '^pandoc-[\\d.]+-windows-x86_64\\.zip$'), exe: 'pandoc.exe', cli: true },
|
|
66
|
+
// ---- network
|
|
67
|
+
{ id: 'mitmproxy', name: 'mitmproxy', category: 'network', about: 'Intercepting HTTPS proxy (mitmweb opens a web UI).', size: '90 MB', source: { resolve: 'mitmproxy' }, exe: 'mitmweb.exe', cli: true },
|
|
68
|
+
{ id: 'xh', name: 'xh', category: 'network', about: 'Friendly HTTP requests from the terminal (like HTTPie).', size: '5 MB', source: gh('ducaale/xh', '^xh-v[\\d.]+-x86_64-pc-windows-msvc\\.zip$'), exe: 'xh.exe', cli: true },
|
|
69
|
+
{ id: 'grpcurl', name: 'grpcurl', category: 'network', about: 'curl for gRPC servers.', size: '10 MB', source: gh('fullstorydev/grpcurl', '^grpcurl_[\\d.]+_windows_x86_64\\.zip$'), exe: 'grpcurl.exe', cli: true },
|
|
70
|
+
{ id: 'k6', name: 'k6', category: 'network', about: 'Load testing with JavaScript scripts.', size: '30 MB', source: gh('grafana/k6', '^k6-v[\\d.]+-windows-amd64\\.zip$'), exe: 'k6.exe', cli: true },
|
|
71
|
+
// ---- command-line tools
|
|
72
|
+
{ id: 'jq', name: 'jq', category: 'cli', about: 'Query and reshape JSON.', size: '1 MB', source: gh('jqlang/jq', '^jq-windows-amd64\\.exe$'), unpack: 'file', exe: 'jq.exe', cli: true },
|
|
73
|
+
{ id: 'yq', name: 'yq', category: 'cli', about: 'Query and edit YAML, JSON, XML and TOML.', size: '10 MB', source: gh('mikefarah/yq', '^yq_windows_amd64\\.exe$'), unpack: 'file', exe: 'yq.exe', cli: true },
|
|
74
|
+
{ id: 'ripgrep', name: 'ripgrep', category: 'cli', about: 'Very fast recursive search (rg).', size: '2 MB', source: gh('BurntSushi/ripgrep', '^ripgrep-[\\d.]+-x86_64-pc-windows-msvc\\.zip$'), exe: 'rg.exe', cli: true },
|
|
75
|
+
{ id: 'fd', name: 'fd', category: 'cli', about: 'Fast, friendly alternative to find.', size: '2 MB', source: gh('sharkdp/fd', '^fd-v[\\d.]+-x86_64-pc-windows-msvc\\.zip$'), exe: 'fd.exe', cli: true },
|
|
76
|
+
{ id: 'bat', name: 'bat', category: 'cli', about: 'cat with syntax highlighting and git changes.', size: '3 MB', source: gh('sharkdp/bat', '^bat-v[\\d.]+-x86_64-pc-windows-msvc\\.zip$'), exe: 'bat.exe', cli: true },
|
|
77
|
+
{ id: 'fzf', name: 'fzf', category: 'cli', about: 'Fuzzy finder for the terminal.', size: '2 MB', source: gh('junegunn/fzf', '^fzf-[\\d.]+-windows_amd64\\.zip$'), exe: 'fzf.exe', cli: true },
|
|
78
|
+
{ id: 'delta', name: 'delta', category: 'cli', about: 'Readable git diffs with syntax highlighting.', size: '3 MB', source: gh('dandavison/delta', '^delta-[\\d.]+-x86_64-pc-windows-msvc\\.zip$'), exe: 'delta.exe', cli: true },
|
|
79
|
+
{ id: 'just', name: 'just', category: 'cli', about: 'Command runner (a simpler make for project tasks).', size: '2 MB', source: gh('casey/just', '^just-[\\d.]+-x86_64-pc-windows-msvc\\.zip$'), exe: 'just.exe', cli: true },
|
|
80
|
+
{ id: 'hyperfine', name: 'hyperfine', category: 'cli', about: 'Benchmark commands.', size: '1 MB', source: gh('sharkdp/hyperfine', '^hyperfine-v[\\d.]+-x86_64-pc-windows-msvc\\.zip$'), exe: 'hyperfine.exe', cli: true },
|
|
81
|
+
{ id: 'watchexec', name: 'watchexec', category: 'cli', about: 'Run a command when files change.', size: '5 MB', source: gh('watchexec/watchexec', '^watchexec-[\\d.]+-x86_64-pc-windows-msvc\\.zip$'), exe: 'watchexec.exe', cli: true },
|
|
82
|
+
{ id: 'protoc', name: 'protoc', category: 'cli', about: 'Protocol Buffers compiler.', size: '3 MB', source: gh('protocolbuffers/protobuf', '^protoc-[\\d.]+-win64\\.zip$'), exe: 'protoc.exe', cli: true },
|
|
83
|
+
{ id: 'ninja', name: 'Ninja', category: 'cli', about: 'Small, fast build system (CMake\'s favourite generator).', size: '1 MB', source: gh('ninja-build/ninja', '^ninja-win\\.zip$'), exe: 'ninja.exe', cli: true },
|
|
84
|
+
{ id: 'ffmpeg', name: 'FFmpeg', category: 'cli', about: 'Convert and process audio and video.', size: '200 MB', source: gh('BtbN/FFmpeg-Builds', '^ffmpeg-master-latest-win64-gpl\\.zip$'), exe: 'ffmpeg.exe', cli: true },
|
|
85
|
+
{ id: 'sqlite', name: 'SQLite tools', category: 'cli', about: 'sqlite3 command-line shell.', size: '5 MB', source: { resolve: 'sqlite' }, exe: 'sqlite3.exe', cli: true },
|
|
86
|
+
{ id: 'shellcheck', name: 'ShellCheck', category: 'cli', about: 'Finds bugs in shell scripts.', size: '10 MB', source: gh('koalaman/shellcheck', '^shellcheck-v[\\d.]+\\.zip$'), exe: 'shellcheck.exe', cli: true },
|
|
87
|
+
{ id: 'gitleaks', name: 'Gitleaks', category: 'cli', about: 'Finds secrets committed to git.', size: '8 MB', source: gh('gitleaks/gitleaks', '^gitleaks_[\\d.]+_windows_x64\\.zip$'), exe: 'gitleaks.exe', cli: true },
|
|
88
|
+
];
|
|
89
|
+
export const appById = (id) => APPS.find((a) => a.id === id);
|
|
90
|
+
/** A catalogue `exe` that is a regex ("/.../") matches names with a version in them. */
|
|
91
|
+
export const exeMatcher = (exe) => {
|
|
92
|
+
const regex = /^\/(.+)\/$/.exec(exe);
|
|
93
|
+
if (regex) {
|
|
94
|
+
const r = new RegExp(regex[1], 'i');
|
|
95
|
+
return (name) => r.test(name);
|
|
96
|
+
}
|
|
97
|
+
return (name) => name.toLowerCase() === exe.toLowerCase();
|
|
98
|
+
};
|
|
99
|
+
/** Your own program as a catalogue entry (its folder is apps\custom\<id>). */
|
|
100
|
+
const customEntry = (id, app) => ({
|
|
101
|
+
id: 'custom/' + id, name: app.name, category: 'utilities', about: 'Your own program.', size: '?',
|
|
102
|
+
source: app.github && app.asset ? { github: app.github, asset: app.asset } : { url: app.url },
|
|
103
|
+
unpack: app.unpack, exe: app.exe, cli: app.cli,
|
|
104
|
+
});
|
|
105
|
+
/** The apps to install, in order: 7-Zip first when anything needs it to unpack. */
|
|
106
|
+
export const appsToInstall = (apps, gitAccess) => {
|
|
107
|
+
const chosen = apps.install.flatMap((id) => { const e = appById(id); return e && !e.setting ? [e] : []; });
|
|
108
|
+
const skipped = chosen.filter((e) => e.fullGitOnly && gitAccess !== 'full').map((e) => e.name + ' (needs provisioning.git.access "full")');
|
|
109
|
+
const allowed = [...chosen.filter((e) => !(e.fullGitOnly && gitAccess !== 'full')), ...Object.entries(apps.custom).map(([id, app]) => customEntry(id, app))];
|
|
110
|
+
const needs7zip = allowed.some((e) => e.needs?.includes('7zip') || e.unpack === 'nsis') && !allowed.some((e) => e.id === '7zip');
|
|
111
|
+
return { entries: [...(needs7zip ? [appById('7zip')] : []), ...allowed], skipped };
|
|
112
|
+
};
|
|
113
|
+
/** Does any chosen app draw with OpenGL (so the Mesa step must run first)? */
|
|
114
|
+
export const appsNeedMesa = (apps) => Boolean(apps?.install.some((id) => appById(id)?.needs?.includes('mesa')));
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/** A host name or a "*.example.com" pattern (lower case, no scheme, no port). */
|
|
2
|
+
export declare const HOST_PATTERN: RegExp;
|
|
3
|
+
/**
|
|
4
|
+
* What "@developer" stands for in allowOnly: package registries, source hosts, toolchain downloads and the AI
|
|
5
|
+
* providers the agents talk to. Enough for an agent to install dependencies, push to GitHub and reach its model.
|
|
6
|
+
*/
|
|
7
|
+
export declare const DEVELOPER_HOSTS: string[];
|
|
8
|
+
/** The allowOnly list with "@developer" expanded, lower case, without duplicates. */
|
|
9
|
+
export declare const expandAllowList: (list: string[]) => string[];
|
|
10
|
+
/** Does `host` match one of the patterns? "*.example.com" matches its subdomains (not example.com itself). */
|
|
11
|
+
export declare const hostAllowed: (host: string, patterns: string[]) => boolean;
|
|
12
|
+
export declare const HOSTS_BEGIN = "# >>> windows-development-sandbox network.blockHosts";
|
|
13
|
+
export declare const HOSTS_END = "# <<< windows-development-sandbox network.blockHosts";
|
|
14
|
+
/** The hosts file with our block section replaced by `hosts` (each sent to 0.0.0.0 and ::); everything else stays. */
|
|
15
|
+
export declare const withBlockedHosts: (hostsFile: string, hosts: string[]) => string;
|
|
16
|
+
/** One forwarded port: <pc> on this PC (localhost only) reaches <sandbox> inside. */
|
|
17
|
+
export interface PortForward {
|
|
18
|
+
pc: number;
|
|
19
|
+
sandbox: number;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* The port the sandbox's relay for forward `index` listens on (it connects on to localhost:<sandbox> inside, so a
|
|
23
|
+
* server that only listens on localhost is reachable too). Out of the range Windows hands out for outgoing connections.
|
|
24
|
+
*/
|
|
25
|
+
export declare const relayPort: (index: number) => number;
|
|
26
|
+
/** The address the sandbox's built-in proxy listens on (allowOnly). */
|
|
27
|
+
export declare const PROXY_PORT = 38080;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// The network settings as rules: which hosts the sandbox may reach (network.allowOnly), which never (blockHosts), and
|
|
2
|
+
// the ports forwarded from this PC into the sandbox. Pure (tested); the guest applies them (guest/setup/network.ts,
|
|
3
|
+
// guest/netproxy.ts) and the host runs the forwarders (host/port-forward.ts).
|
|
4
|
+
/** A host name or a "*.example.com" pattern (lower case, no scheme, no port). */
|
|
5
|
+
export const HOST_PATTERN = /^(\*\.)?([a-z0-9-]+\.)*[a-z0-9-]+$/i;
|
|
6
|
+
/**
|
|
7
|
+
* What "@developer" stands for in allowOnly: package registries, source hosts, toolchain downloads and the AI
|
|
8
|
+
* providers the agents talk to. Enough for an agent to install dependencies, push to GitHub and reach its model.
|
|
9
|
+
*/
|
|
10
|
+
export const DEVELOPER_HOSTS = [
|
|
11
|
+
// AI providers
|
|
12
|
+
'api.anthropic.com', 'claude.ai', '*.claude.ai', 'console.anthropic.com', 'statsig.anthropic.com', 'sentry.io', '*.sentry.io',
|
|
13
|
+
'api.openai.com', 'chatgpt.com', '*.openai.com', 'generativelanguage.googleapis.com', 'oauth2.googleapis.com', 'accounts.google.com',
|
|
14
|
+
// source hosts
|
|
15
|
+
'github.com', '*.github.com', '*.githubusercontent.com', 'codeload.github.com', 'gitlab.com', '*.gitlab.com', 'bitbucket.org',
|
|
16
|
+
// package registries
|
|
17
|
+
'registry.npmjs.org', '*.npmjs.org', 'registry.yarnpkg.com', 'pypi.org', 'files.pythonhosted.org', 'crates.io', 'index.crates.io', 'static.crates.io',
|
|
18
|
+
'proxy.golang.org', 'sum.golang.org', 'repo1.maven.org', 'repo.maven.apache.org', 'plugins.gradle.org', 'services.gradle.org', 'api.nuget.org',
|
|
19
|
+
'*.nuget.org', 'rubygems.org', 'index.rubygems.org', 'repo.packagist.org', 'packagist.org', 'jsr.io', 'deno.land',
|
|
20
|
+
// toolchains
|
|
21
|
+
'nodejs.org', '*.nodejs.org', 'static.rust-lang.org', 'go.dev', 'dl.google.com', 'aka.ms', 'download.visualstudio.microsoft.com',
|
|
22
|
+
'dotnetcli.azureedge.net', 'builds.dotnet.microsoft.com', 'api.adoptium.net', 'objects.githubusercontent.com', 'marketplace.visualstudio.com',
|
|
23
|
+
'*.gallerycdn.vsassets.io', '*.vscode-cdn.net', 'update.code.visualstudio.com',
|
|
24
|
+
];
|
|
25
|
+
/** The allowOnly list with "@developer" expanded, lower case, without duplicates. */
|
|
26
|
+
export const expandAllowList = (list) => [...new Set(list.flatMap((h) => (h.trim().toLowerCase() === '@developer' ? DEVELOPER_HOSTS : [h])).map((h) => h.trim().toLowerCase()).filter((h) => HOST_PATTERN.test(h)))];
|
|
27
|
+
/** Does `host` match one of the patterns? "*.example.com" matches its subdomains (not example.com itself). */
|
|
28
|
+
export const hostAllowed = (host, patterns) => {
|
|
29
|
+
const h = host.toLowerCase().replace(/\.$/, '');
|
|
30
|
+
return patterns.some((p) => (p.startsWith('*.') ? h.endsWith(p.slice(1)) && h.length > p.length - 1 : h === p));
|
|
31
|
+
};
|
|
32
|
+
export const HOSTS_BEGIN = '# >>> windows-development-sandbox network.blockHosts';
|
|
33
|
+
export const HOSTS_END = '# <<< windows-development-sandbox network.blockHosts';
|
|
34
|
+
/** The hosts file with our block section replaced by `hosts` (each sent to 0.0.0.0 and ::); everything else stays. */
|
|
35
|
+
export const withBlockedHosts = (hostsFile, hosts) => {
|
|
36
|
+
const begin = hostsFile.indexOf(HOSTS_BEGIN);
|
|
37
|
+
const end = hostsFile.indexOf(HOSTS_END);
|
|
38
|
+
const outside = begin >= 0 && end > begin ? hostsFile.slice(0, begin) + hostsFile.slice(end + HOSTS_END.length) : hostsFile;
|
|
39
|
+
const clean = [...new Set(hosts.map((h) => h.trim().toLowerCase()).filter((h) => HOST_PATTERN.test(h) && !h.startsWith('*.')))];
|
|
40
|
+
const base = outside.replace(/\s+$/, '');
|
|
41
|
+
if (!clean.length)
|
|
42
|
+
return base + '\r\n';
|
|
43
|
+
return [base, HOSTS_BEGIN, ...clean.flatMap((h) => ['0.0.0.0 ' + h, ':: ' + h]), HOSTS_END, ''].join('\r\n');
|
|
44
|
+
};
|
|
45
|
+
/**
|
|
46
|
+
* The port the sandbox's relay for forward `index` listens on (it connects on to localhost:<sandbox> inside, so a
|
|
47
|
+
* server that only listens on localhost is reachable too). Out of the range Windows hands out for outgoing connections.
|
|
48
|
+
*/
|
|
49
|
+
export const relayPort = (index) => 38100 + index;
|
|
50
|
+
/** The address the sandbox's built-in proxy listens on (allowOnly). */
|
|
51
|
+
export const PROXY_PORT = 38080;
|
|
@@ -10,6 +10,7 @@ export declare const FrameType: {
|
|
|
10
10
|
readonly Error: 18;
|
|
11
11
|
readonly Restarting: 19;
|
|
12
12
|
readonly Status: 20;
|
|
13
|
+
readonly Console: 21;
|
|
13
14
|
};
|
|
14
15
|
/** HELLO flags: control-only connections get no output and never size the console; a query only asks for STATUS. */
|
|
15
16
|
export declare const HelloFlag: {
|
|
@@ -49,12 +50,16 @@ export declare class FrameReader {
|
|
|
49
50
|
push(chunk: Buffer): Frame[];
|
|
50
51
|
}
|
|
51
52
|
/**
|
|
52
|
-
* Removes
|
|
53
|
-
*
|
|
54
|
-
*
|
|
53
|
+
* Removes the terminal "strings" an untrusted program could abuse from the sandbox's output before it reaches your
|
|
54
|
+
* terminal: clipboard writes (OSC 52), terminal macros (ConEmu's OSC 9), DCS / APC / PM / SOS, and their C1 forms.
|
|
55
|
+
* Titles, links, progress and colours pass. A byte-level state machine, so no split across chunks, leading zero
|
|
56
|
+
* (ESC]052) or huge payload slips through: a dropped string stays dropped until its terminator, however long it is.
|
|
55
57
|
*/
|
|
56
58
|
export declare class OutputFilter {
|
|
57
|
-
|
|
59
|
+
/** text: normal output; esc: after ESC; c2: after a 0xC2 lead byte; string: inside an OSC/DCS/...; stringEsc / stringC2: a possible terminator inside one. */
|
|
60
|
+
private state;
|
|
61
|
+
/** The string being read: an OSC worth keeping is collected until its terminator; null = being dropped. */
|
|
62
|
+
private body;
|
|
58
63
|
push(chunk: Buffer): Buffer;
|
|
59
64
|
}
|
|
60
65
|
/** Tries every address the sandbox published and returns the first one that answers. */
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Client side of the ptyhost protocol (see sandbox/ptyhost.cs). Used by the window inside the sandbox and by
|
|
2
2
|
// `--terminal` / `attach` on the host, so it only depends on Node built-ins.
|
|
3
3
|
import net from 'node:net';
|
|
4
|
-
export const FrameType = { Hello: 1, Input: 2, Resize: 3, Restart: 4, Ping: 5, Output: 16, Exit: 17, Error: 18, Restarting: 19, Status: 20 };
|
|
4
|
+
export const FrameType = { Hello: 1, Input: 2, Resize: 3, Restart: 4, Ping: 5, Output: 16, Exit: 17, Error: 18, Restarting: 19, Status: 20, Console: 21 };
|
|
5
5
|
/** HELLO flags: control-only connections get no output and never size the console; a query only asks for STATUS. */
|
|
6
6
|
export const HelloFlag = { ControlOnly: 1, Query: 2 };
|
|
7
7
|
/** Viewers ping this often; ptyhost counts a viewer as gone after 15 s of silence (e.g. its terminal tab was closed). */
|
|
@@ -47,23 +47,140 @@ export class FrameReader {
|
|
|
47
47
|
return frames;
|
|
48
48
|
}
|
|
49
49
|
}
|
|
50
|
+
/** OSC commands that may reach your terminal: window title (0, 1, 2), links (8), progress (9;4), shell marks (133). */
|
|
51
|
+
const KEPT_OSC = (body) => {
|
|
52
|
+
const number = /^(\d{1,4})(;|$)/.exec(body);
|
|
53
|
+
if (!number)
|
|
54
|
+
return false;
|
|
55
|
+
const n = Number(number[1]);
|
|
56
|
+
return n <= 2 || n === 8 || n === 133 || (n === 9 && body.startsWith(number[1] + ';4;'));
|
|
57
|
+
};
|
|
58
|
+
/** Longest OSC kept (a title or a link); anything longer is dropped whole. */
|
|
59
|
+
const MAX_KEPT_STRING = 4096;
|
|
50
60
|
/**
|
|
51
|
-
* Removes
|
|
52
|
-
*
|
|
53
|
-
*
|
|
61
|
+
* Removes the terminal "strings" an untrusted program could abuse from the sandbox's output before it reaches your
|
|
62
|
+
* terminal: clipboard writes (OSC 52), terminal macros (ConEmu's OSC 9), DCS / APC / PM / SOS, and their C1 forms.
|
|
63
|
+
* Titles, links, progress and colours pass. A byte-level state machine, so no split across chunks, leading zero
|
|
64
|
+
* (ESC]052) or huge payload slips through: a dropped string stays dropped until its terminator, however long it is.
|
|
54
65
|
*/
|
|
55
66
|
export class OutputFilter {
|
|
56
|
-
|
|
67
|
+
/** text: normal output; esc: after ESC; c2: after a 0xC2 lead byte; string: inside an OSC/DCS/...; stringEsc / stringC2: a possible terminator inside one. */
|
|
68
|
+
state = 'text';
|
|
69
|
+
/** The string being read: an OSC worth keeping is collected until its terminator; null = being dropped. */
|
|
70
|
+
body = null;
|
|
57
71
|
push(chunk) {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
72
|
+
const out = Buffer.allocUnsafe(chunk.length + MAX_KEPT_STRING + 8);
|
|
73
|
+
let n = 0;
|
|
74
|
+
const put = (...bytes) => { for (const b of bytes)
|
|
75
|
+
out[n++] = b; };
|
|
76
|
+
/** After ESC (or the C1 form): ] opens an OSC (maybe kept), P X ^ _ open DCS / SOS / PM / APC (dropped). */
|
|
77
|
+
const opens = (b, c1) => {
|
|
78
|
+
if (b === (c1 ? 0x9d : 0x5d)) {
|
|
79
|
+
this.body = c1 ? null : [];
|
|
80
|
+
this.state = 'string';
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
if ((c1 ? [0x90, 0x98, 0x9e, 0x9f] : [0x50, 0x58, 0x5e, 0x5f]).includes(b)) {
|
|
84
|
+
this.body = null;
|
|
85
|
+
this.state = 'string';
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
return false;
|
|
89
|
+
};
|
|
90
|
+
const endString = (terminator) => {
|
|
91
|
+
const body = this.body && Buffer.from(this.body).toString('latin1');
|
|
92
|
+
if (body !== null && KEPT_OSC(body)) {
|
|
93
|
+
put(0x1b, 0x5d);
|
|
94
|
+
out.write(body, n, 'latin1');
|
|
95
|
+
n += body.length;
|
|
96
|
+
put(...terminator);
|
|
97
|
+
}
|
|
98
|
+
this.body = null;
|
|
99
|
+
this.state = 'text';
|
|
100
|
+
};
|
|
101
|
+
const collect = (b) => { if (this.body && this.body.push(b) > MAX_KEPT_STRING)
|
|
102
|
+
this.body = null; };
|
|
103
|
+
for (const b of chunk) {
|
|
104
|
+
switch (this.state) {
|
|
105
|
+
case 'text':
|
|
106
|
+
if (b === 0x1b)
|
|
107
|
+
this.state = 'esc';
|
|
108
|
+
else if (b === 0xc2)
|
|
109
|
+
this.state = 'c2';
|
|
110
|
+
else
|
|
111
|
+
put(b);
|
|
112
|
+
break;
|
|
113
|
+
case 'esc':
|
|
114
|
+
if (opens(b, false))
|
|
115
|
+
break;
|
|
116
|
+
if (b !== 0x1b) {
|
|
117
|
+
put(0x1b, b);
|
|
118
|
+
this.state = 'text';
|
|
119
|
+
}
|
|
120
|
+
else
|
|
121
|
+
put(0x1b);
|
|
122
|
+
break;
|
|
123
|
+
case 'c2':
|
|
124
|
+
if (opens(b, true))
|
|
125
|
+
break;
|
|
126
|
+
if (b === 0x1b) {
|
|
127
|
+
put(0xc2);
|
|
128
|
+
this.state = 'esc';
|
|
129
|
+
}
|
|
130
|
+
else if (b === 0xc2)
|
|
131
|
+
put(0xc2);
|
|
132
|
+
else {
|
|
133
|
+
put(0xc2, b);
|
|
134
|
+
this.state = 'text';
|
|
135
|
+
}
|
|
136
|
+
break;
|
|
137
|
+
case 'string':
|
|
138
|
+
if (b === 0x07)
|
|
139
|
+
endString([0x07]);
|
|
140
|
+
else if (b === 0x1b)
|
|
141
|
+
this.state = 'stringEsc';
|
|
142
|
+
else if (b === 0xc2)
|
|
143
|
+
this.state = 'stringC2';
|
|
144
|
+
else if (b === 0x18 || b === 0x1a) {
|
|
145
|
+
this.body = null;
|
|
146
|
+
this.state = 'text';
|
|
147
|
+
}
|
|
148
|
+
else
|
|
149
|
+
collect(b);
|
|
150
|
+
break;
|
|
151
|
+
case 'stringC2':
|
|
152
|
+
// U+009C is the C1 string terminator; any other character is part of the string.
|
|
153
|
+
if (b === 0x9c)
|
|
154
|
+
endString([0x1b, 0x5c]);
|
|
155
|
+
else {
|
|
156
|
+
collect(0xc2);
|
|
157
|
+
this.state = 'string';
|
|
158
|
+
if (b === 0x07)
|
|
159
|
+
endString([0x07]);
|
|
160
|
+
else if (b === 0x1b)
|
|
161
|
+
this.state = 'stringEsc';
|
|
162
|
+
else if (b !== 0xc2)
|
|
163
|
+
collect(b);
|
|
164
|
+
else
|
|
165
|
+
this.state = 'stringC2';
|
|
166
|
+
}
|
|
167
|
+
break;
|
|
168
|
+
case 'stringEsc':
|
|
169
|
+
if (b === 0x5c) {
|
|
170
|
+
endString([0x1b, 0x5c]);
|
|
171
|
+
break;
|
|
172
|
+
}
|
|
173
|
+
// ESC inside a string ends it (dropped) and starts a new sequence.
|
|
174
|
+
this.body = null;
|
|
175
|
+
this.state = 'esc';
|
|
176
|
+
if (!opens(b, false) && b !== 0x1b) {
|
|
177
|
+
put(0x1b, b);
|
|
178
|
+
this.state = 'text';
|
|
179
|
+
}
|
|
180
|
+
break;
|
|
181
|
+
}
|
|
65
182
|
}
|
|
66
|
-
return
|
|
183
|
+
return out.subarray(0, n);
|
|
67
184
|
}
|
|
68
185
|
}
|
|
69
186
|
const connectOnce = (host, port, timeoutMs) => {
|
|
@@ -177,15 +294,18 @@ export const runTerminalClient = (socket, token, options = {}) => {
|
|
|
177
294
|
}
|
|
178
295
|
socket.write(encodeFrame(FrameType.Input, swapKeys ? fixWindowsKeys(chunk) : chunk));
|
|
179
296
|
};
|
|
180
|
-
//
|
|
181
|
-
//
|
|
182
|
-
// then let ptyhost repaint everything once at the new size.
|
|
297
|
+
// Windows' own ConPTY repaints the whole screen on every resize; dragging a window edge fires dozens of them, and
|
|
298
|
+
// repaints racing each other left the screen garbled. Wait until the size settles, clear what this terminal
|
|
299
|
+
// reflowed on its own, then let ptyhost repaint everything once at the new size. With conpty.dll (passthrough)
|
|
300
|
+
// nothing is repainted: this terminal's own reflow and scrollback stay, and the program redraws what it needs.
|
|
301
|
+
let passthrough = false;
|
|
183
302
|
const sendSize = () => {
|
|
184
303
|
const [c, r] = size();
|
|
185
304
|
if (c === sent[0] && r === sent[1])
|
|
186
305
|
return;
|
|
187
306
|
sent = [c, r];
|
|
188
|
-
|
|
307
|
+
if (!passthrough)
|
|
308
|
+
stdout.write('\x1b[H\x1b[J');
|
|
189
309
|
socket.write(resizeFrame(c, r));
|
|
190
310
|
};
|
|
191
311
|
const onResize = () => { clearTimeout(resizeTimer); resizeTimer = setTimeout(() => { resizeTimer = undefined; sendSize(); }, 120); };
|
|
@@ -205,6 +325,8 @@ export const runTerminalClient = (socket, token, options = {}) => {
|
|
|
205
325
|
for (const frame of frames) {
|
|
206
326
|
if (frame.type === FrameType.Output)
|
|
207
327
|
stdout.write(filter.push(frame.payload));
|
|
328
|
+
else if (frame.type === FrameType.Console)
|
|
329
|
+
passthrough = frame.payload.toString('latin1') === 'conpty.dll';
|
|
208
330
|
else if (frame.type === FrameType.Restarting)
|
|
209
331
|
stdout.write('\x1b[2J\x1b[3J\x1b[H');
|
|
210
332
|
else if (frame.type === FrameType.Exit)
|
package/dist/shared/rundir.d.ts
CHANGED
|
@@ -11,6 +11,8 @@ export declare const runPaths: (dataDir: string) => {
|
|
|
11
11
|
responses: string;
|
|
12
12
|
sessions: string;
|
|
13
13
|
projects: string;
|
|
14
|
+
/** audit.processes: programs the sandbox started, in small .jsonl files the host takes in and deletes. */
|
|
15
|
+
audit: string;
|
|
14
16
|
request: (id: string) => string;
|
|
15
17
|
response: (id: string) => string;
|
|
16
18
|
session: (id: string) => string;
|
package/dist/shared/rundir.js
CHANGED
|
@@ -19,6 +19,8 @@ export const runPaths = (dataDir) => {
|
|
|
19
19
|
responses: path.join(run, 'responses'),
|
|
20
20
|
sessions: path.join(run, 'sessions'),
|
|
21
21
|
projects: path.join(run, 'projects'),
|
|
22
|
+
/** audit.processes: programs the sandbox started, in small .jsonl files the host takes in and deletes. */
|
|
23
|
+
audit: path.join(run, 'audit'),
|
|
22
24
|
request: file('requests'),
|
|
23
25
|
response: file('responses'),
|
|
24
26
|
session: file('sessions'),
|