windows-development-sandbox 1.18.4 → 1.20.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 +1 -1
- package/config.example.json +9 -0
- package/dist/app/features/accounts/accounts.def.d.ts +4 -0
- package/dist/app/features/accounts/accounts.run.d.ts +1 -0
- package/dist/app/features/accounts/services.def.d.ts +3 -0
- package/dist/app/features/accounts/services.def.js +4 -2
- package/dist/app/features/accounts/services.run.d.ts +1 -0
- package/dist/app/features/accounts/services.run.js +3 -1
- package/dist/app/features/agent-commands/agent-commands.def.d.ts +37 -0
- package/dist/app/features/agent-commands/agent-commands.def.js +91 -0
- package/dist/app/features/agent-commands/agent-commands.run.d.ts +28 -0
- package/dist/app/features/agent-commands/agent-commands.run.js +35 -0
- package/dist/app/features/agent-commands/index.d.ts +2 -0
- package/dist/app/features/agent-commands/index.js +3 -0
- package/dist/app/features/agents/agents.def.d.ts +14 -1
- package/dist/app/features/agents/agents.def.js +61 -6
- package/dist/app/features/agents/agents.run.d.ts +1 -0
- package/dist/app/features/agents/agents.run.js +8 -1
- package/dist/app/features/agents/claude-env.def.d.ts +36 -0
- package/dist/app/features/agents/claude-env.def.js +305 -0
- package/dist/app/features/agents/env.def.d.ts +163 -0
- package/dist/app/features/agents/env.def.js +184 -0
- package/dist/app/features/agents/env.run.d.ts +43 -0
- package/dist/app/features/agents/env.run.js +86 -0
- package/dist/app/features/agents/index.d.ts +2 -0
- package/dist/app/features/agents/index.js +2 -0
- package/dist/app/features/defs.d.ts +132 -1
- package/dist/app/features/defs.js +9 -0
- package/dist/app/features/settings/categories.def.js +5 -2
- package/dist/app/features/settings/settings-rows.js +5 -1
- package/dist/app/features/settings/settings.run.d.ts +8 -0
- package/dist/app/features/settings/settings.run.js +9 -0
- package/dist/app/features/updates/updates.run.js +12 -2
- package/dist/app/features/vpn/vpn.def.d.ts +33 -0
- package/dist/app/features/vpn/vpn.def.js +30 -6
- package/dist/app/features/vpn/vpn.run.d.ts +11 -0
- package/dist/app/features/vpn/vpn.run.js +4 -1
- package/dist/app/kernel/rpc.d.ts +1 -1
- package/dist/app/registry.js +7 -1
- package/dist/bin/agent-sandbox.js +2 -2
- package/dist/bin/{chunk-MY7NLQQY.js → chunk-2IMT7ZCT.js} +1 -1
- package/dist/bin/{chunk-N4GXYTLV.js → chunk-DK4NTBUF.js} +69 -26
- package/dist/bin/{chunk-FRPLQLSY.js → chunk-Z2P5OGKL.js} +4323 -3463
- package/dist/bin/claude-host.js +2 -2
- package/dist/bin/claude-sbx.js +2 -2
- package/dist/bin/codex-sbx.js +2 -2
- package/dist/bin/gemini-sbx.js +2 -2
- package/dist/bin/sandbox.js +2 -2
- package/dist/bin/sbx-hide.js +2 -2
- package/dist/bin/sbx-show.js +2 -2
- package/dist/cli/commands/agent-commands.d.ts +0 -1
- package/dist/cli/commands/agent-commands.js +2 -32
- package/dist/cli/commands/index.d.ts +1 -1
- package/dist/cli/commands/index.js +1 -1
- package/dist/cli/commands/launch/launch.js +2 -2
- package/dist/cli/commands/registry.js +3 -2
- package/dist/cli/manifest.d.ts +1 -1
- package/dist/cli/manifest.js +1 -1
- package/dist/cli/menu/menu.d.ts +1 -1
- package/dist/cli/menu/menu.js +3 -3
- package/dist/cli/menu/pages/agents-page.d.ts +7 -2
- package/dist/cli/menu/pages/agents-page.js +67 -11
- package/dist/cli/menu/pages/maintenance-page.js +1 -2
- package/dist/cli/menu/pages/settings-page.js +1 -0
- package/dist/cli/menu/views.js +2 -1
- package/dist/cli/services/agent-commands-service.d.ts +2 -0
- package/dist/cli/services/agent-commands-service.js +13 -7
- package/dist/cli/services/vpn-service.d.ts +5 -1
- package/dist/cli/services/vpn-service.js +6 -1
- package/dist/cli/tui/dialogs/text-screen.js +2 -1
- package/dist/cli/tui/terminal.js +3 -1
- package/dist/core/config/defaults.js +9 -0
- package/dist/core/detect/toolchains.js +9 -0
- package/dist/core/disk.d.ts +1 -5
- package/dist/core/disk.js +1 -16
- package/dist/core/schema/codex.d.ts +29 -0
- package/dist/core/schema/codex.js +24 -0
- package/dist/core/schema/config.d.ts +46 -14
- package/dist/core/schema/config.js +2 -0
- package/dist/core/schema/index.d.ts +1 -0
- package/dist/core/schema/index.js +1 -0
- package/dist/core/schema/toolchains.d.ts +4 -0
- package/dist/core/schema/toolchains.js +3 -0
- package/dist/core/types.d.ts +2 -0
- package/dist/guest/accounts/apply.d.ts +6 -6
- package/dist/guest/agents/agent-files.d.ts +26 -6
- package/dist/guest/agents/agent-files.js +84 -14
- package/dist/guest/agents/local-state.d.ts +5 -2
- package/dist/guest/agents/local-state.js +24 -10
- package/dist/guest/boot/boot.js +10 -5
- package/dist/guest/daemon/sessions.js +2 -1
- package/dist/guest/toolchains/installers.d.ts +6 -10
- package/dist/guest/toolchains/installers.js +4 -12
- package/dist/guest/toolchains/native/autosdk.d.ts +3 -0
- package/dist/guest/toolchains/native/autosdk.js +50 -0
- package/dist/guest/toolchains/native/unreal.js +3 -0
- package/dist/guest/toolchains/native/vcruntime.d.ts +0 -3
- package/dist/guest/toolchains/native/vcruntime.js +2 -5
- package/dist/guest/toolchains/native/visual-studio.d.ts +2 -2
- package/dist/guest/toolchains/native/visual-studio.js +100 -16
- package/dist/guest/toolchains/native/vs-request.d.ts +21 -0
- package/dist/guest/toolchains/native/vs-request.js +60 -1
- package/dist/guest/toolchains/not-yet.d.ts +1 -1
- package/dist/guest/toolchains/not-yet.js +3 -3
- package/dist/guest/toolchains/persisted.js +2 -7
- package/dist/guest/toolchains/plan.js +9 -4
- package/dist/guest/toolchains/signature.d.ts +5 -0
- package/dist/guest/toolchains/signature.js +22 -0
- package/dist/guest/vpn/tunnel.d.ts +3 -0
- package/dist/guest/vpn/tunnel.js +10 -12
- package/dist/guest/vpn/vpn.js +6 -1
- package/dist/host/accounts/leases.d.ts +3 -3
- package/dist/host/accounts/source.js +1 -1
- package/dist/host/agent-commands/commands.js +1 -1
- package/dist/host/agent-commands/forwarders.d.ts +14 -2
- package/dist/host/agent-commands/forwarders.js +31 -13
- package/dist/host/agent-commands/reconcile.d.ts +2 -1
- package/dist/host/agent-commands/reconcile.js +7 -8
- package/dist/host/agent-commands/user-path.d.ts +5 -0
- package/dist/host/agent-commands/user-path.js +13 -0
- package/dist/host/launch/session-file.js +1 -0
- package/dist/host/profile/accounts.js +2 -4
- package/dist/host/profile/claude-login.d.ts +3 -2
- package/dist/host/profile/claude-login.js +3 -13
- package/dist/host/vpn/index.d.ts +2 -1
- package/dist/host/vpn/index.js +2 -1
- package/dist/host/vpn/last.d.ts +28 -0
- package/dist/host/vpn/last.js +43 -0
- package/dist/host/vpn/login.d.ts +3 -1
- package/dist/host/vpn/login.js +17 -3
- package/dist/runtime/agent.js +28 -0
- package/dist/runtime/daemon.js +1117 -912
- package/dist/runtime/db.js +30 -0
- package/dist/runtime/main.js +1308 -1036
- package/dist/runtime/netproxy.js +22 -0
- package/dist/runtime/tool.js +238 -208
- package/dist/runtime/view.js +22 -0
- package/dist/selftest/agent-commands.test.js +45 -7
- package/dist/selftest/agent-paths.test.js +1 -12
- package/dist/selftest/agent-settings.test.d.ts +2 -0
- package/dist/selftest/agent-settings.test.js +94 -0
- package/dist/selftest/app.test.js +2 -1
- package/dist/selftest/cli.test.js +4 -0
- package/dist/selftest/index.js +2 -0
- package/dist/selftest/menu.test.js +5 -3
- package/dist/selftest/pc-installs.test.js +5 -0
- package/dist/selftest/persisted-installs.test.js +69 -0
- package/dist/selftest/service-accounts.test.js +1 -1
- package/dist/selftest/support/engine-golden.js +1 -1
- package/dist/selftest/support/fixtures.d.ts +9 -0
- package/dist/selftest/vpn.test.js +51 -3
- package/dist/shared/accounts/anthropic.d.ts +5 -0
- package/dist/shared/accounts/anthropic.js +15 -0
- package/dist/shared/accounts/index.d.ts +1 -0
- package/dist/shared/accounts/index.js +1 -0
- package/dist/shared/channel/schema.d.ts +19 -0
- package/dist/shared/channel/schema.js +2 -0
- package/dist/shared/io/disk.d.ts +9 -0
- package/dist/shared/io/disk.js +19 -0
- package/dist/shared/io/index.d.ts +1 -0
- package/dist/shared/io/index.js +1 -0
- package/dist/shared/readiness.d.ts +9 -0
- package/dist/shared/readiness.js +11 -0
- package/dist/shared/signer.d.ts +24 -0
- package/dist/shared/signer.js +15 -0
- package/dist/shared/vpn/failure.d.ts +10 -0
- package/dist/shared/vpn/failure.js +28 -0
- package/dist/sync/watcher.js +2 -0
- package/dist/web/index.html +1 -1
- package/dist/web/{main-GVZWWP4I.js → main-OUUQYNK4.js} +75 -74
- package/native/csharp/HcsHost/AssemblyInfo.cs +3 -3
- package/native/csharp/HostTools/AssemblyInfo.cs +3 -3
- package/native/csharp/HostTools/Program.cs +14 -1
- package/native/csharp/HostTools/UserPath.cs +17 -0
- package/native/csharp/StorageHelper/AssemblyInfo.cs +3 -3
- package/native/scripts/edit-user-path.ps1 +6 -2
- package/package.json +1 -1
- package/sandbox/scripts/get-signature.ps1 +13 -0
- package/schemas/config.schema.json +64 -0
- package/schemas/sandbox-config.schema.json +8 -0
- package/sandbox/scripts/get-signer.ps1 +0 -8
package/dist/runtime/view.js
CHANGED
|
@@ -25955,6 +25955,22 @@ var DatabasesSchema = external_exports.strictObject({
|
|
|
25955
25955
|
})
|
|
25956
25956
|
});
|
|
25957
25957
|
|
|
25958
|
+
// src/core/schema/codex.ts
|
|
25959
|
+
var CodexSchema = external_exports.strictObject({
|
|
25960
|
+
sandboxMode: external_exports.enum(["danger-full-access", "workspace-write", "read-only"]).describe(
|
|
25961
|
+
"Codex's own sandbox. Inside Windows Sandbox Codex's own sandbox only slows it down (it re-grants file permissions over your profile at every boot), so the default is danger-full-access: Windows Sandbox is the sandbox."
|
|
25962
|
+
),
|
|
25963
|
+
approvalPolicy: external_exports.enum(["never", "on-request", "on-failure", "untrusted"]).describe('When Codex asks before it runs a command. "never" with danger-full-access starts it with no questions.'),
|
|
25964
|
+
model: external_exports.string().max(100).regex(/^[A-Za-z0-9._:/-]*$/, "letters, digits and . _ : / - only").describe("The model Codex uses. Empty = Codex's own default."),
|
|
25965
|
+
reasoningEffort: external_exports.enum(["default", "minimal", "low", "medium", "high", "xhigh"]).describe(`How hard Codex thinks. "default" = Codex's own default.`),
|
|
25966
|
+
windowsSandbox: external_exports.enum(["off", "unelevated", "elevated"]).describe(
|
|
25967
|
+
`Codex's Windows sandbox mode in its config.toml. "off" removes the setting so Codex does not set up its sandbox users and firewall rules at every boot.`
|
|
25968
|
+
),
|
|
25969
|
+
mcpLoginsInFile: external_exports.boolean().describe(
|
|
25970
|
+
`Keep the sign-ins of Codex's MCP servers in a file in the saved Codex folder (mcp_oauth_credentials_store = "file"), so they survive between sandboxes. A value you set in config.toml is kept.`
|
|
25971
|
+
)
|
|
25972
|
+
});
|
|
25973
|
+
|
|
25958
25974
|
// src/core/schema/harness.ts
|
|
25959
25975
|
var SubfolderSchema = external_exports.string().regex(/^(?!.*(^|[\\/])\.\.([\\/]|$))[A-Za-z0-9._ -]+([\\/][A-Za-z0-9._ -]+)*$/, "a plain relative folder");
|
|
25960
25976
|
var HarnessHomeSchema = external_exports.strictObject({
|
|
@@ -26267,6 +26283,9 @@ var ToolchainsSchema = external_exports.strictObject({
|
|
|
26267
26283
|
keepInstallFiles: external_exports.boolean().describe(
|
|
26268
26284
|
"Keep an offline copy of the Visual Studio install files in tools\\visualstudio (~10 GB, made once), so later boots install without downloading them again. Skipped while that disk has too little room."
|
|
26269
26285
|
),
|
|
26286
|
+
visualStudio: external_exports.enum(["full", "autosdk"]).describe(
|
|
26287
|
+
`"full": install Visual Studio Community (the engine finds it through the Setup API; ~10 GB, redone every boot). "autosdk" (experimental): skip Visual Studio and build an Unreal AutoSDK tree from the portable MSVC in tools\\unreal\\autosdk (kept between sandboxes); UnrealBuildTool finds MSVC through AutoSDK, but the editor's Visual Studio integration is not available. Needs toolchains.msvc (turned on for you).`
|
|
26288
|
+
),
|
|
26270
26289
|
persistEngines: external_exports.boolean().describe(
|
|
26271
26290
|
"Engines the Epic Games Launcher installs go to tools\\unreal\\engines (kept between sandboxes, 40-60 GB each) and are registered with the launcher again every boot. Off: they go on the sandbox disk and are lost when it closes. Engines installed on this PC can be mapped instead (pcInstalls.unrealEngines)."
|
|
26272
26291
|
),
|
|
@@ -27723,6 +27742,7 @@ var ConfigSchema = external_exports.strictObject({
|
|
|
27723
27742
|
apps: AppsSchema,
|
|
27724
27743
|
claudeProfile: ClaudeProfileSchema,
|
|
27725
27744
|
harnesses: external_exports.record(NameSchema, HarnessSchema).describe("Agents that can run in the sandbox."),
|
|
27745
|
+
codex: CodexSchema,
|
|
27726
27746
|
hooks: external_exports.strictObject({ failFast: external_exports.boolean().describe("Stop when a hook script fails.") }),
|
|
27727
27747
|
projects: ProjectsSchema.describe(
|
|
27728
27748
|
"Several projects in one sandbox: folders of projects mapped at every start, and adding folders while it runs."
|
|
@@ -27966,6 +27986,8 @@ var ProjectStatusSchema = external_exports.object({
|
|
|
27966
27986
|
var VpnStatusSchema = external_exports.object({
|
|
27967
27987
|
state: external_exports.enum(["off", "starting", "up", "failed"]),
|
|
27968
27988
|
message: Text(2e3),
|
|
27989
|
+
/** Why it failed, when it did: `auth` (login refused), `unreachable`, `driver`, `timeout` or `other`. */
|
|
27990
|
+
code: external_exports.enum(["auth", "unreachable", "driver", "timeout", "other"]).optional(),
|
|
27969
27991
|
killSwitch: external_exports.boolean(),
|
|
27970
27992
|
/** A direct connection was tried after the kill switch was applied: false = it still got out. */
|
|
27971
27993
|
killSwitchHeld: external_exports.boolean().optional(),
|
|
@@ -1,14 +1,18 @@
|
|
|
1
|
-
// Agent commands
|
|
1
|
+
// Agent commands: which agents get a forwarding command, what the files contain, and that "off" changes nothing.
|
|
2
2
|
// Everything happens in the test's temp folder with a made-up PATH; your real PATH is never read or written.
|
|
3
3
|
import fs from 'node:fs';
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import { DEFAULTS } from '../core/config/index.js';
|
|
6
6
|
import { agentCommands, commandsDir, realAgent } from '../host/agent-commands/index.js';
|
|
7
7
|
import { forwardingCmd, forwardingSh } from '../host/agent-commands/index.js';
|
|
8
|
-
import { reconcileAgentCommands } from '../host/agent-commands/index.js';
|
|
8
|
+
import { forwardTarget, reconcileAgentCommands } from '../host/agent-commands/index.js';
|
|
9
|
+
import { agentCommandsSection } from '../app/features/agent-commands/agent-commands.def.js';
|
|
10
|
+
import { applyAgentCommandsChange } from '../app/features/settings/settings.run.js';
|
|
11
|
+
import { fakeIO } from '../app/kernel/index.js';
|
|
12
|
+
import { runDefinition } from '../app/registry.js';
|
|
9
13
|
export const agentCommandsSuite = {
|
|
10
14
|
name: 'agent-commands',
|
|
11
|
-
title: 'agent commands
|
|
15
|
+
title: 'agent commands: claude, codex ... forwarded into a hidden sandbox',
|
|
12
16
|
run: async (t) => {
|
|
13
17
|
const storage = t.at('agent-cmd-storage');
|
|
14
18
|
const npmDir = t.at('agent-cmd-npm');
|
|
@@ -33,19 +37,53 @@ export const agentCommandsSuite = {
|
|
|
33
37
|
t.check(!agentCommands(on, storage, unsafeDir).some((a) => a.command === 'aider'), 'an agent in a folder whose path has % or ^ (which cmd.exe would expand) is left alone');
|
|
34
38
|
t.section('the forwarding files');
|
|
35
39
|
const claude = { harness: 'claude', command: 'claude', real: path.join(localBin, 'claude.exe') };
|
|
36
|
-
const
|
|
40
|
+
const direct = { node: 'C:\\node\\node.exe', entry: 'C:\\tool\\dist\\bin\\sandbox.js' };
|
|
41
|
+
const cmd = forwardingCmd(claude, direct);
|
|
37
42
|
t.check(cmd.includes('"C:\\node\\node.exe" "C:\\tool\\dist\\bin\\sandbox.js" launch --harness claude --headless --terminal -- %*') &&
|
|
38
43
|
cmd.includes('if "%SANDBOX_AGENT_DIRECT%"=="1" goto direct') &&
|
|
39
44
|
cmd.includes('call "' + claude.real + '" %*'), 'the .cmd starts the agent in a hidden sandbox attached to this terminal, unless SANDBOX_AGENT_DIRECT=1 (real agent)');
|
|
40
|
-
|
|
45
|
+
t.check(cmd.includes('if not exist "C:\\node\\node.exe" goto direct') &&
|
|
46
|
+
cmd.includes('if not exist "C:\\tool\\dist\\bin\\sandbox.js" goto direct') &&
|
|
47
|
+
cmd.indexOf('if not exist') < cmd.indexOf('launch --harness'), 'the .cmd runs the real agent when the tool was uninstalled or moved, instead of failing');
|
|
48
|
+
const managed = forwardTarget('C:\\tool\\versions\\1.0\\package', 'C:\\tool', 'C:\\node\\node.exe');
|
|
49
|
+
const viaLauncher = forwardingCmd(claude, managed);
|
|
50
|
+
t.check(managed.launcher === 'C:\\tool\\bin\\sandbox.cmd' &&
|
|
51
|
+
forwardTarget('C:\\src\\repo', null, 'C:\\node\\node.exe').launcher === undefined &&
|
|
52
|
+
viaLauncher.includes('call "C:\\tool\\bin\\sandbox.cmd" launch --harness claude --headless --terminal -- %*') &&
|
|
53
|
+
!viaLauncher.includes('versions') &&
|
|
54
|
+
forwardingSh(claude, managed).includes("[ -f '/c/tool/bin/sandbox' ] || exec "), 'under a managed install the files call its launcher (it survives updates), else node and the entry file');
|
|
55
|
+
const sh = forwardingSh({ ...claude, real: "C:\\Users\\me\\it's\\claude.exe" }, {
|
|
56
|
+
node: 'C:\\node\\node.exe',
|
|
57
|
+
entry: 'C:\\tool\\sandbox.js',
|
|
58
|
+
});
|
|
41
59
|
t.check(sh.startsWith('#!/bin/sh') &&
|
|
42
60
|
sh.includes("exec '/c/node/node.exe' '/c/tool/sandbox.js' launch --harness claude --headless --terminal -- \"$@\"") &&
|
|
43
61
|
sh.includes("'/c/Users/me/it'\\''s/claude.exe'"), 'Git Bash gets a shell script with POSIX paths, quoted safely');
|
|
44
62
|
t.section('off changes nothing');
|
|
45
63
|
// Off with nothing set up: nothing to do, no PATH edit (which would be refused during the self-test anyway).
|
|
46
64
|
const fresh = t.at('agent-cmd-fresh');
|
|
47
|
-
const
|
|
65
|
+
const target = { node: 'C:\\node\\node.exe', entry: 'C:\\tool\\sandbox.js' };
|
|
66
|
+
const off = reconcileAgentCommands(DEFAULTS, fresh, target);
|
|
48
67
|
t.equal({ on: off.on, changed: off.changed, wrote: fs.existsSync(commandsDir(fresh)) }, { on: false, changed: false, wrote: false }, 'off (the default) writes nothing and leaves your PATH alone');
|
|
49
|
-
await t.throws(() => reconcileAgentCommands(on, t.at('agent-cmd-on'),
|
|
68
|
+
await t.throws(() => reconcileAgentCommands(on, t.at('agent-cmd-on'), target), /self-test|PATH/i, 'turning it on edits your PATH, which the self-test refuses (it never changes this PC)');
|
|
69
|
+
t.section('every surface');
|
|
70
|
+
await t.throws(() => runDefinition('agentCommands.on', {}, {}, fakeIO().io), /self-test|PATH|inside the sandbox/i, 'agentCommands.on refuses in the self-test (it would change this PC)');
|
|
71
|
+
const reconciled = [];
|
|
72
|
+
const fakeService = { reconcile: (c) => reconciled.push(c) };
|
|
73
|
+
const ctx = { loaded: { configPath: undefined } };
|
|
74
|
+
applyAgentCommandsChange(ctx, 'user', 'agentCommands.enabled', fakeService);
|
|
75
|
+
applyAgentCommandsChange(ctx, 'user', 'defaultHarness', fakeService);
|
|
76
|
+
applyAgentCommandsChange(ctx, 'project', 'agentCommands.enabled', fakeService);
|
|
77
|
+
t.equal(reconciled.length, 1, 'changing agentCommands.* in your settings (any surface) brings this PC in line');
|
|
78
|
+
const section = agentCommandsSection({
|
|
79
|
+
enabled: false,
|
|
80
|
+
dir: 'C:\\x',
|
|
81
|
+
found: [{ command: 'claude', harness: 'claude', real: 'C:\\r\\claude.exe' }],
|
|
82
|
+
});
|
|
83
|
+
const toggle = section.items?.[0].primary;
|
|
84
|
+
t.check(section.items?.map((i) => i.id).join() === 'agentCommands,agentCommand:claude' &&
|
|
85
|
+
toggle?.kind === 'operation' &&
|
|
86
|
+
toggle.op === 'agentCommands.on' &&
|
|
87
|
+
!JSON.stringify(section).includes('sandbox agent-commands'), 'the Agents page offers the switch as an action and lists each agent found, naming no command');
|
|
50
88
|
},
|
|
51
89
|
};
|
|
@@ -5,7 +5,7 @@ import path from 'node:path';
|
|
|
5
5
|
import { DEFAULTS, mergeConfig, validateConfig } from '../core/config/index.js';
|
|
6
6
|
import { isWin } from '../core/util/index.js';
|
|
7
7
|
import { SYSTEM } from '../shared/system.js';
|
|
8
|
-
import { agentCommand, agentShims, bootResetPaths,
|
|
8
|
+
import { agentCommand, agentShims, bootResetPaths, isCopyableDatabaseFile, isDatabaseFile, localStateEntries, shimText, } from '../guest/agents/agent-files.js';
|
|
9
9
|
import { appendMissing, expandVars, isPinnedNodeDir, parseRegPath, pathKey, planPath, rebuildPath, splitPath, } from '../guest/setup/path-plan.js';
|
|
10
10
|
import { withReloadEnvironment } from '../host/tools/index.js';
|
|
11
11
|
import { HarnessSchema } from '../core/schema/index.js';
|
|
@@ -59,17 +59,6 @@ export const agentPathsSuite = {
|
|
|
59
59
|
], "codex's DPAPI-sealed sandbox users and setup marker are removed at boot (CryptUnprotectData otherwise)");
|
|
60
60
|
t.check(HarnessSchema.safeParse({ ...DEFAULTS.harnesses.codex, bootReset: ['..\\login'] }).success === false &&
|
|
61
61
|
HarnessSchema.safeParse({ ...DEFAULTS.harnesses.codex, bootReset: ['C:\\Windows'] }).success === false, 'bootReset takes only plain paths below the agent state folder');
|
|
62
|
-
t.section('codex without its own command sandbox');
|
|
63
|
-
const userToml = '[tui]\nscreen_reader_detection_done = true\n\n[windows]\nsandbox = "elevated"\n';
|
|
64
|
-
const off = codexConfigWithoutSandbox(userToml) ?? '';
|
|
65
|
-
t.check(off.split('\n').find((l) => !l.startsWith('#')) === 'sandbox_mode = "danger-full-access"' &&
|
|
66
|
-
off.endsWith(userToml), 'sandbox_mode goes above the first table, the rest of the file is kept');
|
|
67
|
-
t.check(codexConfigWithoutSandbox('') === off.slice(0, off.indexOf('\n\n') + 1), 'a missing config.toml gets just the setting');
|
|
68
|
-
t.equal([
|
|
69
|
-
codexConfigWithoutSandbox(off),
|
|
70
|
-
codexConfigWithoutSandbox('model = "o3"\nsandbox_mode = "workspace-write"\n[windows]\n'),
|
|
71
|
-
], [null, null], 'a sandbox_mode already set (by the sandbox or by the user) is left alone');
|
|
72
|
-
t.check(codexConfigWithoutSandbox('[profiles.x]\nsandbox_mode = "read-only"\n') !== null, 'a sandbox_mode inside a table does not count as the top-level one');
|
|
73
62
|
t.section('PATH planner');
|
|
74
63
|
const present = new Set(['c:\\a', 'c:\\b', 'c:\\c\\d', 'c:\\nvm\\v24.1.0']);
|
|
75
64
|
const exists = (f) => present.has(pathKey(f));
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// Codex settings (launch flags, config.toml), the Claude environment editor and the one Anthropic key pattern.
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import { DEFAULTS } from '../core/config/index.js';
|
|
4
|
+
import { CodexSchema } from '../core/schema/index.js';
|
|
5
|
+
import { agentEnv, agentsEnvSet } from '../app/features/agents/index.js';
|
|
6
|
+
import { agentsOptions } from '../cli/menu/pages/agents-page.js';
|
|
7
|
+
import { fakeIO } from '../app/kernel/index.js';
|
|
8
|
+
import { runDefinition } from '../app/registry.js';
|
|
9
|
+
import { agentArguments, agentShims, codexArguments, codexConfigOwned, shimText } from '../guest/agents/agent-files.js';
|
|
10
|
+
import { ANTHROPIC_KEY_PATTERN, anthropicKeyProblem } from '../shared/accounts/index.js';
|
|
11
|
+
import { CLAUDE_ENV, ENV_GROUPS } from '../app/features/agents/claude-env.def.js';
|
|
12
|
+
const CODEX = DEFAULTS.codex;
|
|
13
|
+
export const agentSettingsSuite = {
|
|
14
|
+
name: 'agent-settings',
|
|
15
|
+
title: 'Codex settings, Claude environment editor, Anthropic key pattern',
|
|
16
|
+
run: async (t) => {
|
|
17
|
+
t.section('codex launch flags');
|
|
18
|
+
t.equal(codexArguments(CODEX), ['--dangerously-bypass-approvals-and-sandbox'], 'full access + never = the bypass flag');
|
|
19
|
+
t.equal(codexArguments({ ...CODEX, sandboxMode: 'workspace-write', approvalPolicy: 'on-request' }), ['--sandbox', 'workspace-write', '--ask-for-approval', 'on-request'], 'another mode gets explicit flags');
|
|
20
|
+
t.equal(codexArguments({ ...CODEX, sandboxMode: 'danger-full-access', approvalPolicy: 'on-failure' }), ['--sandbox', 'danger-full-access', '--ask-for-approval', 'on-failure'], 'the bypass flag is only for full access with no questions');
|
|
21
|
+
t.equal(codexArguments({ ...CODEX, model: 'gpt-5', reasoningEffort: 'high' }).slice(1), ['-c', 'model="gpt-5"', '-c', 'model_reasoning_effort="high"'], 'model and reasoning effort follow as -c overrides');
|
|
22
|
+
t.check(CodexSchema.safeParse({ ...CODEX, model: 'x"; evil' }).success === false, 'a model name cannot carry quotes');
|
|
23
|
+
const codex = DEFAULTS.harnesses.codex;
|
|
24
|
+
t.equal(agentArguments('codex', codex, CODEX), [...codex.arguments, '--dangerously-bypass-approvals-and-sandbox'], "codex's launch arguments carry the flags");
|
|
25
|
+
t.equal(agentArguments('claude', DEFAULTS.harnesses.claude, CODEX), [...DEFAULTS.harnesses.claude.arguments], 'other agents do not');
|
|
26
|
+
const shim = agentShims('C:\\AgentData\\tools', DEFAULTS.harnesses, { codex: codexArguments(CODEX) }).find((s) => s.name === 'codex');
|
|
27
|
+
const text = shim ? shimText(shim, { node: 'node.exe', script: 'agent.js' }) : '';
|
|
28
|
+
t.includes(text, '"%SBX_REAL%" --dangerously-bypass-approvals-and-sandbox %*', 'the codex.cmd shim adds the flags');
|
|
29
|
+
t.excludes(shimText(agentShims('C:\\AgentData\\tools', DEFAULTS.harnesses).find((s) => s.name === 'codex'), { node: 'node.exe', script: 'agent.js' }), 'dangerously', 'a shim without flags passes only %*');
|
|
30
|
+
t.section('codex config.toml');
|
|
31
|
+
const old = '# Set by the sandbox: x\nsandbox_mode = "danger-full-access"\n\n[tui]\nscreen_reader_detection_done = true\n\n[windows]\nsandbox = "elevated"\n';
|
|
32
|
+
const owned = codexConfigOwned(old, CODEX);
|
|
33
|
+
t.check(!owned.includes('sandbox_mode') && !owned.includes('Set by the sandbox') && !owned.includes('sandbox = '), 'the old sandbox_mode lines and [windows] sandbox are removed', owned);
|
|
34
|
+
t.check(owned.includes('mcp_oauth_credentials_store = "file"') &&
|
|
35
|
+
owned.includes('[tui]\nscreen_reader_detection_done = true\n') &&
|
|
36
|
+
owned.includes('[windows]'), 'MCP logins go to a file; every other line stays', owned);
|
|
37
|
+
t.equal(codexConfigOwned(owned, CODEX), owned, 'running it again changes nothing');
|
|
38
|
+
t.includes(codexConfigOwned(old, { ...CODEX, windowsSandbox: 'unelevated' }), '[windows]\nsandbox = "unelevated"', 'a Windows sandbox mode is written when asked for');
|
|
39
|
+
t.includes(codexConfigOwned('mcp_oauth_credentials_store = "keyring"\n', CODEX), 'mcp_oauth_credentials_store = "keyring"', "a user's own mcp_oauth_credentials_store is kept");
|
|
40
|
+
t.equal(codexConfigOwned('', { ...CODEX, mcpLoginsInFile: false }), '', 'an empty file stays empty when nothing is owned');
|
|
41
|
+
t.section('Anthropic key pattern');
|
|
42
|
+
const body = 'a'.repeat(60);
|
|
43
|
+
t.check(ANTHROPIC_KEY_PATTERN.test('sk-ant-usr01-' + body) && ANTHROPIC_KEY_PATTERN.test('sk-ant-api03-' + body), 'sk-ant-api03- and sk-ant-usr01- keys are accepted');
|
|
44
|
+
t.check(!ANTHROPIC_KEY_PATTERN.test('sk-ant-usr1-x') && anthropicKeyProblem('sk-ant-usr1-x') !== null, 'a short or malformed key is refused');
|
|
45
|
+
t.check(anthropicKeyProblem('sk-ant-usr01-' + body) === null, 'no problem for a user-workspace key');
|
|
46
|
+
t.section('Claude environment catalogue');
|
|
47
|
+
const names = CLAUDE_ENV.map((s) => s.name);
|
|
48
|
+
t.check(new Set(names).size === names.length, 'names are unique');
|
|
49
|
+
t.check(names.every((n) => /^[A-Z][A-Z0-9_]+$/.test(n)) && CLAUDE_ENV.every((s) => ENV_GROUPS.includes(s.group)), 'names and groups are valid');
|
|
50
|
+
t.check(names.includes('CLAUDE_CODE_MAX_CONCURRENT_SUBAGENTS'), 'the subagent limit is there');
|
|
51
|
+
t.section('Claude environment operations');
|
|
52
|
+
const configPath = t.at('agent-env', 'config.json');
|
|
53
|
+
t.write(configPath, '{}\n');
|
|
54
|
+
const ctx = {
|
|
55
|
+
packageRoot: t.dir('agent-env', 'package'),
|
|
56
|
+
options: {},
|
|
57
|
+
loaded: { config: structuredClone(DEFAULTS), configPath, base: t.temp },
|
|
58
|
+
storageRoot: t.dir('agent-env', 'storage'),
|
|
59
|
+
dataPath: t.dir('agent-env', 'storage', 'data', 'o'),
|
|
60
|
+
};
|
|
61
|
+
const io = fakeIO([]).io;
|
|
62
|
+
const set = (name, value) => runDefinition(agentsEnvSet.id, ctx, { agent: 'claude', name, value }, io);
|
|
63
|
+
const read = async () => (await runDefinition(agentEnv.id, ctx, { agent: 'claude' }, io));
|
|
64
|
+
await set('CLAUDE_CODE_MAX_CONCURRENT_SUBAGENTS', '40');
|
|
65
|
+
let row = (await read()).rows.find((r) => r.name === 'CLAUDE_CODE_MAX_CONCURRENT_SUBAGENTS');
|
|
66
|
+
t.check(row?.set === true && row.value === '40', 'a number is saved and read back');
|
|
67
|
+
t.includes(fs.readFileSync(configPath, 'utf8'), '"CLAUDE_CODE_MAX_CONCURRENT_SUBAGENTS": "40"', 'it is in the settings file');
|
|
68
|
+
await t.throws(() => set('CLAUDE_CODE_MAX_CONCURRENT_SUBAGENTS', 'many'), 'whole number', 'a word is refused for a number');
|
|
69
|
+
await t.throws(() => set('CLAUDE_CODE_MAX_CONCURRENT_SUBAGENTS', '0'), 'Use a number', 'zero is refused');
|
|
70
|
+
await set('CLAUDE_CODE_MAX_CONCURRENT_SUBAGENTS', null);
|
|
71
|
+
row = (await read()).rows.find((r) => r.name === 'CLAUDE_CODE_MAX_CONCURRENT_SUBAGENTS');
|
|
72
|
+
t.check(row?.set === false, 'an empty value removes it');
|
|
73
|
+
await t.throws(() => set('ANTHROPIC_API_KEY', 'sk-ant-usr1-x'), 'API key', 'the environment editor refuses a bad key');
|
|
74
|
+
const key = 'sk-ant-usr01-' + 'k'.repeat(60);
|
|
75
|
+
await set('ANTHROPIC_API_KEY', key);
|
|
76
|
+
const shown = JSON.stringify(await read());
|
|
77
|
+
t.check(!shown.includes(key.slice(13)), 'a secret value is never returned');
|
|
78
|
+
t.check((await read()).rows.find((r) => r.name === 'ANTHROPIC_API_KEY')?.set === true, 'but it says it is set');
|
|
79
|
+
const env = await read();
|
|
80
|
+
const menu = agentsOptions({
|
|
81
|
+
folder: 'C:/code',
|
|
82
|
+
defaultAgent: 'claude',
|
|
83
|
+
agents: [{ name: 'claude', ready: true }],
|
|
84
|
+
last: undefined,
|
|
85
|
+
attach: false,
|
|
86
|
+
window: 'config',
|
|
87
|
+
hostAgents: [],
|
|
88
|
+
claudeEnv: env,
|
|
89
|
+
});
|
|
90
|
+
t.check(menu.categories?.some((c) => c.id === 'env') === true &&
|
|
91
|
+
menu.items.some((i) => i.label === 'Subagents at once' || i.label === 'API key'), 'the menu has a Claude environment tab with the variables');
|
|
92
|
+
await t.throws(() => set('bad name', '1'), '', 'a bad variable name is refused');
|
|
93
|
+
},
|
|
94
|
+
};
|
|
@@ -282,6 +282,7 @@ export const appSuite = {
|
|
|
282
282
|
t.check(ops(sample).every((op) => known.has(op)), view.id + ': the page offers only operations its view declares');
|
|
283
283
|
}
|
|
284
284
|
t.section('agents');
|
|
285
|
+
const noCommands = { enabled: false, dir: 'C:/x', found: [] };
|
|
285
286
|
const agentsSample = agentsPage({
|
|
286
287
|
folder: 'C:\\code\\api',
|
|
287
288
|
defaultAgent: 'claude',
|
|
@@ -289,7 +290,7 @@ export const appSuite = {
|
|
|
289
290
|
{ name: 'codex', ready: false, isDefault: false, removable: true },
|
|
290
291
|
{ name: 'claude', ready: true, isDefault: true, removable: false },
|
|
291
292
|
],
|
|
292
|
-
});
|
|
293
|
+
}, noCommands);
|
|
293
294
|
const agentItems = agentsSample.sections[0].items ?? [];
|
|
294
295
|
const agentKeys = (id) => (agentItems.find((i) => i.id === id)?.actions ?? [])
|
|
295
296
|
.flatMap((a) => (a.kind === 'operation' && a.key ? [a.key] : []))
|
|
@@ -158,6 +158,10 @@ export const cliSuite = {
|
|
|
158
158
|
});
|
|
159
159
|
multi.handle(key('space'));
|
|
160
160
|
t.equal(multi.handle(key('return')), { done: { kind: 'multi', values: ['a', 'b'] } }, 'list screen: space ticks several');
|
|
161
|
+
const pasted = new TextScreen({ path: ['T'], label: 'Secret', masked: true });
|
|
162
|
+
pasted.handle(key('', '\u001b[200~abc\r\n\u001b[201~'));
|
|
163
|
+
pasted.handle(key('', '[200~d[201~'));
|
|
164
|
+
t.equal(pasted.handle(key('return')), { done: 'abcd' }, 'text field: the markers around pasted text are dropped');
|
|
161
165
|
const typed = new TextScreen({ path: ['T'], label: 'Port', validate: (v) => (Number(v) > 0 ? true : 'a number') });
|
|
162
166
|
typed.handle(key('x', 'x'));
|
|
163
167
|
const refused = typed.handle(key('return'));
|
package/dist/selftest/index.js
CHANGED
|
@@ -28,6 +28,7 @@ import { channelSuite } from './channel.test.js';
|
|
|
28
28
|
import { heavyLoadSuite } from './heavy-load.test.js';
|
|
29
29
|
import { cliSuite } from './cli.test.js';
|
|
30
30
|
import { agentPathsSuite } from './agent-paths.test.js';
|
|
31
|
+
import { agentSettingsSuite } from './agent-settings.test.js';
|
|
31
32
|
import { cmdlineSuite } from './cmdline.test.js';
|
|
32
33
|
import { configSuite } from './config.test.js';
|
|
33
34
|
import { migrateSuite } from './migrate.test.js';
|
|
@@ -140,6 +141,7 @@ export const SUITES = [
|
|
|
140
141
|
personalizationSuite,
|
|
141
142
|
cmdlineSuite,
|
|
142
143
|
agentPathsSuite,
|
|
144
|
+
agentSettingsSuite,
|
|
143
145
|
setupSuite,
|
|
144
146
|
dataSuite,
|
|
145
147
|
repairSuite,
|
|
@@ -243,8 +243,8 @@ export const menuSuite = {
|
|
|
243
243
|
agentList.categories?.map((c) => c.id),
|
|
244
244
|
], [
|
|
245
245
|
['repeat', 'agent:codex', 'agent:claude', 'add'],
|
|
246
|
-
['start', 'options'],
|
|
247
|
-
], 'Agents: Repeat last, the default agent first, the others, then Add an agent;
|
|
246
|
+
['start', 'options', 'host'],
|
|
247
|
+
], 'Agents: Repeat last, the default agent first, the others, then Add an agent; "On this PC" always holds the agent commands switch');
|
|
248
248
|
t.check(agentList.items
|
|
249
249
|
.filter((i) => i.category === 'options')
|
|
250
250
|
.map((i) => i.value)
|
|
@@ -580,7 +580,9 @@ export const menuSuite = {
|
|
|
580
580
|
keyFor('x', 'agent:mine') &&
|
|
581
581
|
!keyFor('x', 'agent:codex') &&
|
|
582
582
|
typeof actionList.actions === 'function' &&
|
|
583
|
-
actionList.actions('start').length ===
|
|
583
|
+
actionList.actions('start').length === 3 &&
|
|
584
|
+
keyFor('e', 'agent:claude') &&
|
|
585
|
+
!keyFor('e', 'agent:codex') &&
|
|
584
586
|
actionList.actions('options').length === 0, 'Make default shows on any other agent, Remove only on one you added; neither on built-in agents or other rows');
|
|
585
587
|
const agentsFile = ConfigFile.user(t.at('agents-actions', 'config.json'));
|
|
586
588
|
t.write(agentsFile.file, '{\n // mine\n "defaultHarness": "claude"\n}\n');
|
|
@@ -11,6 +11,7 @@ import { LocalOverridesSchema } from '../core/schema/index.js';
|
|
|
11
11
|
import { discoverPcInstalls } from '../host/pc-installs/index.js';
|
|
12
12
|
import { generateSandbox } from '../host/launch/index.js';
|
|
13
13
|
import { isValidGuestPath, PC_INSTALLS_GUEST } from '../shared/paths/index.js';
|
|
14
|
+
import { NOT_READY } from '../shared/readiness.js';
|
|
14
15
|
const PC = { localAppData: '', appData: '', programData: '' };
|
|
15
16
|
export const pcInstallsSuite = {
|
|
16
17
|
name: 'pc-installs',
|
|
@@ -70,6 +71,9 @@ export const pcInstallsSuite = {
|
|
|
70
71
|
t.check(session.pcInstalls?.items.map((i) => i.id).join() === '480,UE_5.4' &&
|
|
71
72
|
!JSON.stringify(session.pcInstalls).includes(game), 'session.json lists them by their place in the sandbox only');
|
|
72
73
|
t.section('detection of the new toolchains');
|
|
74
|
+
// These toolchains are hidden until written (NOT_READY); their detection rules are tested as if they were ready.
|
|
75
|
+
const hidden = [...NOT_READY.toolchains];
|
|
76
|
+
NOT_READY.toolchains.length = 0;
|
|
73
77
|
const tc = structuredClone(DEFAULTS.toolchains);
|
|
74
78
|
const decide = (entries, hints = {}, settingsOf = tc) => decideToolchains(settingsOf, entries, [], hints);
|
|
75
79
|
const godot = decide(['project.godot']);
|
|
@@ -92,6 +96,7 @@ export const pcInstallsSuite = {
|
|
|
92
96
|
t.check(decide(['project.godot'], {}, godotNet).dotnet.on, "Godot's .NET build brings .NET");
|
|
93
97
|
const fivem = decide(['fxmanifest.lua']);
|
|
94
98
|
t.check(!fivem.fivem.on && fivem.fivem.reason.includes('fivem.acceptLicense'), 'the FiveM server is licensed too');
|
|
99
|
+
NOT_READY.toolchains.push(...hidden);
|
|
95
100
|
t.section('a project never accepts a licence or shares a sign-in');
|
|
96
101
|
t.check(LICENSED_TOOLCHAINS.every((n) => OFF_IS_SAFER.includes('toolchains.' + n + '.acceptLicense')) &&
|
|
97
102
|
OFF_IS_SAFER.includes('accounts.epic.share') &&
|
|
@@ -6,6 +6,13 @@ import path from 'node:path';
|
|
|
6
6
|
import { DEFAULTS } from '../core/config/index.js';
|
|
7
7
|
import { assertLocalRoom, ensurePersisted, installRecords, InstallRecordsSchema, LOCAL_RESERVE_BYTES, recordUse, } from '../guest/toolchains/persisted.js';
|
|
8
8
|
import { BUILD_TOOLS_COMPONENTS, UNREAL_COMPONENTS, visualStudioWanted, } from '../guest/toolchains/native/vs-request.js';
|
|
9
|
+
import { autosdkLayout, layoutNextStep, unrealUsesAutosdk, vsExitText } from '../guest/toolchains/native/vs-request.js';
|
|
10
|
+
import { freeBytesAt } from '../shared/io/disk.js';
|
|
11
|
+
import { SIGNERS, signerMatches } from '../shared/signer.js';
|
|
12
|
+
import { decideToolchains } from '../core/detect/toolchains.js';
|
|
13
|
+
import { NOT_READY } from '../shared/readiness.js';
|
|
14
|
+
import { ConfigFile } from '../app/features/settings/settings-file.js';
|
|
15
|
+
import { settingsOf } from '../app/features/settings/settings-rows.js';
|
|
9
16
|
import { collectDataEntries } from '../cli/services/data-collect.js';
|
|
10
17
|
const GB = 1024 ** 3;
|
|
11
18
|
export const persistedInstallsSuite = {
|
|
@@ -103,5 +110,67 @@ export const persistedInstallsSuite = {
|
|
|
103
110
|
all.for.join() === 'unreal,flutter,unity' &&
|
|
104
111
|
UNREAL_COMPONENTS.every((c) => all.components.includes(c)) &&
|
|
105
112
|
all.components.includes('Microsoft.VisualStudio.Component.VC.Llvm.Clang'), 'with Unreal: one Community install serves all of them');
|
|
113
|
+
t.section('Unreal on the portable MSVC (autosdk)');
|
|
114
|
+
tc.unreal.visualStudio = 'autosdk';
|
|
115
|
+
const noUnreal = visualStudioWanted(tc);
|
|
116
|
+
t.check(noUnreal?.product === 'buildtools' && noUnreal.for.join() === 'flutter,unity', 'autosdk: Unreal asks for no Visual Studio (the others still get the Build Tools)');
|
|
117
|
+
tc.flutter.enabled = false;
|
|
118
|
+
tc.unity.enabled = false;
|
|
119
|
+
t.equal(visualStudioWanted(tc), null, 'autosdk alone: no Visual Studio at all');
|
|
120
|
+
t.check(unrealUsesAutosdk(tc, (s) => s === true) && !unrealUsesAutosdk(DEFAULTS.toolchains), 'autosdk is only when asked');
|
|
121
|
+
const pairs = autosdkLayout('T:\\msvc', '14.44.35207', '10.0.26100.0', 'T:\\autosdk');
|
|
122
|
+
t.check(pairs.length === 4 &&
|
|
123
|
+
pairs[0].to === 'T:\\autosdk\\HostWin64\\Win64\\VS2022\\14.44.35207' &&
|
|
124
|
+
pairs[0].from === 'T:\\msvc\\VC\\Tools\\MSVC\\14.44.35207' &&
|
|
125
|
+
pairs[1].from === 'T:\\msvc\\Windows Kits\\10\\Include\\10.0.26100.0' &&
|
|
126
|
+
pairs[1].to === 'T:\\autosdk\\HostWin64\\Win64\\Windows Kits\\10\\Include\\10.0.26100.0', 'the AutoSDK tree is MSVC under VS2022\\<version> and the SDK under Windows Kits\\10 (copies)');
|
|
127
|
+
const on = DEFAULTS.toolchains;
|
|
128
|
+
const auto = {
|
|
129
|
+
...structuredClone(on),
|
|
130
|
+
unreal: { ...on.unreal, visualStudio: 'autosdk', acceptLicense: true },
|
|
131
|
+
};
|
|
132
|
+
const decided = decideToolchains(auto, ['Game.uproject'], []);
|
|
133
|
+
t.check(decided.unreal.on && decided.msvc.on, 'an Unreal project with autosdk turns the portable MSVC on, under its licence');
|
|
134
|
+
t.section('the Visual Studio layout');
|
|
135
|
+
t.check(vsExitText(3762504530).includes('crashed') &&
|
|
136
|
+
vsExitText(-1).includes('exit 4294967295') &&
|
|
137
|
+
vsExitText(0) === 'installed' &&
|
|
138
|
+
vsExitText(1602) === 'cancelled' &&
|
|
139
|
+
vsExitText(8006).includes('running') &&
|
|
140
|
+
vsExitText(3010).includes('restart') &&
|
|
141
|
+
vsExitText(5007).includes('refused'), 'installer exit codes are told in words (0xE0434352 is a crash)');
|
|
142
|
+
const reserve = LOCAL_RESERVE_BYTES;
|
|
143
|
+
const room = (free) => freeBytesAt(t.dir('layout-room'), () => ({ bavail: free, bsize: 1 })) ?? 0;
|
|
144
|
+
t.check(layoutNextStep(0, 0, reserve) === 'done' &&
|
|
145
|
+
layoutNextStep(3762504530, room(40 * GB), reserve) === 'stage' &&
|
|
146
|
+
layoutNextStep(3762504530, room(20 * GB), reserve) === 'skip' &&
|
|
147
|
+
layoutNextStep(null, room(40 * GB), reserve) === 'stage', 'a failed layout is built on the sandbox disk only with room for it (12 GB plus the reserve)');
|
|
148
|
+
t.equal(freeBytesAt(path.join(t.dir('layout-room'), 'not', 'there'), () => ({ bavail: 3, bsize: 2 })), 6, 'free room of a folder that does not exist asks its parent');
|
|
149
|
+
t.equal(freeBytesAt(t.dir('layout-room'), () => {
|
|
150
|
+
throw new Error('no');
|
|
151
|
+
}), null, 'unknown free room is null');
|
|
152
|
+
t.section('signers');
|
|
153
|
+
const ms = { subject: SIGNERS.microsoft.subject, issuer: undefined };
|
|
154
|
+
const msSig = {
|
|
155
|
+
subject: 'CN=Microsoft Corporation, O=Microsoft Corporation, L=Redmond, S=Washington, C=US',
|
|
156
|
+
issuer: 'CN=Microsoft Code Signing PCA',
|
|
157
|
+
};
|
|
158
|
+
t.check(signerMatches(msSig, ms) &&
|
|
159
|
+
!signerMatches({ ...msSig, subject: 'CN=Microsoft Corporation Fake, O=Evil' }, ms) &&
|
|
160
|
+
!signerMatches(null, ms) &&
|
|
161
|
+
signerMatches({ subject: 'CN="Valve Corp.", O=Valve', issuer: 'x' }, SIGNERS.valve) &&
|
|
162
|
+
signerMatches({ subject: 'CN=Unity Technologies ApS', issuer: 'x' }, SIGNERS.unity) &&
|
|
163
|
+
!signerMatches({ subject: 'CN=Microsoft Corporation', issuer: 'x' }, SIGNERS.valve) &&
|
|
164
|
+
signerMatches(msSig, { subject: ['^CN=Microsoft'], issuer: ['code signing'] }) &&
|
|
165
|
+
!signerMatches(msSig, { subject: ['^CN=Microsoft'], issuer: ['nobody'] }), 'a signer matches by subject (and issuer when named), never an empty or unsigned file');
|
|
166
|
+
t.section('not ready yet');
|
|
167
|
+
const everything = structuredClone(on);
|
|
168
|
+
for (const name of NOT_READY.toolchains)
|
|
169
|
+
everything[name].enabled = 'auto';
|
|
170
|
+
const found = decideToolchains(everything, ['ProjectVersion.txt', 'project.godot', 'fxmanifest.lua'], ['unity']);
|
|
171
|
+
t.check(NOT_READY.toolchains.every((n) => !found[n].on), '"auto" never turns on a toolchain that is not written yet, whatever the project holds');
|
|
172
|
+
const shown = settingsOf(ConfigFile.user(t.at('settings', 'config.json'))).map((r) => r.key.join('.'));
|
|
173
|
+
t.check(!shown.some((k) => /^toolchains\.(unity|godot|android|flutter|dart|reactNative|fivem)\./.test(k)) &&
|
|
174
|
+
shown.includes('toolchains.unreal.visualStudio'), 'settings do not list the toolchains that are not written yet');
|
|
106
175
|
},
|
|
107
176
|
};
|
|
@@ -306,7 +306,7 @@ export const serviceAccountsSuite = {
|
|
|
306
306
|
});
|
|
307
307
|
t.check(empty.message.includes('No saved'), 'no launcher sign-in on this PC: it says so');
|
|
308
308
|
await t.throws(() => loginServiceFlow(fakeIO([]).io, 'epic', { store: flowStore, share: true }), 'nothing to type', 'Epic has nothing to type');
|
|
309
|
-
await t.throws(() => loginServiceFlow(fakeIO([CFX, '']).io, 'cfx', { store: flowStore, share: true }), '
|
|
309
|
+
await t.throws(() => loginServiceFlow(fakeIO([CFX, '']).io, 'cfx', { store: flowStore, share: true }), 'comes in a later version', 'a kind whose host side is not written yet says so');
|
|
310
310
|
t.check(Boolean(HOST_SOURCES.epic.fromPc) && !HOST_SOURCES.epic.fromAnswers, 'Epic comes from this PC (or a sandbox), never typed');
|
|
311
311
|
const gone = await logoutServiceFlow(fakeIO([true]).io, 'epic', t.dir('svc-flow-root'), flowStore, new AccountLeases(t.dir('svc-flow-root')));
|
|
312
312
|
t.check(gone.deleted && flowStore.state('epic') === 'none', 'forgetting deletes it after asking');
|
|
@@ -67,7 +67,7 @@ export const LAUNCH_FILES_GOLDEN = [
|
|
|
67
67
|
'sandbox/scripts/enable-vt-input.ps1',
|
|
68
68
|
'sandbox/scripts/expand-archive.ps1',
|
|
69
69
|
'sandbox/scripts/get-gpu-names.ps1',
|
|
70
|
-
'sandbox/scripts/get-
|
|
70
|
+
'sandbox/scripts/get-signature.ps1',
|
|
71
71
|
'sandbox/scripts/install-shell-path.ps1',
|
|
72
72
|
'sandbox/scripts/link-profile-dirs.ps1',
|
|
73
73
|
'sandbox/scripts/new-shortcuts.ps1',
|
|
@@ -194,6 +194,7 @@ export declare function createFixtures(t: Harness): {
|
|
|
194
194
|
acceptLicense: boolean;
|
|
195
195
|
components: string[];
|
|
196
196
|
keepInstallFiles: boolean;
|
|
197
|
+
visualStudio: "full" | "autosdk";
|
|
197
198
|
persistEngines: boolean;
|
|
198
199
|
derivedDataCache: "persisted" | "local";
|
|
199
200
|
};
|
|
@@ -429,6 +430,14 @@ export declare function createFixtures(t: Harness): {
|
|
|
429
430
|
resumeArguments?: string[] | undefined;
|
|
430
431
|
resumeOneArguments?: string[] | undefined;
|
|
431
432
|
}>;
|
|
433
|
+
codex: {
|
|
434
|
+
sandboxMode: "danger-full-access" | "workspace-write" | "read-only";
|
|
435
|
+
approvalPolicy: "never" | "on-request" | "on-failure" | "untrusted";
|
|
436
|
+
model: string;
|
|
437
|
+
reasoningEffort: "default" | "minimal" | "low" | "medium" | "high" | "xhigh";
|
|
438
|
+
windowsSandbox: "off" | "unelevated" | "elevated";
|
|
439
|
+
mcpLoginsInFile: boolean;
|
|
440
|
+
};
|
|
432
441
|
hooks: {
|
|
433
442
|
failFast: boolean;
|
|
434
443
|
};
|
|
@@ -14,7 +14,8 @@ import { ConfigSchema, LocalOverridesSchema } from '../core/schema/index.js';
|
|
|
14
14
|
import { SecretStore } from '../host/secrets/index.js';
|
|
15
15
|
import { clearSecrets } from '../host/secrets/index.js';
|
|
16
16
|
import { generateSandbox } from '../host/launch/index.js';
|
|
17
|
-
import {
|
|
17
|
+
import { classifyFailure, FAILURE_TEXT } from '../shared/vpn/failure.js';
|
|
18
|
+
import { findCountry, loginProblem, loginWarning, nordProfile, prepareVpn, wireguardFileProblem, recommendedServer, VpnLogin, vpnLine, vpnNotes, } from '../host/vpn/index.js';
|
|
18
19
|
import { RunDir } from '../shared/channel/index.js';
|
|
19
20
|
import { OPENVPN_MSI, WIREGUARD_MSI } from '../shared/vpn/download.js';
|
|
20
21
|
import { cleanServers, ipv4Except, killSwitchCommands, removeKillSwitchCommands, tunnelAddressFromLog, tunnelCommands, } from '../shared/vpn/kill-switch.js';
|
|
@@ -221,6 +222,52 @@ export const vpnSuite = {
|
|
|
221
222
|
t.check(login.state('nordvpn') === 'unreadable', 'a login this Windows user cannot open is reported as unreadable');
|
|
222
223
|
login.remove('nordvpn');
|
|
223
224
|
t.check(login.state('nordvpn') === 'none', 'logout deletes it');
|
|
225
|
+
t.section('NordVPN login checks and why a tunnel failed');
|
|
226
|
+
const sampleStatus = () => ({
|
|
227
|
+
enabled: true,
|
|
228
|
+
provider: 'nordvpn',
|
|
229
|
+
protocol: 'udp',
|
|
230
|
+
country: '',
|
|
231
|
+
killSwitch: true,
|
|
232
|
+
login: 'saved',
|
|
233
|
+
loginSavedAt: '2026-11-02T19:00:00.000Z',
|
|
234
|
+
running: null,
|
|
235
|
+
lastFailure: null,
|
|
236
|
+
});
|
|
237
|
+
t.check((loginProblem({ username: 'me@example.com', password: 'x' }, 'nordvpn') ?? '').includes('service credentials') &&
|
|
238
|
+
loginProblem({ username: 'me@example.com', password: 'x' }, 'openvpn-file') === null &&
|
|
239
|
+
(loginProblem({ username: 'abc', password: 'p[200~w' }, 'nordvpn') ?? '').includes('paste'), 'an e-mail is refused for NordVPN only (it needs the service credentials); hidden paste markers are refused');
|
|
240
|
+
login.save('nordvpn', { username: ' ' + USERNAME + ' ', password: ' ' + PASSWORD + ' ' });
|
|
241
|
+
t.equal(login.read('nordvpn'), { username: USERNAME, password: PASSWORD }, 'spaces around the user name and the password are not saved (a pasted login often has them)');
|
|
242
|
+
t.check(loginWarning({ username: 'x', password: 'y' }, 'openvpn-file') === null &&
|
|
243
|
+
loginWarning({ username: 'a'.repeat(24), password: 'B'.repeat(24) }, 'nordvpn') === null &&
|
|
244
|
+
loginWarning({ username: 'a b', password: 'c' }, 'nordvpn') !== null, 'a NordVPN login that does not look like service credentials gets a hint, not a refusal');
|
|
245
|
+
login.remove('nordvpn');
|
|
246
|
+
const failed = (log) => classifyFailure(log)?.code;
|
|
247
|
+
t.equal([
|
|
248
|
+
failed('AUTH: Received control message: AUTH_FAILED'),
|
|
249
|
+
failed('TLS Error: TLS key negotiation failed to occur within 60 seconds'),
|
|
250
|
+
failed('Could not create Wintun interface'),
|
|
251
|
+
failed('Exiting due to fatal error'),
|
|
252
|
+
failed('all fine'),
|
|
253
|
+
], ['auth', 'unreachable', 'driver', 'other', undefined], 'the OpenVPN log is turned into a stable code the host acts on');
|
|
254
|
+
const refusedPage = vpnPage({
|
|
255
|
+
...sampleStatus(),
|
|
256
|
+
lastFailure: { code: 'auth', text: FAILURE_TEXT.auth, at: '2026-11-02T19:15:21.000Z' },
|
|
257
|
+
});
|
|
258
|
+
const refusedText = pageLines(refusedPage).join('\n');
|
|
259
|
+
const refusedOps = pageActions(refusedPage).flatMap((a) => (a.kind === 'operation' ? [[a.op, a.label]] : []));
|
|
260
|
+
t.check(refusedText.includes('Last attempt') &&
|
|
261
|
+
refusedText.includes('login refused') &&
|
|
262
|
+
refusedText.includes('service credentials') &&
|
|
263
|
+
refusedOps.some(([op, label]) => op === 'vpn.login' && label === 'Save the login again…') &&
|
|
264
|
+
!/sandbox (vpn|config|agent)/.test(refusedText), 'after a refused login the VPN page says so and offers to save the login again, without naming a command');
|
|
265
|
+
const unreachablePage = vpnPage({
|
|
266
|
+
...sampleStatus(),
|
|
267
|
+
lastFailure: { code: 'unreachable', text: FAILURE_TEXT.unreachable, at: '2026-11-02T19:15:21.000Z' },
|
|
268
|
+
});
|
|
269
|
+
t.check(pageActions(unreachablePage).some((a) => a.kind === 'operation' && a.input.key === 'vpn.protocol') &&
|
|
270
|
+
!pageActions(vpnPage({ ...sampleStatus(), protocol: 'tcp', lastFailure: { code: 'unreachable', text: 'x', at: 'y' } })).some((a) => a.kind === 'operation' && a.input.key === 'vpn.protocol'), 'a server that could not be reached offers TCP instead of UDP (once)');
|
|
224
271
|
t.section('settings: what a project may and may not do');
|
|
225
272
|
t.check(DEFAULTS.vpn.enabled === false && DEFAULTS.vpn.provider === 'nordvpn' && DEFAULTS.vpn.killSwitch === true, 'the VPN is off by default, NordVPN, with the kill switch on');
|
|
226
273
|
t.check(ConfigSchema.safeParse(DEFAULTS).success, 'the defaults are valid');
|
|
@@ -458,11 +505,12 @@ export const vpnSuite = {
|
|
|
458
505
|
login: 'none',
|
|
459
506
|
loginSavedAt: null,
|
|
460
507
|
running: null,
|
|
508
|
+
lastFailure: null,
|
|
461
509
|
};
|
|
462
510
|
const lines = pageLines(vpnPage(sample)).join('\n');
|
|
463
|
-
t.check(
|
|
511
|
+
t.check(!/sandbox (vpn|config|agent)/.test(lines) &&
|
|
464
512
|
VPN_WARNINGS.every((w) => lines.includes(w)) &&
|
|
465
|
-
!lines.includes('Running now'), 'vpn status page: the facts
|
|
513
|
+
!lines.includes('Running now'), 'vpn status page: the facts and the warnings, no command to type', lines);
|
|
466
514
|
t.equal(pageActions(vpnPage({ ...sample, provider: 'wireguard-file' })).map((a) => a.kind === 'operation' ? a.op : a.kind), ['settings.set'], 'vpn: wireguard needs no login and offers only turning the VPN on or off');
|
|
467
515
|
t.equal(pageActions(vpnPage({ ...sample, login: 'saved', loginSavedAt: '2026-10-14T09:30:00.000Z' })).map((a) => a.kind === 'operation' ? a.op : a.kind), ['settings.set', 'vpn.login', 'vpn.logout'], 'a saved login offers turning the VPN on or off, saving the login again and deleting it');
|
|
468
516
|
t.equal(vpnLoginRowOf(sample).hint, 'not saved', 'the Settings row says not saved when no login is saved');
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export declare const ANTHROPIC_KEY_PATTERN: RegExp;
|
|
2
|
+
/** The only part of a key ever shown: sk-ant-...abcd. */
|
|
3
|
+
export declare const ANTHROPIC_KEY_HINT: RegExp;
|
|
4
|
+
/** What is wrong with a typed or found API key, or null when it has the right shape. Never quotes the key. */
|
|
5
|
+
export declare function anthropicKeyProblem(key: string): string | null;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// The one shape of an Anthropic API key, for the host and the web (no node imports). A key from the Claude Console
|
|
2
|
+
// starts with sk-ant-api, a key from a user workspace with sk-ant-usr.
|
|
3
|
+
export const ANTHROPIC_KEY_PATTERN = /^sk-ant-(api|usr)\d{2}-[A-Za-z0-9_-]{20,400}$/;
|
|
4
|
+
/** The only part of a key ever shown: sk-ant-...abcd. */
|
|
5
|
+
export const ANTHROPIC_KEY_HINT = /^sk-ant-\.\.\.[A-Za-z0-9_-]{4}$/;
|
|
6
|
+
/** What is wrong with a typed or found API key, or null when it has the right shape. Never quotes the key. */
|
|
7
|
+
export function anthropicKeyProblem(key) {
|
|
8
|
+
if (ANTHROPIC_KEY_PATTERN.test(key))
|
|
9
|
+
return null;
|
|
10
|
+
if (key.startsWith('sk-ant-oat'))
|
|
11
|
+
return 'That is a Claude subscription token (from claude setup-token), not an API key. Save the subscription login instead.';
|
|
12
|
+
if (key.startsWith('sk-ant-admin'))
|
|
13
|
+
return 'That is an Admin key: it cannot run Claude. Make an API key instead.';
|
|
14
|
+
return 'An Anthropic API key starts with sk-ant-api or sk-ant-usr and has only letters, digits, - and _ (copy it from the Claude Console).';
|
|
15
|
+
}
|