windows-development-sandbox 1.1.0 → 1.4.1
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 +2 -1
- package/README.md +3 -2
- package/config.example.json +39 -5
- package/dist/cli/args.d.ts +3 -0
- package/dist/cli/args.js +3 -1
- package/dist/cli/boot-progress.d.ts +22 -0
- package/dist/cli/boot-progress.js +59 -0
- 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.d.ts +5 -2
- package/dist/cli/commands/launch.js +113 -25
- package/dist/cli/commands/maintenance.d.ts +2 -0
- package/dist/cli/commands/maintenance.js +70 -4
- package/dist/cli/commands/project.d.ts +10 -6
- package/dist/cli/commands/project.js +42 -3
- package/dist/cli/commands/restart.js +17 -7
- package/dist/cli/commands/sessions.js +5 -4
- package/dist/cli/commands/user-config.js +1 -1
- package/dist/cli/index.js +6 -4
- package/dist/cli/manifest.js +6 -1
- 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/progress.d.ts +15 -0
- package/dist/cli/progress.js +52 -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 +10 -7
- package/dist/core/local-config.d.ts +31 -1
- package/dist/core/local-config.js +72 -7
- package/dist/core/project-policy.d.ts +50 -3
- package/dist/core/project-policy.js +90 -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 +69 -3
- 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/projects.d.ts +1 -0
- package/dist/core/schema/projects.js +1 -0
- package/dist/core/schema/sandbox.d.ts +29 -1
- package/dist/core/schema/sandbox.js +29 -1
- package/dist/core/schema/toolchains.d.ts +3 -0
- package/dist/core/schema/toolchains.js +6 -1
- package/dist/core/types.d.ts +24 -1
- package/dist/core/util.d.ts +2 -0
- package/dist/core/util.js +2 -0
- package/dist/core/workspace.d.ts +21 -4
- package/dist/core/workspace.js +99 -21
- 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 +7 -2
- package/dist/guest/env.js +17 -3
- package/dist/guest/helpers.d.ts +1 -1
- package/dist/guest/main.js +20 -3
- 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/index.js +29 -3
- 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/channel.d.ts +2 -2
- package/dist/host/channel.js +3 -2
- package/dist/host/generate.d.ts +7 -1
- package/dist/host/generate.js +23 -6
- package/dist/host/join.d.ts +7 -0
- package/dist/host/join.js +23 -9
- package/dist/host/launch/mappings.d.ts +7 -2
- package/dist/host/launch/mappings.js +68 -18
- package/dist/host/launch/projects.d.ts +2 -0
- package/dist/host/launch/projects.js +11 -5
- 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/projects.d.ts +25 -2
- package/dist/host/projects.js +54 -5
- 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 +1387 -558
- package/dist/runtime/db.js +199 -14
- package/dist/runtime/main.js +3082 -631
- package/dist/runtime/netproxy.js +25948 -0
- package/dist/runtime/view.js +531 -148
- package/dist/selftest/audit.test.d.ts +2 -0
- package/dist/selftest/audit.test.js +57 -0
- package/dist/selftest/channel.test.js +45 -2
- package/dist/selftest/cli.test.js +47 -17
- package/dist/selftest/fixtures.d.ts +47 -1
- package/dist/selftest/fixtures.js +3 -1
- package/dist/selftest/generate.test.js +7 -2
- package/dist/selftest/index.js +6 -2
- package/dist/selftest/live-network.test.d.ts +2 -0
- package/dist/selftest/live-network.test.js +63 -0
- package/dist/selftest/live-terminal.test.d.ts +2 -0
- package/dist/selftest/live-terminal.test.js +169 -0
- package/dist/selftest/local-config.test.js +42 -4
- package/dist/selftest/mirror.test.js +36 -7
- package/dist/selftest/network.test.js +12 -0
- package/dist/selftest/projects.test.js +77 -8
- 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/console-input.d.ts +11 -0
- package/dist/shared/console-input.js +61 -0
- package/dist/shared/download.d.ts +3 -0
- package/dist/shared/download.js +25 -2
- package/dist/shared/guest-paths.d.ts +27 -0
- package/dist/shared/guest-paths.js +68 -0
- package/dist/shared/netpolicy.d.ts +27 -0
- package/dist/shared/netpolicy.js +51 -0
- package/dist/shared/projects.d.ts +27 -4
- package/dist/shared/projects.js +72 -13
- package/dist/shared/pty-client.d.ts +26 -4
- package/dist/shared/pty-client.js +204 -25
- package/dist/shared/rundir.d.ts +2 -0
- package/dist/shared/rundir.js +2 -0
- package/dist/shared/schema.d.ts +34 -0
- package/dist/shared/schema.js +14 -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 +162 -7
- package/schemas/config.schema.json +318 -3
- package/schemas/sandbox-config.schema.json +129 -14
- 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
|
@@ -6,22 +6,27 @@ import { Name, SchemaRef, Strings, toggle } from './common.js';
|
|
|
6
6
|
import { Databases } from './databases.js';
|
|
7
7
|
import { Harness } from './harness.js';
|
|
8
8
|
import { AgentCommands, Attach, ClaudeProfile, Secrets } from './profile.js';
|
|
9
|
-
import { Downloads, Git, GuestWorkspace, Mapping, Provisioning, Sandbox } from './sandbox.js';
|
|
9
|
+
import { Audit, Downloads, Git, GuestWorkspace, Mapping, Network, Provisioning, Sandbox, WorkspaceFilter } from './sandbox.js';
|
|
10
10
|
import { Toolchains } from './toolchains.js';
|
|
11
|
+
import { Apps } from './apps.js';
|
|
11
12
|
import { Projects } from './projects.js';
|
|
12
13
|
export const Config = z.strictObject({
|
|
13
14
|
$schema: SchemaRef.optional(),
|
|
14
15
|
defaultHarness: Name.describe('Agent started by `sandbox launch` (sandbox claude always runs claude).'),
|
|
15
16
|
guestWorkspace: GuestWorkspace,
|
|
16
17
|
readOnly: z.boolean().describe('Map the workspace read-only.'),
|
|
18
|
+
workspaceFilter: WorkspaceFilter,
|
|
19
|
+
audit: Audit,
|
|
17
20
|
mappings: z.array(Mapping).describe('Extra folders from this PC.'),
|
|
18
21
|
sandbox: Sandbox,
|
|
19
22
|
storage: z.strictObject({
|
|
20
23
|
root: z.string().min(1).describe('Where everything is kept on this PC (tools, databases, logins, launches).'),
|
|
21
24
|
persistAgentState: z.boolean().describe('Keep agent logins and history between sandboxes.'),
|
|
22
25
|
dataName: Name.describe('Name of the persistent data set (C:\\AgentData) shared by all workspaces.'),
|
|
26
|
+
markSandboxFiles: z.boolean().describe('Programs and scripts (.exe, .ps1, .bat, .cmd, .msi, .lnk ...) that appear or change in your project while a sandbox runs get Windows\' "downloaded from the internet" mark, so SmartScreen and PowerShell ask before they run on this PC.'),
|
|
27
|
+
warnDataGb: z.number().min(0).describe('Say at launch when the persisted data (tools, databases, logins) is bigger than this many GB; 0 = never. sandbox data shows what takes the space.'),
|
|
23
28
|
}),
|
|
24
|
-
network:
|
|
29
|
+
network: Network,
|
|
25
30
|
provisioning: Provisioning,
|
|
26
31
|
downloads: Downloads,
|
|
27
32
|
toolchains: Toolchains,
|
|
@@ -30,6 +35,7 @@ export const Config = z.strictObject({
|
|
|
30
35
|
agentCommands: AgentCommands,
|
|
31
36
|
secrets: Secrets,
|
|
32
37
|
personalization: toggle('Copy your desktop look (theme, accent, taskbar, wallpaper, ...) into the sandbox.'),
|
|
38
|
+
apps: Apps,
|
|
33
39
|
claudeProfile: ClaudeProfile,
|
|
34
40
|
harnesses: z.record(Name, Harness).describe('Agents that can run in the sandbox.'),
|
|
35
41
|
hooks: z.strictObject({ failFast: z.boolean().describe('Stop when a hook script fails.') }),
|
|
@@ -44,10 +50,14 @@ export const LocalOverrides = z.strictObject({
|
|
|
44
50
|
harness: Name.describe('Agent for this project.'),
|
|
45
51
|
guestWorkspace: GuestWorkspace,
|
|
46
52
|
readOnly: z.boolean().describe('Map this project read-only.'),
|
|
47
|
-
|
|
53
|
+
// A project may turn the filter on and keep more out, never let ignored files in.
|
|
54
|
+
workspaceFilter: WorkspaceFilter.pick({ enabled: true, exclude: true }).partial(),
|
|
55
|
+
mappings: z.array(Mapping).describe('Extra folders (relative paths are relative to this file), e.g. a read-only data folder at another place in the sandbox. Folders outside the project are used only as your projects.allowOutsideMappings setting allows (by default you are asked once).'),
|
|
48
56
|
scripts: Strings.describe('Scripts (.ps1 / .mjs / .cmd, relative to this file) run inside the sandbox before the agent.'),
|
|
49
|
-
|
|
50
|
-
|
|
57
|
+
// This PC's resources are yours to share: CPU and paging file (this PC's disk) are not a project's to set.
|
|
58
|
+
sandbox: Sandbox.omit({ cpuLimit: true, pageFileMb: true }).partial(),
|
|
59
|
+
// A project may block more (the LAN, hosts); it never opens hosts, ports, DNS or a proxy.
|
|
60
|
+
network: Network.pick({ blockPrivateRanges: true, blockHosts: true }).partial(),
|
|
51
61
|
provisioning: Provisioning.pick({ copyFromHost: true, fallbackToInstall: true, refresh: true }).extend({ git: Git.partial(), terminal: toggle('Windows Terminal as the sandbox window.').partial() }).partial(),
|
|
52
62
|
downloads: Downloads.partial(),
|
|
53
63
|
// Every toolchain setting, each one optional (a project usually only switches languages on or pins a version).
|
|
@@ -58,6 +68,8 @@ export const LocalOverrides = z.strictObject({
|
|
|
58
68
|
databases: z.strictObject(_.mapValues(Databases.shape, (shape) => shape.partial())).partial(),
|
|
59
69
|
attach: Attach.partial(),
|
|
60
70
|
personalization: toggle('Copy your desktop look into the sandbox.').partial(),
|
|
61
|
-
|
|
71
|
+
// Catalogue apps only (a vetted list): your own programs (apps.custom) download anything, so a repo cannot add them.
|
|
72
|
+
apps: Apps.pick({ install: true }).partial(),
|
|
73
|
+
claudeProfile: ClaudeProfile.pick({ enabled: true, overwrite: true, copyCredentials: true, syncHistory: true, syncClaudeMem: true }).partial(),
|
|
62
74
|
hooks: z.strictObject({ failFast: z.boolean() }).partial(),
|
|
63
75
|
}).partial();
|
|
@@ -23,5 +23,10 @@ export declare const ClaudeProfile: z.ZodObject<{
|
|
|
23
23
|
copyCredentials: z.ZodBoolean;
|
|
24
24
|
overwrite: z.ZodBoolean;
|
|
25
25
|
syncHistory: z.ZodBoolean;
|
|
26
|
+
historyScope: z.ZodEnum<{
|
|
27
|
+
project: "project";
|
|
28
|
+
all: "all";
|
|
29
|
+
}>;
|
|
26
30
|
syncClaudeMem: z.ZodBoolean;
|
|
31
|
+
syncClaudeMemBack: z.ZodBoolean;
|
|
27
32
|
}, z.core.$strict>;
|
|
@@ -24,6 +24,8 @@ export const ClaudeProfile = z.strictObject({
|
|
|
24
24
|
globalStateFile: z.string().describe('Your .claude.json, or "auto". Empty string = do not copy.'),
|
|
25
25
|
copyCredentials: z.boolean().describe('Copy your Claude LOGIN into the sandbox. Prefer sandbox account instead.'),
|
|
26
26
|
overwrite: z.boolean().describe('Refresh these settings from this PC on every launch.'),
|
|
27
|
-
syncHistory: z.boolean().describe('Bring
|
|
28
|
-
|
|
27
|
+
syncHistory: z.boolean().describe('Bring conversation history in, and sync new history back (path-translated).'),
|
|
28
|
+
historyScope: z.enum(['project', 'all']).describe('Which history comes in: "project" = only the projects in this sandbox (other projects\' conversations, file backups and prompts stay on this PC), "all" = everything. Only this sandbox\'s projects ever sync back.'),
|
|
29
|
+
syncClaudeMem: z.boolean().describe('Bring claude-mem data into the sandbox (its settings and backups stay on this PC).'),
|
|
30
|
+
syncClaudeMemBack: z.boolean().describe('Also copy claude-mem\'s database back to this PC when the sandbox closes. Off by default: what is in it is loaded into every Claude session on this PC, so a compromised agent could plant instructions there.'),
|
|
29
31
|
});
|
|
@@ -5,5 +5,6 @@ export declare const Projects: z.ZodObject<{
|
|
|
5
5
|
roots: z.ZodArray<z.ZodString>;
|
|
6
6
|
askToAddRoot: z.ZodBoolean;
|
|
7
7
|
declinedRoots: z.ZodArray<z.ZodString>;
|
|
8
|
+
allowOutsideMappings: z.ZodUnion<readonly [z.ZodLiteral<"ask">, z.ZodBoolean]>;
|
|
8
9
|
wsbShare: z.ZodBoolean;
|
|
9
10
|
}, z.core.$strict>;
|
|
@@ -7,5 +7,6 @@ export const Projects = z.strictObject({
|
|
|
7
7
|
roots: z.array(z.string().min(1)).describe('Folders whose subfolders are your projects (e.g. "F:\\\\Code"), mapped read/write at every start. Any project inside one, including ones you create later, opens in the running sandbox at once, without a restart. Every agent in the sandbox can see all of them.'),
|
|
8
8
|
askToAddRoot: z.boolean().describe('When you start a project whose parent folder is not a root yet, ask once whether to add it.'),
|
|
9
9
|
declinedRoots: z.array(z.string().min(1)).describe('Parent folders you said no to (not asked again).'),
|
|
10
|
+
allowOutsideMappings: z.union([z.literal('ask'), z.boolean()]).describe('A project\'s .sandbox-config.json may map folders from outside the project (e.g. a game folder on another drive). "ask" = an interactive launch asks once per project and set of folders, and remembers your answer; true = always allowed (still listed when you approve the file); false = never (the launch stops and says which folders). Only your own settings decide this, never a project file.'),
|
|
10
11
|
wsbShare: z.boolean().describe('On Windows 11 24H2+ with the Windows Sandbox command line (wsb), add other folders to the running sandbox with `wsb share` (untested). Otherwise starting a project outside every root offers a restart.'),
|
|
11
12
|
});
|
|
@@ -2,7 +2,7 @@ import { z } from 'zod';
|
|
|
2
2
|
export declare const GuestWorkspace: z.ZodUnion<readonly [z.ZodLiteral<"auto">, z.ZodLiteral<"mirror">, z.ZodString]>;
|
|
3
3
|
export declare const Mapping: z.ZodObject<{
|
|
4
4
|
host: z.ZodString;
|
|
5
|
-
guest: z.ZodString
|
|
5
|
+
guest: z.ZodOptional<z.ZodString>;
|
|
6
6
|
readOnly: z.ZodBoolean;
|
|
7
7
|
optional: z.ZodOptional<z.ZodBoolean>;
|
|
8
8
|
}, z.core.$strict>;
|
|
@@ -11,6 +11,7 @@ export declare const Sandbox: z.ZodObject<{
|
|
|
11
11
|
maxMemoryMb: z.ZodNumber;
|
|
12
12
|
maxMemoryPercent: z.ZodNumber;
|
|
13
13
|
cpuLimit: z.ZodUnion<readonly [z.ZodLiteral<"none">, z.ZodNumber]>;
|
|
14
|
+
pageFileMb: z.ZodNumber;
|
|
14
15
|
reserveHostMb: z.ZodNumber;
|
|
15
16
|
vGpu: z.ZodBoolean;
|
|
16
17
|
networking: z.ZodBoolean;
|
|
@@ -20,6 +21,30 @@ export declare const Sandbox: z.ZodObject<{
|
|
|
20
21
|
printerRedirection: z.ZodBoolean;
|
|
21
22
|
protectedClient: z.ZodBoolean;
|
|
22
23
|
headless: z.ZodBoolean;
|
|
24
|
+
keepAwake: z.ZodBoolean;
|
|
25
|
+
}, z.core.$strict>;
|
|
26
|
+
export declare const Network: z.ZodObject<{
|
|
27
|
+
blockPrivateRanges: z.ZodBoolean;
|
|
28
|
+
allowOnly: z.ZodArray<z.ZodString>;
|
|
29
|
+
blockHosts: z.ZodArray<z.ZodString>;
|
|
30
|
+
forwardPorts: z.ZodArray<z.ZodObject<{
|
|
31
|
+
pc: z.ZodNumber;
|
|
32
|
+
sandbox: z.ZodNumber;
|
|
33
|
+
}, z.core.$strict>>;
|
|
34
|
+
dns: z.ZodArray<z.ZodString>;
|
|
35
|
+
proxy: z.ZodString;
|
|
36
|
+
}, z.core.$strict>;
|
|
37
|
+
export declare const Audit: z.ZodObject<{
|
|
38
|
+
enabled: z.ZodBoolean;
|
|
39
|
+
days: z.ZodNumber;
|
|
40
|
+
maxMb: z.ZodNumber;
|
|
41
|
+
processes: z.ZodBoolean;
|
|
42
|
+
files: z.ZodBoolean;
|
|
43
|
+
}, z.core.$strict>;
|
|
44
|
+
export declare const WorkspaceFilter: z.ZodObject<{
|
|
45
|
+
enabled: z.ZodBoolean;
|
|
46
|
+
exclude: z.ZodArray<z.ZodString>;
|
|
47
|
+
include: z.ZodArray<z.ZodString>;
|
|
23
48
|
}, z.core.$strict>;
|
|
24
49
|
export declare const Git: z.ZodObject<{
|
|
25
50
|
enabled: z.ZodBoolean;
|
|
@@ -44,6 +69,9 @@ export declare const Provisioning: z.ZodObject<{
|
|
|
44
69
|
terminal: z.ZodObject<{
|
|
45
70
|
enabled: z.ZodBoolean;
|
|
46
71
|
}, z.core.$strict>;
|
|
72
|
+
conpty: z.ZodObject<{
|
|
73
|
+
enabled: z.ZodBoolean;
|
|
74
|
+
}, z.core.$strict>;
|
|
47
75
|
}, z.core.$strict>;
|
|
48
76
|
export declare const Downloads: z.ZodObject<{
|
|
49
77
|
requireChecksum: z.ZodBoolean;
|
|
@@ -4,7 +4,7 @@ import { GitAccess, GuestPath, toggle } from './common.js';
|
|
|
4
4
|
export const GuestWorkspace = z.union([z.literal('auto'), z.literal('mirror'), GuestPath]).describe('Where the workspace appears inside the sandbox. "auto" = C:\\Workspace\\<folder name>. "mirror" = the same path as on this PC, so absolute paths in configs and build caches match (another drive letter such as F: is a subst drive onto C:\\Mirror\\F); paths the sandbox reserves (C:\\Users\\..., C:\\Program Files\\...) fall back to "auto".');
|
|
5
5
|
export const Mapping = z.strictObject({
|
|
6
6
|
host: z.string().min(1).describe('Folder on this PC (relative paths are relative to the config file).'),
|
|
7
|
-
guest:
|
|
7
|
+
guest: z.string().min(1).max(1024).optional().describe('Where it appears inside the sandbox, e.g. C:\\Assets, or on another drive letter (D: to Z:) such as F:\\SteamLibrary\\steamapps\\common\\Game (shown as F: at boot). Leave it out to use the same path as on this PC (a folder inside the project stays where it is in the project). A path the sandbox cannot use (C:\\Windows\\..., C:\\Users\\..., one already in use) goes to C:\\Mapped\\<drive>\\<path> instead, and the launch says so.'),
|
|
8
8
|
readOnly: z.boolean().describe('true = the agent can only read it.'),
|
|
9
9
|
optional: z.boolean().optional().describe('true = skip it quietly when the host folder does not exist.'),
|
|
10
10
|
});
|
|
@@ -13,6 +13,7 @@ export const Sandbox = z.strictObject({
|
|
|
13
13
|
maxMemoryMb: z.number().int().positive().describe('Upper limit for memoryMb "auto".'),
|
|
14
14
|
maxMemoryPercent: z.number().int().min(10).max(100).describe('memoryMb "auto" never takes more than this share of this PC\'s total RAM (percent).'),
|
|
15
15
|
cpuLimit: z.union([z.literal('none'), z.number().int().min(5).max(100)]).describe('Most of this PC\'s CPU the sandbox may use, in percent (5-100), e.g. 50 keeps half free for you; "none" = no cap (Windows Sandbox would take every processor). Covers everything this tool starts in the sandbox (agents, installs, builds, its terminal).'),
|
|
16
|
+
pageFileMb: z.number().int().min(0).max(262144).describe('Size of the sandbox\'s paging file in MB, created at full size when it starts so it never grows while builds run (8 GB of its disk always stays free). Windows Sandbox starts with about 1 GB: parallel builds then fail with "The paging file is too small", and Windows grows the file under load. 0 = leave Windows\' own.'),
|
|
16
17
|
reserveHostMb: z.number().int().nonnegative().describe('RAM always left for this PC when memoryMb is "auto".'),
|
|
17
18
|
vGpu: z.boolean().describe('Virtual GPU. Off by default: Microsoft notes it widens the sandbox attack surface.'),
|
|
18
19
|
networking: z.boolean().describe('Internet access. false is the only hard network isolation Windows Sandbox offers.'),
|
|
@@ -22,6 +23,32 @@ export const Sandbox = z.strictObject({
|
|
|
22
23
|
printerRedirection: z.boolean().describe('Printers from this PC.'),
|
|
23
24
|
protectedClient: z.boolean().describe('Run the sandbox window in Protected Client mode (recommended).'),
|
|
24
25
|
headless: z.boolean().describe('Hide the sandbox window (sbx-show brings it back). A headless sandbox shuts down when its last session ends.'),
|
|
26
|
+
keepAwake: z.boolean().describe('Keep this PC from sleeping while a sandbox runs, so agents left working overnight keep going (the screen may still turn off; closing a laptop lid still sleeps it).'),
|
|
27
|
+
});
|
|
28
|
+
const Port = z.number().int().min(1).max(65535);
|
|
29
|
+
const HostEntry = z.string().max(253).regex(/^(@developer|(\*\.)?([a-zA-Z0-9-]+\.)*[a-zA-Z0-9-]+)$/, 'a host name like example.com, *.example.com, or @developer');
|
|
30
|
+
export const Network = z.strictObject({
|
|
31
|
+
blockPrivateRanges: z.boolean().describe('Block your LAN (10/8, 172.16/12, 192.168/16, SMB) from inside the sandbox.'),
|
|
32
|
+
allowOnly: z.array(HostEntry).describe('Only these hosts can be reached once the sandbox is set up (tools still download first). *.example.com covers its subdomains; @developer adds package registries, GitHub, toolchain downloads and the AI providers. Programs go through a proxy in the sandbox (HTTP_PROXY); anything else is blocked by its firewall. Empty = no limit.'),
|
|
33
|
+
blockHosts: z.array(z.string().max(253).regex(/^([a-zA-Z0-9-]+\.)*[a-zA-Z0-9-]+$/, 'a host name like telemetry.example.com')).describe('Never reachable from the sandbox, e.g. production servers or telemetry (each exact name; the sandbox\'s hosts file sends them nowhere).'),
|
|
34
|
+
forwardPorts: z.array(z.strictObject({
|
|
35
|
+
pc: Port.describe('The port on this PC (localhost only).'),
|
|
36
|
+
sandbox: Port.describe('The port inside the sandbox (a server there may listen on localhost only).'),
|
|
37
|
+
})).describe('Reach servers in the sandbox from this PC: http://localhost:<pc> here goes to <sandbox> inside, while it runs. Nothing else on your network can use them.'),
|
|
38
|
+
dns: z.array(z.string().regex(/^\d{1,3}(\.\d{1,3}){3}$/, 'an IPv4 address like 1.1.1.1')).describe('DNS servers for the sandbox instead of your network\'s, e.g. 1.1.1.1 and 9.9.9.9.'),
|
|
39
|
+
proxy: z.string().regex(/^(https?:\/\/[^\s]+)?$/, 'http://host:port').describe('An HTTP proxy the sandbox uses to reach the internet (HTTP_PROXY / HTTPS_PROXY for everything in it), e.g. http://proxy.corp:8080. Empty = none.'),
|
|
40
|
+
});
|
|
41
|
+
export const Audit = z.strictObject({
|
|
42
|
+
enabled: z.boolean().describe('Keep an audit log on this PC of what happened in your sandboxes: programs started (with their command lines), files changed in the project, sessions, and what the repo guard caught. The sandbox cannot change it. Read it with sandbox audit.'),
|
|
43
|
+
days: z.number().int().min(1).max(365).describe('Days the audit log is kept.'),
|
|
44
|
+
maxMb: z.number().int().min(5).max(5000).describe('Most space the audit log may take, in MB (older days go first). Days before today are compressed.'),
|
|
45
|
+
processes: z.boolean().describe('Record every program started in the sandbox with its command line (Windows process auditing; repeated runs are counted, not listed).'),
|
|
46
|
+
files: z.boolean().describe('Record the files changed in the project while the sandbox runs.'),
|
|
47
|
+
});
|
|
48
|
+
export const WorkspaceFilter = z.strictObject({
|
|
49
|
+
enabled: z.boolean().describe('Keep what your .gitignore lists (.env files, node_modules, build output ...) out of the sandbox. The sandbox works on a copy of the project without those files, kept in step with it both ways within about a second; what it creates that is ignored (its own node_modules, builds) stays in the sandbox. Git works as usual. Off = the project folder itself is mapped.'),
|
|
50
|
+
exclude: z.array(z.string().max(500)).describe('Also keep these out, as .gitignore patterns, e.g. secrets/, *.pem, data/private/.'),
|
|
51
|
+
include: z.array(z.string().max(500)).describe('Let these in although .gitignore lists them, e.g. .env.example, data/fixtures/.'),
|
|
25
52
|
});
|
|
26
53
|
export const Git = z.strictObject({ enabled: z.boolean().describe('Provide Git for Windows (and Git Bash, which Claude Code needs).'), access: GitAccess });
|
|
27
54
|
export const Provisioning = z.strictObject({
|
|
@@ -30,6 +57,7 @@ export const Provisioning = z.strictObject({
|
|
|
30
57
|
refresh: z.boolean().describe('Re-copy / re-download everything on the next launch.'),
|
|
31
58
|
git: Git,
|
|
32
59
|
terminal: toggle('Windows Terminal as the sandbox window (downloaded once).'),
|
|
60
|
+
conpty: toggle('Agent sessions use Microsoft\'s newer pseudo console (conpty.dll, downloaded once) instead of the one built into Windows: no garbled screen after resizing, real scrollback for the mouse wheel. Off = the one built into Windows.'),
|
|
33
61
|
});
|
|
34
62
|
export const Downloads = z.strictObject({
|
|
35
63
|
requireChecksum: z.boolean().describe('Refuse any toolchain, database or tool download that has no published checksum (SHA-256/SHA-512) to verify it against. false = allow those, as before. Downloads that do have a published checksum are always verified. A project\'s .sandbox-config.json can only turn this on.'),
|
|
@@ -88,6 +88,9 @@ export declare const Toolchains: z.ZodObject<{
|
|
|
88
88
|
}, z.core.$strict>;
|
|
89
89
|
vscode: z.ZodObject<{
|
|
90
90
|
enabled: z.ZodBoolean;
|
|
91
|
+
setup: z.ZodBoolean;
|
|
92
|
+
recommended: z.ZodBoolean;
|
|
93
|
+
extensions: z.ZodArray<z.ZodString>;
|
|
91
94
|
}, z.core.$strict>;
|
|
92
95
|
environment: z.ZodRecord<z.ZodString, z.ZodString>;
|
|
93
96
|
}, z.core.$strict>;
|
|
@@ -78,6 +78,11 @@ export const Toolchains = z.strictObject({
|
|
|
78
78
|
enabled: z.boolean().describe('Mesa 3D (llvmpipe): OpenGL 4.5 in software for games and apps when there is no GPU (sandbox.vGpu off). Copy opengl32.dll and libgallium_wgl.dll from %MESA_DIR% next to the program\'s .exe.'),
|
|
79
79
|
vulkan: z.boolean().describe('Also software Vulkan 1.4 (Mesa lavapipe) with the Khronos loader, registered for every program; for wgpu set WGPU_BACKEND=vulkan.'),
|
|
80
80
|
}),
|
|
81
|
-
vscode:
|
|
81
|
+
vscode: z.strictObject({
|
|
82
|
+
enabled: z.boolean().describe('Portable VS Code inside the sandbox (code on PATH, desktop shortcut); its settings and extensions persist.'),
|
|
83
|
+
setup: z.boolean().describe('Point VS Code at the sandbox\'s own tools (JDK, Python, Go, compilers, .NET, Git, PowerShell 7 and Git Bash terminals) and install the language extensions for the toolchains in use.'),
|
|
84
|
+
recommended: z.boolean().describe('Also install the extensions a project recommends in its .vscode/extensions.json.'),
|
|
85
|
+
extensions: z.array(z.string().regex(/^[\w-]+\.[\w-]+$/, 'publisher.name, e.g. eamodio.gitlens')).describe('More extensions to install in every sandbox, e.g. eamodio.gitlens, ms-azuretools.vscode-docker.'),
|
|
86
|
+
}),
|
|
82
87
|
environment: z.record(z.string().regex(/^[A-Za-z_][A-Za-z0-9_]*$/, 'letters, digits and _ only'), z.string()).describe('Extra environment variables for the agent and every terminal, e.g. registry mirrors: {"NPM_CONFIG_REGISTRY": "https://...", "PIP_INDEX_URL": "https://..."}.'),
|
|
83
88
|
});
|
package/dist/core/types.d.ts
CHANGED
|
@@ -8,10 +8,12 @@ export type Harness = z.output<typeof HarnessSchema>;
|
|
|
8
8
|
export type Mapping = z.output<typeof MappingSchema>;
|
|
9
9
|
export type Toolchains = Config['toolchains'];
|
|
10
10
|
export type ClaudeProfileConfig = Config['claudeProfile'];
|
|
11
|
+
/** label: shown in the launch table, e.g. "outside the project, allowed". */
|
|
11
12
|
export interface ResolvedMapping {
|
|
12
13
|
host: string;
|
|
13
14
|
guest: string;
|
|
14
15
|
readOnly: boolean;
|
|
16
|
+
label?: string;
|
|
15
17
|
}
|
|
16
18
|
export interface LocalConfigFile {
|
|
17
19
|
path: string;
|
|
@@ -39,6 +41,8 @@ export interface RunRequest {
|
|
|
39
41
|
nodeVersion?: string;
|
|
40
42
|
pythonVersion?: string;
|
|
41
43
|
ask?: (question: string) => Promise<boolean>;
|
|
44
|
+
/** `ask` can reach you (default: stdin is a terminal). Tests set it. */
|
|
45
|
+
interactive?: boolean;
|
|
42
46
|
}
|
|
43
47
|
export interface ResolvedRun {
|
|
44
48
|
config: Config;
|
|
@@ -52,6 +56,8 @@ export interface ResolvedRun {
|
|
|
52
56
|
versions: ToolVersions;
|
|
53
57
|
/** Your config asks for guestWorkspace "mirror" (project roots then appear at their PC paths too). */
|
|
54
58
|
mirror?: boolean;
|
|
59
|
+
/** The project file's folders outside the project that you allowed (host paths; the launch table marks them). */
|
|
60
|
+
outsideMappings?: string[];
|
|
55
61
|
}
|
|
56
62
|
/** A requested version plus where the request came from (shown to the user). */
|
|
57
63
|
export interface VersionSpec {
|
|
@@ -100,6 +106,11 @@ export interface GenerateOptions {
|
|
|
100
106
|
knownProjects?: string[];
|
|
101
107
|
/** false: no agent session at boot. */
|
|
102
108
|
autoSession?: boolean;
|
|
109
|
+
/** The other projects mapped at boot with their effective configs: the sandbox boots with what they want too. */
|
|
110
|
+
bootWants?: {
|
|
111
|
+
projects: import('./project-policy.js').BootWant[];
|
|
112
|
+
keepOff: string[];
|
|
113
|
+
};
|
|
103
114
|
}
|
|
104
115
|
export interface Generated {
|
|
105
116
|
wsbPath: string;
|
|
@@ -107,6 +118,10 @@ export interface Generated {
|
|
|
107
118
|
launchId: string;
|
|
108
119
|
sessionPath: string;
|
|
109
120
|
memoryMb: number;
|
|
121
|
+
/** The config the sandbox boots with: the launch folder's plus what the other projects mapped at boot want. */
|
|
122
|
+
bootConfig: Config;
|
|
123
|
+
/** Boot settings turned on for another project (setting -> project). */
|
|
124
|
+
widened: Record<string, string>;
|
|
110
125
|
mappings: ResolvedMapping[];
|
|
111
126
|
profile: ProfileResult;
|
|
112
127
|
databases: DatabasePlan[];
|
|
@@ -123,6 +138,8 @@ export interface Generated {
|
|
|
123
138
|
notes: string[];
|
|
124
139
|
};
|
|
125
140
|
toolchains: Record<import('./detect-toolchains.js').AutoToolchain, import('./detect-toolchains.js').ToolchainDecision>;
|
|
141
|
+
/** Where extra folders went when their own path could not be used, and the optional ones skipped. */
|
|
142
|
+
mappingNotes: string[];
|
|
126
143
|
}
|
|
127
144
|
/** secretsRemoved: what secrets.strip kept on this PC, e.g. "settings.json env.AWS_SECRET_ACCESS_KEY". */
|
|
128
145
|
export interface ProfileResult {
|
|
@@ -140,7 +157,7 @@ export interface GuestSession {
|
|
|
140
157
|
workspaceHost: string;
|
|
141
158
|
/** The launch folder's project id (file-name safe; status files and sessions use it). Older launch folders lack it. */
|
|
142
159
|
projectId?: string;
|
|
143
|
-
/** Where
|
|
160
|
+
/** Where project roots appear (C:\Projects), and the guest folders of the roots and the projects mapped at boot. Older launch folders lack them. */
|
|
144
161
|
projectsDir?: string;
|
|
145
162
|
roots?: string[];
|
|
146
163
|
/** false: start no agent session at boot (a restart that adds a project opens its sessions itself). */
|
|
@@ -187,4 +204,10 @@ export interface GuestSession {
|
|
|
187
204
|
}[];
|
|
188
205
|
/** sandbox.cpuLimit as a number (null = no cap); sandbox\cpucap.ps1 applies it before anything starts. */
|
|
189
206
|
cpuLimitPercent?: number | null;
|
|
207
|
+
/** sandbox.pageFileMb (0 = Windows' own); sandbox\pagefile.ps1 applies it at logon. */
|
|
208
|
+
pageFileMb?: number;
|
|
209
|
+
/** Apps to install (catalogue ids and your own programs); missing in launch folders of older versions. */
|
|
210
|
+
apps?: Config['apps'];
|
|
211
|
+
/** audit.enabled and audit.processes: the sandbox reports the programs it starts (host keeps the log). */
|
|
212
|
+
auditProcesses?: boolean;
|
|
190
213
|
}
|
package/dist/core/util.d.ts
CHANGED
|
@@ -34,6 +34,8 @@ export declare const isInsideOrEqual: (parent: string, child: string) => boolean
|
|
|
34
34
|
* plain colours (SGR), so nothing written by the sandbox can drive your terminal (titles, clipboard, cursor tricks).
|
|
35
35
|
*/
|
|
36
36
|
export declare const safeText: (text: string) => string;
|
|
37
|
+
/** safeText without colours either: for text you are asked to approve, which must not be able to hide parts of itself. */
|
|
38
|
+
export declare const plainText: (text: string) => string;
|
|
37
39
|
export declare const findOnPath: (names: string[]) => string | null;
|
|
38
40
|
/** True if `p` itself is a symlink or junction (lstat; never follows it). */
|
|
39
41
|
export declare const isLink: (p: string) => boolean;
|
package/dist/core/util.js
CHANGED
|
@@ -81,6 +81,8 @@ export const isInsideOrEqual = (parent, child) => {
|
|
|
81
81
|
* plain colours (SGR), so nothing written by the sandbox can drive your terminal (titles, clipboard, cursor tricks).
|
|
82
82
|
*/
|
|
83
83
|
export const safeText = (text) => text.replace(/\x1b\[[0-9;]*m|\x1b(\[[0-9;?]*[ -\/]*[@-~]|\][^\x07\x1b]*(\x07|\x1b\\)|[@-Z\\-_])|[\x00-\x08\x0b-\x1f\x7f-\x9f]/g, (m) => (/^\x1b\[[0-9;]*m$/.test(m) ? m : ''));
|
|
84
|
+
/** safeText without colours either: for text you are asked to approve, which must not be able to hide parts of itself. */
|
|
85
|
+
export const plainText = (text) => safeText(text).replace(/\x1b\[[0-9;]*m/g, '');
|
|
84
86
|
export const findOnPath = (names) => {
|
|
85
87
|
const dirs = (process.env.PATH ?? process.env.Path ?? '').split(path.delimiter).filter((d) => d !== '');
|
|
86
88
|
for (const name of names)
|
package/dist/core/workspace.d.ts
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
|
-
import type { Config, Overrides, ResolvedRun, RunRequest, ToolVersions } from './types.js';
|
|
1
|
+
import type { Config, Mapping, Overrides, ResolvedRun, RunRequest, ToolVersions } from './types.js';
|
|
2
|
+
/** 'auto' guest workspace: C:\Workspace\<folder name> (shared/projects.ts, where every project's place is decided). */
|
|
3
|
+
export declare const autoGuestWorkspace: (hostDir: string) => string;
|
|
2
4
|
/**
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
+
* A launch folder inside a project root appears under the root's folder (C:\Projects\<root>\<name>, or its PC path
|
|
6
|
+
* with "mirror"), where it also is when another project started the sandbox, so git worktrees and Claude's project
|
|
7
|
+
* folders keep working whichever project starts it. Only for "auto" / "mirror"; a filtered or read-only workspace
|
|
8
|
+
* keeps its own mapping (the root would show the folder unfiltered and writable at that place).
|
|
5
9
|
*/
|
|
6
|
-
export declare const
|
|
10
|
+
export declare const placeInRoot: (config: Config, workspaceHost: string, mirror: boolean, notes: string[]) => Config;
|
|
7
11
|
/** The Node version a Volta project pins ("volta": { "node": "20.11.1" } in package.json), so nvm defaults to it too. */
|
|
8
12
|
export declare const voltaNode: (workspace: string) => string | null;
|
|
9
13
|
/**
|
|
@@ -16,3 +20,16 @@ export declare const resolveVersions: (config: Config, workspace: string, local:
|
|
|
16
20
|
}, hostNodeVersion: string) => ToolVersions;
|
|
17
21
|
/** Precedence (lowest to highest): built-in defaults, user config, .sandbox-config.json in the workspace, command-line flags. */
|
|
18
22
|
export declare const resolveRun: (req: RunRequest, hostNodeVersion?: string) => Promise<ResolvedRun>;
|
|
23
|
+
/** Where the workspace appears: guestWorkspace "auto", "mirror" (with a note when it falls back) or a path. */
|
|
24
|
+
export declare const guestWorkspaceOf: (config: Pick<Config, "guestWorkspace">, workspaceHost: string) => {
|
|
25
|
+
guest: string;
|
|
26
|
+
note: string | null;
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* Where a mapping will most likely appear, before the launch knows every other folder (the prompt and config check
|
|
30
|
+
* show it; the launch places it for real in host/launch/mappings.ts with the same rules).
|
|
31
|
+
*/
|
|
32
|
+
export declare const previewGuest: (m: Pick<Mapping, "host" | "guest">, workspaceHost: string, workspaceGuest: string) => {
|
|
33
|
+
guest: string;
|
|
34
|
+
note: string | null;
|
|
35
|
+
};
|
package/dist/core/workspace.js
CHANGED
|
@@ -1,18 +1,29 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { z } from 'zod';
|
|
4
|
-
import { planMirrorWorkspace } from '../shared/guest-paths.js';
|
|
4
|
+
import { deriveGuest, placeGuest, planMirrorWorkspace } from '../shared/guest-paths.js';
|
|
5
|
+
import { autoGuestFolder, PROJECTS_GUEST, rootGuestDir, usableRoots } from '../shared/projects.js';
|
|
5
6
|
import { DATABASES } from './detect-databases.js';
|
|
6
|
-
import { applyOverrides, describeOverrides, isTrusted, normalizeOverrides, readLocalConfig, saveTrust, trustStorePath } from './local-config.js';
|
|
7
|
+
import { allowedAnswer, allowRequests, allowStorePath, applyOverrides, describeOverrides, isTrusted, LOCAL_CONFIG_NAME, loosenedRequests, normalizeOverrides, OUTSIDE_FOLDER, outsideKey, outsideMappings, readLocalConfig, saveAllowed, saveTrust, trustStorePath } from './local-config.js';
|
|
7
8
|
import { assertSafeWorkspace } from './safe-paths.js';
|
|
8
|
-
import { dirExists, expandPath, fail, fileExists } from './util.js';
|
|
9
|
+
import { dirExists, expandPath, fail, fileExists, isInsideOrEqual, plainText } from './util.js';
|
|
10
|
+
/** 'auto' guest workspace: C:\Workspace\<folder name> (shared/projects.ts, where every project's place is decided). */
|
|
11
|
+
export const autoGuestWorkspace = autoGuestFolder;
|
|
9
12
|
/**
|
|
10
|
-
*
|
|
11
|
-
*
|
|
13
|
+
* A launch folder inside a project root appears under the root's folder (C:\Projects\<root>\<name>, or its PC path
|
|
14
|
+
* with "mirror"), where it also is when another project started the sandbox, so git worktrees and Claude's project
|
|
15
|
+
* folders keep working whichever project starts it. Only for "auto" / "mirror"; a filtered or read-only workspace
|
|
16
|
+
* keeps its own mapping (the root would show the folder unfiltered and writable at that place).
|
|
12
17
|
*/
|
|
13
|
-
export const
|
|
14
|
-
const
|
|
15
|
-
|
|
18
|
+
export const placeInRoot = (config, workspaceHost, mirror, notes) => {
|
|
19
|
+
const guest = rootGuestDir(usableRoots(config.projects.roots, dirExists), mirror, workspaceHost);
|
|
20
|
+
if (!guest)
|
|
21
|
+
return config;
|
|
22
|
+
if (config.workspaceFilter.enabled || config.readOnly) {
|
|
23
|
+
notes.push(workspaceHost + ' is inside a project root, but ' + (config.readOnly ? 'readOnly' : 'workspaceFilter') + ' is on, so it is mapped on its own at ' + config.guestWorkspace + ' (when another project starts the sandbox it is at ' + guest + ').');
|
|
24
|
+
return config;
|
|
25
|
+
}
|
|
26
|
+
return { ...config, guestWorkspace: guest };
|
|
16
27
|
};
|
|
17
28
|
const firstLine = (file) => {
|
|
18
29
|
if (!fileExists(file))
|
|
@@ -79,24 +90,77 @@ export const resolveRun = async (req, hostNodeVersion = process.version) => {
|
|
|
79
90
|
let localConfigPath = null;
|
|
80
91
|
let overrides = {};
|
|
81
92
|
const local = req.noLocalConfig ? null : readLocalConfig(workspaceHost);
|
|
93
|
+
let outsideAllowed = [];
|
|
82
94
|
if (local) {
|
|
83
95
|
overrides = normalizeOverrides(local.data, workspaceHost, local.path);
|
|
84
96
|
const store = trustStorePath(storageRoot);
|
|
85
97
|
if (!isTrusted(store, local.path, local.hash)) {
|
|
86
|
-
const summary = describeOverrides(overrides);
|
|
98
|
+
const summary = describeOverrides(overrides, workspaceHost);
|
|
87
99
|
let trusted = req.trustConfig;
|
|
88
100
|
if (!trusted && req.ask) {
|
|
89
101
|
console.log('\nFound ' + local.path + ' which is not trusted yet. It would apply:');
|
|
90
102
|
for (const line of summary)
|
|
91
|
-
console.log(' ' + line);
|
|
103
|
+
console.log(' ' + plainText(line));
|
|
92
104
|
trusted = await req.ask('Trust and use this file?');
|
|
93
105
|
}
|
|
94
106
|
if (!trusted)
|
|
95
|
-
fail(local.path + ' has not been approved yet (a repo can ship this file, so it needs your OK). It would apply:\n ' + (summary.join('\n ') || '(no changes)'), 'Review it, then run again in an interactive terminal and answer y, or add --trust-config. To ignore the file: --no-local-config');
|
|
107
|
+
fail(local.path + ' has not been approved yet (a repo can ship this file, so it needs your OK). It would apply:\n ' + (summary.map(plainText).join('\n ') || '(no changes)'), 'Review it, then run again in an interactive terminal and answer y, or add --trust-config. To ignore the file: --no-local-config');
|
|
96
108
|
saveTrust(store, local.path, local.hash);
|
|
97
109
|
notes.push('Trusted ' + local.path + ' (you will only be asked again if the file changes).');
|
|
98
110
|
}
|
|
99
|
-
|
|
111
|
+
// A project may only tighten. What it asks for beyond that (a looser setting, a folder outside the project) is used
|
|
112
|
+
// only if you allowed it for this project: asked once, in one question, at an interactive launch, never by
|
|
113
|
+
// --trust-config. Folders outside the project follow projects.allowOutsideMappings (your settings only). A looser
|
|
114
|
+
// setting you did not allow is named in the notes instead of dropped silently.
|
|
115
|
+
const allowStore = allowStorePath(storageRoot);
|
|
116
|
+
const policy = config.projects.allowOutsideMappings;
|
|
117
|
+
// An optional folder that is not on this PC is skipped at launch, so it needs no answer (yet).
|
|
118
|
+
const outside = outsideMappings(workspaceHost, overrides.mappings).filter((m) => !m.optional || dirExists(m.host));
|
|
119
|
+
const name = path.basename(workspaceHost) + '\'s ' + LOCAL_CONFIG_NAME;
|
|
120
|
+
const preview = guestWorkspaceOf(applyOverrides(config, overrides), workspaceHost).guest;
|
|
121
|
+
const describe = (m) => (m.readOnly ? 'read-only ' : 'READ/WRITE ') + m.host + ' -> ' + previewGuest(m, workspaceHost, preview).guest;
|
|
122
|
+
if (outside.length && policy === false) {
|
|
123
|
+
fail(name + ' maps ' + outside.length + ' folder' + (outside.length === 1 ? '' : 's') + ' outside the project, and your settings do not allow that:\n ' + outside.map(describe).join('\n '), 'Allow it: set projects.allowOutsideMappings to "ask" or true in your settings (sandbox > Settings, or the file sandbox config path shows). Or remove those mappings from the file.');
|
|
124
|
+
}
|
|
125
|
+
const looser = loosenedRequests(config, overrides);
|
|
126
|
+
const interactive = Boolean(req.ask) && (req.interactive ?? Boolean(process.stdin.isTTY));
|
|
127
|
+
const openOutside = policy === 'ask' ? outside.filter((m) => allowedAnswer(allowStore, local.path, OUTSIDE_FOLDER, outsideKey(m)) !== true) : [];
|
|
128
|
+
const openLooser = looser.filter((a) => allowedAnswer(allowStore, local.path, a.key, a.wanted) === null);
|
|
129
|
+
let declinedOutside = false;
|
|
130
|
+
if (interactive && req.ask && (openOutside.length || openLooser.length)) {
|
|
131
|
+
console.log('\n' + name + ' asks for more than your settings allow:');
|
|
132
|
+
if (openOutside.length)
|
|
133
|
+
console.log(' Folders outside the project:\n' + openOutside.map((m) => ' ' + plainText(describe(m))).join('\n'));
|
|
134
|
+
for (const a of openLooser)
|
|
135
|
+
console.log(' ' + a.key + ' = ' + JSON.stringify(a.wanted) + ' (your settings: ' + JSON.stringify(a.yours) + ')');
|
|
136
|
+
const ok = await req.ask('Allow ' + (openOutside.length + openLooser.length === 1 ? 'it' : 'these') + ' for this project?');
|
|
137
|
+
for (const a of openLooser)
|
|
138
|
+
saveAllowed(allowStore, local.path, a.key, a.wanted, ok);
|
|
139
|
+
// A "no" to folders is not kept: the next interactive launch asks again.
|
|
140
|
+
if (ok)
|
|
141
|
+
for (const m of openOutside)
|
|
142
|
+
saveAllowed(allowStore, local.path, OUTSIDE_FOLDER, outsideKey(m), true);
|
|
143
|
+
declinedOutside = !ok && openOutside.length > 0;
|
|
144
|
+
}
|
|
145
|
+
const notAllowed = policy === 'ask' ? outside.filter((m) => allowedAnswer(allowStore, local.path, OUTSIDE_FOLDER, outsideKey(m)) !== true) : [];
|
|
146
|
+
if (notAllowed.length) {
|
|
147
|
+
const what = name + ' maps ' + notAllowed.length + ' folder' + (notAllowed.length === 1 ? '' : 's') + ' outside the project:\n ' + notAllowed.map(describe).join('\n ');
|
|
148
|
+
fail(what + (declinedOutside ? '\nYou did not allow ' + (notAllowed.length === 1 ? 'it' : 'them') + ', so the sandbox was not started.' : '\nThat needs your OK once (a repo ships this file), and this launch cannot ask (no interactive terminal).'), 'Allow ' + (notAllowed.length === 1 ? 'it' : 'them') + ': start it from an interactive terminal and answer y, or set projects.allowOutsideMappings to true in your settings (sandbox > Settings, or the file sandbox config path shows).');
|
|
149
|
+
}
|
|
150
|
+
outsideAllowed = [...new Set(outside.map((m) => m.host))];
|
|
151
|
+
if (outside.length)
|
|
152
|
+
notes.push('Folders outside the project allowed for ' + name + ' (' + (policy === true ? 'projects.allowOutsideMappings is true' : 'your answer is kept in ' + allowStore) + '): ' + outsideAllowed.join(', '));
|
|
153
|
+
const allowed = [];
|
|
154
|
+
for (const ask of looser) {
|
|
155
|
+
const ok = allowedAnswer(allowStore, local.path, ask.key, ask.wanted);
|
|
156
|
+
if (ok) {
|
|
157
|
+
allowed.push(ask);
|
|
158
|
+
notes.push('Allowed for this project: ' + ask.key + ' = ' + JSON.stringify(ask.wanted) + ' (your answer is kept in ' + allowStore + ').');
|
|
159
|
+
}
|
|
160
|
+
else
|
|
161
|
+
notes.push('The project asks for ' + ask.key + ' = ' + JSON.stringify(ask.wanted) + ', but your settings say ' + JSON.stringify(ask.yours) + ', so it is not used. ' + (ok === false ? 'You said no (kept in ' + allowStore + ').' : 'Start it from an interactive terminal to allow it for this project.') + ' Or set ' + ask.key + ' in your own settings (sandbox, then Settings > All projects).');
|
|
162
|
+
}
|
|
163
|
+
config = applyOverrides(allowRequests(config, allowed), overrides);
|
|
100
164
|
scripts.push(...(overrides.scripts ?? []));
|
|
101
165
|
localConfigPath = local.path;
|
|
102
166
|
notes.push('Using ' + local.path);
|
|
@@ -109,14 +173,12 @@ export const resolveRun = async (req, hostNodeVersion = process.version) => {
|
|
|
109
173
|
fail('Unknown database "' + name + '".', 'Choose from: ' + DATABASES.join(', '));
|
|
110
174
|
config = { ...config, databases: { ...config.databases, [db]: { ...config.databases[db], enabled: true } } };
|
|
111
175
|
}
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
config = { ...config, guestWorkspace: plan.guest };
|
|
119
|
-
}
|
|
176
|
+
// auto / mirror: a project inside a root is where the root shows it, whichever project starts the sandbox.
|
|
177
|
+
const guest = guestWorkspaceOf(config, workspaceHost);
|
|
178
|
+
const placed = config.guestWorkspace === 'auto' || mirror ? placeInRoot({ ...config, guestWorkspace: guest.guest }, workspaceHost, mirror, notes) : { ...config, guestWorkspace: guest.guest };
|
|
179
|
+
if (guest.note && placed.guestWorkspace === guest.guest)
|
|
180
|
+
notes.push(guest.note);
|
|
181
|
+
config = placed;
|
|
120
182
|
scripts.push(...req.scripts.map((s) => path.resolve(s)));
|
|
121
183
|
const harnessName = req.noAgent ? 'none' : (req.harness ?? config.defaultHarness);
|
|
122
184
|
if (harnessName !== 'none' && !config.harnesses[harnessName])
|
|
@@ -124,5 +186,21 @@ export const resolveRun = async (req, hostNodeVersion = process.version) => {
|
|
|
124
186
|
const versions = resolveVersions(config, workspaceHost, overrides, req, hostNodeVersion);
|
|
125
187
|
const dataPath = path.join(storageRoot, 'data', config.storage.dataName);
|
|
126
188
|
// A project file may ask for "mirror" too (it decides only its own path); project roots follow the user's choice.
|
|
127
|
-
return { config, storageRoot, dataPath, workspaceHost, harnessName, scripts, localConfigPath, notes, versions, mirror };
|
|
189
|
+
return { config, storageRoot, dataPath, workspaceHost, harnessName, scripts, localConfigPath, notes, versions, mirror, outsideMappings: outsideAllowed };
|
|
190
|
+
};
|
|
191
|
+
/** Where the workspace appears: guestWorkspace "auto", "mirror" (with a note when it falls back) or a path. */
|
|
192
|
+
export const guestWorkspaceOf = (config, workspaceHost) => {
|
|
193
|
+
if (config.guestWorkspace === 'auto')
|
|
194
|
+
return { guest: autoGuestWorkspace(workspaceHost), note: null };
|
|
195
|
+
if (config.guestWorkspace === 'mirror')
|
|
196
|
+
return planMirrorWorkspace(workspaceHost, autoGuestWorkspace(workspaceHost));
|
|
197
|
+
return { guest: config.guestWorkspace, note: null };
|
|
128
198
|
};
|
|
199
|
+
/**
|
|
200
|
+
* Where a mapping will most likely appear, before the launch knows every other folder (the prompt and config check
|
|
201
|
+
* show it; the launch places it for real in host/launch/mappings.ts with the same rules).
|
|
202
|
+
*/
|
|
203
|
+
export const previewGuest = (m, workspaceHost, workspaceGuest) => placeGuest(m.host, m.guest ?? deriveGuest(m.host, workspaceHost, workspaceGuest), [
|
|
204
|
+
...(m.guest || !isInsideOrEqual(workspaceHost, m.host) ? [{ guest: workspaceGuest, what: 'the project' }] : []),
|
|
205
|
+
{ guest: PROJECTS_GUEST, what: 'other projects' },
|
|
206
|
+
]);
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Config, GitAccess } from '../../core/types.js';
|
|
2
|
+
import { type ToolEnv } from '../toolchains/common.js';
|
|
3
|
+
export { appsNeedMesa } from '../../shared/apps.js';
|
|
4
|
+
export interface AppsOptions {
|
|
5
|
+
gitAccess: GitAccess;
|
|
6
|
+
refresh: boolean;
|
|
7
|
+
}
|
|
8
|
+
/** The program in an installed folder (breadth first, bounded: big apps have tens of thousands of files). */
|
|
9
|
+
export declare const findExe: (root: string, exe: string) => string | null;
|
|
10
|
+
export declare const setupApps: (env: ToolEnv, apps: Config["apps"], o: AppsOptions) => Promise<void>;
|