windows-development-sandbox 1.11.7 → 1.13.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 +13 -6
- package/config.example.json +15 -4
- package/dist/app/features/access/access.def.d.ts +190 -0
- package/dist/app/features/access/access.def.js +273 -0
- package/dist/app/features/access/access.run.d.ts +79 -0
- package/dist/app/features/access/access.run.js +129 -0
- package/dist/app/features/access/index.d.ts +2 -0
- package/dist/app/features/access/index.js +3 -0
- package/dist/app/features/agents/agent-entry.js +5 -0
- package/dist/app/features/agents/agents.def.js +1 -0
- package/dist/app/features/apps/apps-rows.d.ts +4 -1
- package/dist/app/features/apps/apps-rows.js +7 -2
- package/dist/app/features/apps/apps.def.d.ts +19 -0
- package/dist/app/features/apps/apps.def.js +50 -12
- package/dist/app/features/apps/apps.run.d.ts +2 -0
- package/dist/app/features/apps/apps.run.js +1 -1
- package/dist/app/features/audit/audit.def.d.ts +350 -0
- package/dist/app/features/audit/audit.def.js +457 -0
- package/dist/app/features/audit/audit.run.d.ts +110 -0
- package/dist/app/features/audit/audit.run.js +133 -0
- package/dist/app/features/audit/index.d.ts +2 -0
- package/dist/app/features/audit/index.js +3 -0
- package/dist/app/features/defs.d.ts +349 -4
- package/dist/app/features/defs.js +16 -1
- package/dist/app/features/files/files.def.js +1 -0
- package/dist/app/features/health/health.def.js +1 -0
- package/dist/app/features/sandbox/sandbox.def.js +3 -0
- package/dist/app/features/sessions/sessions.def.d.ts +36 -0
- package/dist/app/features/sessions/sessions.def.js +77 -9
- package/dist/app/features/sessions/sessions.run.d.ts +25 -2
- package/dist/app/features/sessions/sessions.run.js +71 -3
- package/dist/app/features/settings/settings.def.d.ts +8 -0
- package/dist/app/features/settings/settings.def.js +17 -0
- package/dist/app/features/settings/settings.run.d.ts +3 -0
- package/dist/app/features/settings/settings.run.js +16 -2
- package/dist/app/features/web/web.def.d.ts +9 -0
- package/dist/app/features/web/web.def.js +26 -1
- package/dist/app/features/web/web.run.d.ts +3 -0
- package/dist/app/features/web/web.run.js +11 -3
- package/dist/app/kernel/access.d.ts +33 -0
- package/dist/app/kernel/access.js +44 -0
- package/dist/app/kernel/define.d.ts +26 -2
- package/dist/app/kernel/index.d.ts +1 -0
- package/dist/app/kernel/index.js +1 -0
- package/dist/app/kernel/rpc.d.ts +5 -2
- package/dist/app/registry.d.ts +4 -1
- package/dist/app/registry.js +26 -4
- package/dist/cli/args.d.ts +0 -3
- package/dist/cli/args.js +3 -4
- package/dist/cli/commands/index.d.ts +1 -1
- package/dist/cli/commands/index.js +1 -1
- package/dist/cli/commands/launch/prepare.js +1 -1
- package/dist/cli/commands/maintenance/index.d.ts +0 -1
- package/dist/cli/commands/maintenance/index.js +0 -1
- package/dist/cli/commands/registry.js +3 -2
- package/dist/cli/commands/remote.d.ts +2 -0
- package/dist/cli/commands/remote.js +49 -0
- package/dist/cli/commands/sessions/reopen.js +3 -0
- package/dist/cli/commands/web.js +66 -0
- package/dist/cli/manifest.d.ts +63 -45
- package/dist/cli/manifest.js +33 -18
- package/dist/cli/menu/io-menu.d.ts +5 -3
- package/dist/cli/menu/io-menu.js +12 -5
- package/dist/cli/menu/pages/apps-page.js +2 -15
- package/dist/cli/menu/pages/maintenance-page.d.ts +4 -0
- package/dist/cli/menu/pages/maintenance-page.js +51 -8
- package/dist/cli/menu/remote-menu.d.ts +19 -0
- package/dist/cli/menu/remote-menu.js +108 -0
- package/dist/cli/menu/views.js +4 -0
- package/dist/cli/operations/handler.js +15 -1
- package/dist/cli/services/remote-client.d.ts +16 -0
- package/dist/cli/services/remote-client.js +141 -0
- package/dist/cli/services/sessions-service.d.ts +5 -0
- package/dist/cli/services/sessions-service.js +17 -1
- package/dist/core/config/defaults.js +12 -2
- package/dist/core/local-config/file.js +16 -2
- package/dist/core/local-config/overrides.js +7 -0
- package/dist/core/run/versions.js +9 -8
- package/dist/core/schema/apps.d.ts +9 -0
- package/dist/core/schema/apps.js +9 -0
- package/dist/core/schema/config.d.ts +23 -3
- package/dist/core/schema/config.js +42 -5
- package/dist/core/schema/sandbox.d.ts +1 -0
- package/dist/core/schema/sandbox.js +11 -3
- package/dist/core/schema/web.d.ts +51 -0
- package/dist/core/schema/web.js +46 -1
- package/dist/core/util/fs.d.ts +5 -0
- package/dist/core/util/fs.js +23 -0
- package/dist/core/util/index.d.ts +1 -1
- package/dist/core/util/index.js +1 -1
- package/dist/guest/apps/app-installer.js +50 -4
- package/dist/guest/apps/index.d.ts +1 -1
- package/dist/guest/apps/index.js +1 -1
- package/dist/guest/apps/sources.js +1 -1
- package/dist/guest/audit/parse.d.ts +32 -2
- package/dist/guest/audit/parse.js +43 -5
- package/dist/guest/audit/process-audit.d.ts +3 -1
- package/dist/guest/audit/process-audit.js +9 -6
- package/dist/guest/boot/boot.js +12 -3
- package/dist/guest/boot/guide.d.ts +1 -1
- package/dist/guest/boot/guide.js +27 -3
- package/dist/guest/daemon/daemon.js +2 -0
- package/dist/guest/daemon/idle-shutdown.js +2 -1
- package/dist/guest/daemon/sessions.d.ts +23 -2
- package/dist/guest/daemon/sessions.js +151 -20
- package/dist/guest/entries/daemon.js +2 -1
- package/dist/guest/git/decision.js +7 -0
- package/dist/guest/sessions/cmdline.d.ts +14 -0
- package/dist/guest/sessions/cmdline.js +10 -0
- package/dist/guest/setup/addon-mcp.d.ts +2 -0
- package/dist/guest/setup/addon-mcp.js +49 -0
- package/dist/guest/setup/agent-addons.d.ts +9 -0
- package/dist/guest/setup/agent-addons.js +22 -0
- package/dist/guest/setup/headroom.d.ts +7 -0
- package/dist/guest/setup/headroom.js +60 -0
- package/dist/guest/setup/managed-policy.d.ts +2 -1
- package/dist/guest/setup/managed-policy.js +15 -6
- package/dist/guest/toolchains/plan.js +8 -3
- package/dist/guest/toolchains/setup.js +3 -0
- package/dist/host/audit/attribute.d.ts +46 -0
- package/dist/host/audit/attribute.js +76 -0
- package/dist/host/audit/collect.d.ts +29 -3
- package/dist/host/audit/collect.js +256 -61
- package/dist/host/audit/git-commits.d.ts +29 -0
- package/dist/host/audit/git-commits.js +157 -0
- package/dist/host/audit/index.d.ts +5 -0
- package/dist/host/audit/index.js +6 -0
- package/dist/host/audit/log.d.ts +45 -7
- package/dist/host/audit/log.js +148 -45
- package/dist/host/audit/redact.d.ts +2 -0
- package/dist/host/audit/redact.js +67 -0
- package/dist/host/channel/channel.d.ts +5 -1
- package/dist/host/channel/channel.js +12 -0
- package/dist/host/guard/agent-files.js +7 -1
- package/dist/host/guard/commands.js +13 -1
- package/dist/host/guard/git-config.js +27 -7
- package/dist/host/guard/git-dirs.js +78 -3
- package/dist/host/guard/mark-of-the-web.js +3 -1
- package/dist/host/guard/nested-repos.js +7 -2
- package/dist/host/guard/store.js +1 -0
- package/dist/host/guard/types.d.ts +2 -0
- package/dist/host/launch/mappings.js +11 -1
- package/dist/host/launch/project-scan.js +2 -21
- package/dist/host/launch/stage.js +4 -0
- package/dist/host/life/session-ledger.js +2 -1
- package/dist/host/remote/access-store.d.ts +156 -0
- package/dist/host/remote/access-store.js +308 -0
- package/dist/host/remote/cert.d.ts +20 -0
- package/dist/host/remote/cert.js +103 -0
- package/dist/host/remote/door-client.d.ts +13 -0
- package/dist/host/remote/door-client.js +54 -0
- package/dist/host/remote/firewall.d.ts +44 -0
- package/dist/host/remote/firewall.js +118 -0
- package/dist/host/remote/gate.d.ts +125 -0
- package/dist/host/remote/gate.js +391 -0
- package/dist/host/remote/index.d.ts +8 -0
- package/dist/host/remote/index.js +10 -0
- package/dist/host/remote/lan.d.ts +24 -0
- package/dist/host/remote/lan.js +88 -0
- package/dist/host/remote/tls-store.d.ts +22 -0
- package/dist/host/remote/tls-store.js +63 -0
- package/dist/host/remote/windows-helper.d.ts +67 -0
- package/dist/host/remote/windows-helper.js +129 -0
- package/dist/host/web/bridge.d.ts +5 -1
- package/dist/host/web/bridge.js +6 -2
- package/dist/host/web/browser.js +1 -1
- package/dist/host/web/control.d.ts +4 -1
- package/dist/host/web/control.js +28 -4
- package/dist/host/web/rpc.d.ts +16 -2
- package/dist/host/web/rpc.js +37 -4
- package/dist/host/web/runner.d.ts +4 -0
- package/dist/host/web/runner.js +12 -0
- package/dist/host/web/server.d.ts +18 -2
- package/dist/host/web/server.js +97 -24
- package/dist/host/web/state.d.ts +5 -0
- package/dist/host/web/state.js +8 -0
- package/dist/runtime/agent.js +184 -18
- package/dist/runtime/daemon.js +772 -338
- package/dist/runtime/db.js +184 -18
- package/dist/runtime/gitguard.js +6 -0
- package/dist/runtime/main.js +966 -553
- package/dist/runtime/netproxy.js +184 -18
- package/dist/runtime/view.js +186 -18
- package/dist/selftest/app.test.js +66 -7
- package/dist/selftest/audit.test.js +354 -47
- package/dist/selftest/channel.test.js +16 -0
- package/dist/selftest/cmdline.test.js +16 -0
- package/dist/selftest/git-access.test.js +8 -0
- package/dist/selftest/hcs.test.js +35 -0
- package/dist/selftest/hostile-input.test.js +12 -1
- package/dist/selftest/index.js +2 -0
- package/dist/selftest/live-daemon.test.js +25 -0
- package/dist/selftest/local-config.test.js +30 -0
- package/dist/selftest/menu-screens.js +1 -1
- package/dist/selftest/menu.test.js +3 -3
- package/dist/selftest/remote.test.d.ts +2 -0
- package/dist/selftest/remote.test.js +536 -0
- package/dist/selftest/repo-guard.test.js +56 -2
- package/dist/selftest/resume.test.js +2 -1
- package/dist/selftest/sharing.test.js +9 -2
- package/dist/selftest/support/engine-golden.js +9 -0
- package/dist/selftest/support/fixtures.d.ts +14 -0
- package/dist/selftest/support/web-preview.js +157 -0
- package/dist/selftest/toolchains.test.js +147 -2
- package/dist/selftest/web.test.js +2 -0
- package/dist/shared/apps/agent-addons.d.ts +64 -0
- package/dist/shared/apps/agent-addons.js +95 -0
- package/dist/shared/apps/catalogue.d.ts +33 -2
- package/dist/shared/apps/catalogue.js +102 -11
- package/dist/shared/apps/index.d.ts +3 -2
- package/dist/shared/apps/index.js +3 -2
- package/dist/shared/apps/selection.d.ts +2 -0
- package/dist/shared/apps/selection.js +7 -0
- package/dist/shared/channel/schema.d.ts +23 -0
- package/dist/shared/channel/schema.js +8 -0
- package/dist/shared/pty/protocol.d.ts +2 -0
- package/dist/shared/pty/protocol.js +2 -0
- package/dist/sync/watcher.js +9 -1
- package/dist/web/index.html +2 -2
- package/dist/web/{main-RQFN4WBY.css → main-UAQPI5XD.css} +1 -1
- package/dist/web/main-Z2WYAE2Z.js +221 -0
- package/native/csharp/HcsHost/AssemblyInfo.cs +5 -3
- package/native/csharp/HcsHost/Hcs.cs +9 -0
- package/native/csharp/HcsHost/Program.cs +26 -1
- package/native/csharp/HcsHost/Share.cs +173 -0
- package/native/csharp/HostTools/AssemblyInfo.cs +3 -3
- package/native/csharp/HostTools/Program.cs +17 -1
- package/native/csharp/HostTools/WebHelper.cs +499 -0
- package/native/scripts/get-network-profile.ps1 +19 -0
- package/native/scripts/set-folder-access.ps1 +11 -0
- package/native/scripts/set-web-firewall.ps1 +103 -0
- package/package.json +1 -1
- package/sandbox/addons/ccusage/package-lock.json +111 -0
- package/sandbox/addons/ccusage/package.json +7 -0
- package/sandbox/addons/context7/package-lock.json +1313 -0
- package/sandbox/addons/context7/package.json +7 -0
- package/sandbox/addons/headroom/requirements.in +1 -0
- package/sandbox/addons/headroom/requirements.txt +2922 -0
- package/sandbox/addons/playwright-mcp/package-lock.json +56 -0
- package/sandbox/addons/playwright-mcp/package.json +7 -0
- package/sandbox/csharp/PtyHost.cs +20 -0
- package/schemas/config.schema.json +78 -4
- package/schemas/sandbox-config.schema.json +2 -20
- package/dist/cli/commands/maintenance/audit.d.ts +0 -3
- package/dist/cli/commands/maintenance/audit.js +0 -58
- package/dist/web/main-K2JMCAZI.js +0 -219
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
// `session` and `
|
|
2
|
-
// folder's project (the boot environment) or in a project registered later, which is
|
|
3
|
-
// (daemon/project).
|
|
1
|
+
// `session`, `restart` and `close` requests: agent sessions and plain shell sessions, each in its own ptyhost process.
|
|
2
|
+
// A session runs in the launch folder's project (the boot environment) or in a project registered later, which is
|
|
3
|
+
// provisioned first (daemon/project). A shell gets the same environment as an agent, without the agent's own
|
|
4
|
+
// environment (its keys) and without the agent's install, hooks and conversation tracking.
|
|
5
|
+
import fs from 'node:fs';
|
|
4
6
|
import path from 'node:path';
|
|
5
7
|
import crypto from 'node:crypto';
|
|
6
8
|
import os from 'node:os';
|
|
@@ -10,7 +12,8 @@ import { isWithin } from '../../shared/paths/projects.js';
|
|
|
10
12
|
import { now } from '../../shared/channel/run-dir.js';
|
|
11
13
|
import { SYSTEM } from '../../shared/system.js';
|
|
12
14
|
import { buildJobs, cargoJobs, linkSlotsFor, usableCpus } from '../../shared/memory/index.js';
|
|
13
|
-
import { commandLine } from '../sessions/cmdline.js';
|
|
15
|
+
import { commandLine, shellLaunch } from '../sessions/cmdline.js';
|
|
16
|
+
import { pwshDir } from '../toolchains/tools/pwsh.js';
|
|
14
17
|
import { freePort } from '../runtime/ports.js';
|
|
15
18
|
import { primaryProject } from '../boot/project-status.js';
|
|
16
19
|
import { ensurePtyHost, ptyHostLogTail, startPtyHost, waitForPtyHost } from '../sessions/pty-host.js';
|
|
@@ -30,7 +33,11 @@ import { isConversationId } from '../../shared/paths/conversation.js';
|
|
|
30
33
|
import { errMsg } from '../../core/util/errors.js';
|
|
31
34
|
import { provisioner } from './project-provisioner.js';
|
|
32
35
|
import { IdleShutdown } from './idle-shutdown.js';
|
|
33
|
-
import { insidePrimary, respond, safely, saveStatus } from './state.js';
|
|
36
|
+
import { insidePrimary, insideWorkspace, respond, safely, saveStatus } from './state.js';
|
|
37
|
+
/** How long a close waits for the session to end before it ends its ptyhost instead. */
|
|
38
|
+
const CLOSE_WAIT_MS = 8000;
|
|
39
|
+
/** An ended session's ptyhost is remembered this long: its last programs reach the audit up to 15 s later. */
|
|
40
|
+
const ROOTS_KEPT_MS = 10 * 60 * 1000;
|
|
34
41
|
function terminalTab(id, title, cwd) {
|
|
35
42
|
const view = [process.execPath, path.join(path.dirname(process.argv[1]), 'view.js'), id];
|
|
36
43
|
const wt = path.join(toolsDir, 'terminal', 'wt.exe');
|
|
@@ -93,16 +100,45 @@ function writeBudget(cpus, memoryGb, jobs, env) {
|
|
|
93
100
|
warn('budget.json could not be written: ' + errMsg(e));
|
|
94
101
|
}
|
|
95
102
|
}
|
|
96
|
-
/**
|
|
97
|
-
const
|
|
103
|
+
/** The agent, installed if it is not yet; its environment may hold its API keys. */
|
|
104
|
+
const agentProgram = async (req, env, say) => {
|
|
98
105
|
const harness = harnessFor(req);
|
|
99
106
|
// Never refresh here: main already did, and replacing a running agent's executable would fail.
|
|
100
107
|
say('Preparing ' + req.harness);
|
|
101
108
|
await agents.provision(req.harness, harness, env, false);
|
|
102
109
|
const launch = agents.resolveLaunch(req.harness, harness);
|
|
103
110
|
const base = [...launch.args, ...harness.arguments];
|
|
111
|
+
return {
|
|
112
|
+
command: commandLine(launch.file, [...base, ...req.args]),
|
|
113
|
+
restartCommand: commandLine(launch.file, [...base, ...(harness.resumeArguments ?? [])]),
|
|
114
|
+
environment: harness.environment,
|
|
115
|
+
};
|
|
116
|
+
};
|
|
117
|
+
/** Where the shells are: PowerShell 7 from the toolchains (or an installed copy), Windows' own PowerShell and cmd. */
|
|
118
|
+
function shellPaths() {
|
|
119
|
+
const candidates = [
|
|
120
|
+
path.join(pwshDir(), 'pwsh.exe'),
|
|
121
|
+
path.join(process.env.ProgramFiles ?? 'C:\\Program Files', 'PowerShell', '7', 'pwsh.exe'),
|
|
122
|
+
];
|
|
123
|
+
return { pwsh: candidates.find((f) => fs.existsSync(f)) ?? null, powershell: SYSTEM.powershell(), cmd: SYSTEM.cmd() };
|
|
124
|
+
}
|
|
125
|
+
/** A plain shell: no install, no arguments of the request, nothing of the agent's environment. */
|
|
126
|
+
const shellProgram = (shell) => {
|
|
127
|
+
const line = commandLine(shell.file, shell.args);
|
|
128
|
+
return { command: line, restartCommand: line, environment: {} };
|
|
129
|
+
};
|
|
130
|
+
/** Starts ptyhost for the program and waits until it listens; a failure carries the tail of ptyhost's own log. */
|
|
131
|
+
const launchPtyHost = async (req, program, remote, env, sessions, onSpawn) => {
|
|
104
132
|
const port = await freePort();
|
|
105
133
|
const token = crypto.randomBytes(24).toString('base64url');
|
|
134
|
+
const sessionEnv = {
|
|
135
|
+
...withRegistryPath(env),
|
|
136
|
+
...program.environment,
|
|
137
|
+
...memoryEnv(env, sessions),
|
|
138
|
+
};
|
|
139
|
+
// Each PowerShell builds its own module path; one inherited from another PowerShell breaks its modules.
|
|
140
|
+
if (req.shell)
|
|
141
|
+
delete sessionEnv.PSModulePath;
|
|
106
142
|
const child = startPtyHost({
|
|
107
143
|
exe: ensurePtyHost(),
|
|
108
144
|
port,
|
|
@@ -111,11 +147,12 @@ const launchPtyHost = async (req, remote, env, say, sessions) => {
|
|
|
111
147
|
rows: 32,
|
|
112
148
|
id: req.id,
|
|
113
149
|
cwd: req.cwd,
|
|
114
|
-
env:
|
|
150
|
+
env: sessionEnv,
|
|
115
151
|
remote,
|
|
116
|
-
command:
|
|
117
|
-
restartCommand:
|
|
152
|
+
command: program.command,
|
|
153
|
+
restartCommand: program.restartCommand,
|
|
118
154
|
});
|
|
155
|
+
onSpawn(child);
|
|
119
156
|
try {
|
|
120
157
|
await waitForPtyHost(child, port, 15000);
|
|
121
158
|
}
|
|
@@ -131,6 +168,16 @@ const launchPtyHost = async (req, remote, env, say, sessions) => {
|
|
|
131
168
|
/** The launch folder's project, or the registered project this session is for (provisioned first if needed). */
|
|
132
169
|
const placeFor = async (req, say) => {
|
|
133
170
|
if (!req.project) {
|
|
171
|
+
// A project set up earlier (the web UI and sandbox shell name it by its folder only): its own environment.
|
|
172
|
+
const known = insidePrimary(req.cwd) ? null : provisioner.provisionedFor(req.cwd);
|
|
173
|
+
if (known && exists(req.cwd))
|
|
174
|
+
return {
|
|
175
|
+
env: known.env,
|
|
176
|
+
hooks: known.spec.hooks ? projectHooks(known.spec.name, known.spec.workspace) : null,
|
|
177
|
+
project: known.spec.name,
|
|
178
|
+
};
|
|
179
|
+
if (!insidePrimary(req.cwd) && insideWorkspace(req.cwd))
|
|
180
|
+
throw new Error('The project in ' + req.cwd + ' is not set up in the sandbox yet: run sandbox start in its folder first.');
|
|
134
181
|
if (!insidePrimary(req.cwd) || !exists(req.cwd))
|
|
135
182
|
throw new Error('Folder is not inside the sandbox workspace ' + workspace + ': ' + req.cwd);
|
|
136
183
|
return { env: process.env, hooks: null, project: primaryProject() };
|
|
@@ -160,20 +207,55 @@ export class SessionManager {
|
|
|
160
207
|
/** Sessions and projects being set up right now (a headless sandbox must not shut down under them). */
|
|
161
208
|
starting = 0;
|
|
162
209
|
idle = new IdleShutdown(() => this.live.size > 0 || this.starting > 0);
|
|
210
|
+
/** Each session's ptyhost (process id and when it ran), for linking the programs it starts to it (audit). */
|
|
211
|
+
ptyHosts = new Map();
|
|
163
212
|
/** How many sessions are running. */
|
|
164
213
|
get count() {
|
|
165
214
|
return this.live.size;
|
|
166
215
|
}
|
|
167
|
-
/**
|
|
216
|
+
/** The sessions' ptyhosts, running or ended within ROOTS_KEPT_MS (audit events arrive after they happen). */
|
|
217
|
+
roots() {
|
|
218
|
+
const cutoff = Date.now() - ROOTS_KEPT_MS;
|
|
219
|
+
for (const [id, r] of this.ptyHosts)
|
|
220
|
+
if (r.to !== undefined && r.to < cutoff)
|
|
221
|
+
this.ptyHosts.delete(id);
|
|
222
|
+
return [...this.ptyHosts.values()];
|
|
223
|
+
}
|
|
224
|
+
rootStarted(id, child, from) {
|
|
225
|
+
if (child.pid)
|
|
226
|
+
this.ptyHosts.set(id, { session: id, pid: child.pid, from });
|
|
227
|
+
child.once('exit', () => this.rootEnded(id));
|
|
228
|
+
}
|
|
229
|
+
rootEnded(id) {
|
|
230
|
+
const r = this.ptyHosts.get(id);
|
|
231
|
+
if (r && r.to === undefined)
|
|
232
|
+
this.ptyHosts.set(id, { ...r, to: Date.now() });
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* `session` requests: an agent (or a plain shell) in its own ptyhost, in the launch folder's project or a registered
|
|
236
|
+
* one.
|
|
237
|
+
*/
|
|
168
238
|
async start(req) {
|
|
239
|
+
let shell = null;
|
|
240
|
+
let problem = null;
|
|
241
|
+
if (req.shell)
|
|
242
|
+
try {
|
|
243
|
+
shell = shellLaunch(req.shell, shellPaths());
|
|
244
|
+
}
|
|
245
|
+
catch (e) {
|
|
246
|
+
problem = errMsg(e);
|
|
247
|
+
}
|
|
248
|
+
// A shell is listed by its own name (pwsh, powershell, cmd).
|
|
249
|
+
const name = shell?.name ?? (req.shell ? 'shell' : req.harness);
|
|
169
250
|
let status = {
|
|
170
251
|
id: req.id,
|
|
171
252
|
launchId: session.launchId,
|
|
172
|
-
harness:
|
|
253
|
+
harness: name,
|
|
254
|
+
...(req.shell ? { kind: 'shell' } : {}),
|
|
173
255
|
cwd: req.cwd,
|
|
174
256
|
...(req.project ? { project: req.project.name } : {}),
|
|
175
257
|
phase: 'starting',
|
|
176
|
-
message: 'Starting ' +
|
|
258
|
+
message: 'Starting ' + name,
|
|
177
259
|
updatedAt: now(),
|
|
178
260
|
startedAt: now(),
|
|
179
261
|
};
|
|
@@ -186,29 +268,34 @@ export class SessionManager {
|
|
|
186
268
|
this.starting++;
|
|
187
269
|
// Sessions after the first get their own tab right away (it shows the setup progress until the agent starts).
|
|
188
270
|
if (req.id !== session.launchId && process.env.AGENT_NO_TABS !== '1')
|
|
189
|
-
safely('terminal tab', () => terminalTab(req.id,
|
|
271
|
+
safely('terminal tab', () => terminalTab(req.id, name + ' ' + path.basename(req.cwd), req.cwd));
|
|
190
272
|
try {
|
|
273
|
+
if (problem)
|
|
274
|
+
throw new Error(problem);
|
|
191
275
|
const place = await placeFor(req, say);
|
|
276
|
+
const program = shell ? shellProgram(shell) : await agentProgram(req, place.env, say);
|
|
192
277
|
const remote = session.networking && session.attach.enabled;
|
|
193
|
-
const
|
|
194
|
-
const
|
|
278
|
+
const from = Date.now();
|
|
279
|
+
const { child, port, token } = await launchPtyHost(req, program, remote, place.env, this.live.size + 1, (c) => this.rootStarted(req.id, c, from));
|
|
280
|
+
const agent = shell === null;
|
|
281
|
+
const resumed = agent && req.harness === 'claude' ? resumedConversation(req.args) : undefined;
|
|
195
282
|
const running = {
|
|
196
283
|
...status,
|
|
197
284
|
...(resumed ? { conversation: resumed } : {}),
|
|
198
285
|
project: place.project,
|
|
199
286
|
phase: 'running',
|
|
200
|
-
message:
|
|
287
|
+
message: name + ' is running',
|
|
201
288
|
attach: { addresses: remote ? guestAddresses() : [], port, token },
|
|
202
289
|
};
|
|
203
290
|
this.live.set(req.id, { status: running, child });
|
|
204
291
|
child.once('exit', (code) => this.onExit(running, place, code));
|
|
205
292
|
saveStatus(running);
|
|
206
|
-
if (req.harness === 'claude')
|
|
293
|
+
if (agent && req.harness === 'claude')
|
|
207
294
|
this.trackConversation(running.id, resumed);
|
|
208
295
|
log('Session ' +
|
|
209
296
|
req.id +
|
|
210
297
|
' started (' +
|
|
211
|
-
|
|
298
|
+
name +
|
|
212
299
|
' in ' +
|
|
213
300
|
req.cwd +
|
|
214
301
|
', project ' +
|
|
@@ -218,6 +305,7 @@ export class SessionManager {
|
|
|
218
305
|
')');
|
|
219
306
|
}
|
|
220
307
|
catch (e) {
|
|
308
|
+
this.rootEnded(req.id);
|
|
221
309
|
saveStatus({ ...status, phase: 'failed', message: errMsg(e) });
|
|
222
310
|
warn('Session ' + req.id + ' failed: ' + errMsg(e));
|
|
223
311
|
this.idle.check();
|
|
@@ -246,15 +334,53 @@ export class SessionManager {
|
|
|
246
334
|
this.idle.check();
|
|
247
335
|
}
|
|
248
336
|
}
|
|
249
|
-
/**
|
|
337
|
+
/**
|
|
338
|
+
* `restart` requests: the matching sessions restart their agent (continuing the conversation where it can). Shells
|
|
339
|
+
* are left alone: restarting one would only throw away what it was doing.
|
|
340
|
+
*/
|
|
250
341
|
async restart(req) {
|
|
251
342
|
const targets = [...this.live.values()].filter((s) => s.status.attach &&
|
|
343
|
+
s.status.kind !== 'shell' &&
|
|
252
344
|
(req.harness === null || s.status.harness === req.harness) &&
|
|
253
345
|
(!req.cwd || isWithin(req.cwd, s.status.cwd)));
|
|
254
346
|
for (const s of targets)
|
|
255
347
|
await sendControl({ addresses: ['127.0.0.1'], port: s.status.attach.port, token: s.status.attach.token }, FrameType.Restart);
|
|
256
348
|
respond(req.id, { ok: true, message: 'restarted ' + targets.length + ' session(s)' });
|
|
257
349
|
}
|
|
350
|
+
/**
|
|
351
|
+
* `close` requests: the session's ptyhost ends its program and everything it started (ProcessTree), then exits as
|
|
352
|
+
* if the program had. One that does not end in time has its ptyhost ended instead (closing the pseudo console ends
|
|
353
|
+
* what is attached to it).
|
|
354
|
+
*/
|
|
355
|
+
async close(req) {
|
|
356
|
+
const entry = this.live.get(req.session);
|
|
357
|
+
if (!entry?.status.attach) {
|
|
358
|
+
respond(req.id, { ok: false, message: 'Session ' + req.session + ' is not running.' });
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
const { child, status } = entry;
|
|
362
|
+
const ended = new Promise((resolve) => {
|
|
363
|
+
if (child.exitCode !== null || child.signalCode !== null)
|
|
364
|
+
return resolve(true);
|
|
365
|
+
const timer = setTimeout(() => resolve(false), CLOSE_WAIT_MS);
|
|
366
|
+
child.once('exit', () => {
|
|
367
|
+
clearTimeout(timer);
|
|
368
|
+
resolve(true);
|
|
369
|
+
});
|
|
370
|
+
});
|
|
371
|
+
try {
|
|
372
|
+
await sendControl({ addresses: ['127.0.0.1'], port: status.attach.port, token: status.attach.token }, FrameType.Close);
|
|
373
|
+
}
|
|
374
|
+
catch (e) {
|
|
375
|
+
warn('Session ' + req.session + ' did not take the close request: ' + errMsg(e));
|
|
376
|
+
}
|
|
377
|
+
if (!(await ended)) {
|
|
378
|
+
warn('Session ' + req.session + ' did not end in time; ending its session host.');
|
|
379
|
+
safely('ending a session host', () => child.kill());
|
|
380
|
+
}
|
|
381
|
+
log('Session ' + req.session + ' closed on request');
|
|
382
|
+
respond(req.id, { ok: true, message: 'Closed ' + status.harness + ' session ' + req.session + '.' });
|
|
383
|
+
}
|
|
258
384
|
/**
|
|
259
385
|
* Finds out which of Claude's conversation files this session writes (it appears with the first message) and puts
|
|
260
386
|
* it in the status, so the host can reopen this very conversation. A reopened session starts with the one it was
|
|
@@ -299,6 +425,11 @@ export class SessionManager {
|
|
|
299
425
|
attach: undefined,
|
|
300
426
|
});
|
|
301
427
|
log('Session ' + running.id + ' ended (exit ' + code + ')');
|
|
428
|
+
// A shell ran no agent: no post-agent hooks, nothing of an agent's to save.
|
|
429
|
+
if (running.kind === 'shell') {
|
|
430
|
+
this.idle.check();
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
302
433
|
if (place.hooks)
|
|
303
434
|
void runHooksAsync('post-agent', place.env, place.hooks).catch((e) => warn('post-agent hooks failed: ' + errMsg(e)));
|
|
304
435
|
else
|
|
@@ -9,13 +9,14 @@ import { log, warn } from '../runtime/log.js';
|
|
|
9
9
|
import { adoptBootSecrets } from '../runtime/secrets.js';
|
|
10
10
|
import { errMsg } from '../../core/util/errors.js';
|
|
11
11
|
import { Daemon } from '../daemon/daemon.js';
|
|
12
|
+
import { sessions } from '../daemon/sessions.js';
|
|
12
13
|
import { governor } from '../governor/index.js';
|
|
13
14
|
process.on('uncaughtException', (e) => warn('daemon: unexpected error (continuing): ' + (e.stack ?? errMsg(e))));
|
|
14
15
|
process.on('unhandledRejection', (e) => warn('daemon: unexpected error (continuing): ' + errMsg(e)));
|
|
15
16
|
adoptBootSecrets();
|
|
16
17
|
log('Session daemon started (pid ' + process.pid + ').');
|
|
17
18
|
safely('port forwards', startRelays);
|
|
18
|
-
safely('audit', startProcessCollector);
|
|
19
|
+
safely('audit', () => startProcessCollector(() => sessions.roots()));
|
|
19
20
|
safely('memory governor', () => governor.start());
|
|
20
21
|
log('Memory governor: ' + governor.describe());
|
|
21
22
|
new Daemon().start();
|
|
@@ -57,6 +57,8 @@ const LISTING = {
|
|
|
57
57
|
/** Global options that take a value (`git -C dir status`). Config overrides (-c, --config-env) are refused outright. */
|
|
58
58
|
const WITH_VALUE = new Set(['-C', '--namespace']);
|
|
59
59
|
const FORBIDDEN_GLOBAL = /^(-c|--config-env|--exec-path|--git-dir|--work-tree|--super-prefix)(=|$)/;
|
|
60
|
+
/** Options of reading commands that run a program or write a file (grep -O<cmd>, ls-remote --upload-pack, --output). */
|
|
61
|
+
const RUNS_OR_WRITES = /^(--open-files-in-pager|--upload-pack|--output|--ext-diff)(=|$)/;
|
|
60
62
|
export function gitDecision(args, access) {
|
|
61
63
|
if (access === 'full')
|
|
62
64
|
return { allow: true };
|
|
@@ -72,6 +74,11 @@ export function gitDecision(args, access) {
|
|
|
72
74
|
i += WITH_VALUE.has(args[i]) ? 2 : 1;
|
|
73
75
|
}
|
|
74
76
|
const [command, ...rest] = args.slice(i);
|
|
77
|
+
const runsOrWrites = rest.find((a) => RUNS_OR_WRITES.test(a) ||
|
|
78
|
+
(command === 'ls-remote' && /^-u(.|$)/.test(a)) ||
|
|
79
|
+
(command === 'grep' && /^-O/.test(a)));
|
|
80
|
+
if (runsOrWrites)
|
|
81
|
+
return { allow: false, reason: 'git ' + runsOrWrites + ' is not allowed: git is read-only in this sandbox.' };
|
|
75
82
|
if (command === undefined || command === '--version' || READ_ONLY.has(command) || (LISTING[command]?.(rest) ?? false))
|
|
76
83
|
return { allow: true };
|
|
77
84
|
return {
|
|
@@ -10,3 +10,17 @@ export declare function quoteArg(arg: string): string;
|
|
|
10
10
|
export declare function cmdArgs(args: string[]): string;
|
|
11
11
|
/** The command line ptyhost starts; when the program is cmd.exe its arguments are quoted for cmd (cmdArgs). */
|
|
12
12
|
export declare function commandLine(file: string, args: string[]): string;
|
|
13
|
+
/** A plain shell session's program: which shell, its absolute path and its arguments. */
|
|
14
|
+
export interface ShellLaunch {
|
|
15
|
+
readonly name: 'pwsh' | 'powershell' | 'cmd';
|
|
16
|
+
readonly file: string;
|
|
17
|
+
readonly args: string[];
|
|
18
|
+
}
|
|
19
|
+
/** Where the shells are in this sandbox (pwsh: null when PowerShell 7 is not installed). */
|
|
20
|
+
export interface ShellPaths {
|
|
21
|
+
readonly pwsh: string | null;
|
|
22
|
+
readonly powershell: string;
|
|
23
|
+
readonly cmd: string;
|
|
24
|
+
}
|
|
25
|
+
/** The shell a session asked for runs as (auto: PowerShell 7 when present, else Windows PowerShell). Pure. */
|
|
26
|
+
export declare function shellLaunch(asked: 'auto' | ShellLaunch['name'], paths: ShellPaths): ShellLaunch;
|
|
@@ -31,3 +31,13 @@ export function commandLine(file, args) {
|
|
|
31
31
|
? [quoteArg(file), cmdArgs(args)].filter(Boolean).join(' ')
|
|
32
32
|
: [file, ...args].map(quoteArg).join(' ');
|
|
33
33
|
}
|
|
34
|
+
/** The shell a session asked for runs as (auto: PowerShell 7 when present, else Windows PowerShell). Pure. */
|
|
35
|
+
export function shellLaunch(asked, paths) {
|
|
36
|
+
if (asked === 'cmd')
|
|
37
|
+
return { name: 'cmd', file: paths.cmd, args: [] };
|
|
38
|
+
if (asked === 'powershell' || (asked === 'auto' && !paths.pwsh))
|
|
39
|
+
return { name: 'powershell', file: paths.powershell, args: ['-NoLogo'] };
|
|
40
|
+
if (!paths.pwsh)
|
|
41
|
+
throw new Error('PowerShell 7 is not installed in this sandbox (toolchains.pwsh). Open powershell or cmd instead, or turn pwsh on.');
|
|
42
|
+
return { name: 'pwsh', file: paths.pwsh, args: ['-NoLogo'] };
|
|
43
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// The agent add-ons' MCP servers (Playwright, Context7) in the sandbox's own Claude Code: user-scope servers named
|
|
2
|
+
// sbx-... in its .claude.json, set every boot while the add-on is on and removed again when it is off. Your servers
|
|
3
|
+
// (from this PC or added in the sandbox) are never touched, and nothing on this PC changes.
|
|
4
|
+
import fs from 'node:fs';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { addonMcpServer, APPS, withAddonMcpServers } from '../../shared/apps/index.js';
|
|
7
|
+
import { writeJsonAtomic } from '../../shared/io/json.js';
|
|
8
|
+
import { claudeDir } from '../runtime/session.js';
|
|
9
|
+
import { log, warn } from '../runtime/log.js';
|
|
10
|
+
import { installedMcpAddons } from './agent-addons.js';
|
|
11
|
+
/** The names of every MCP server an add-on can add (the ones the sandbox may remove). */
|
|
12
|
+
const ADDON_MCP_NAMES = APPS.flatMap((a) => (a.mcp ? [a.mcp.name] : []));
|
|
13
|
+
/** Sets the add-ons' MCP servers in the sandbox's .claude.json; the names that are on. */
|
|
14
|
+
export function writeAddonMcpServers() {
|
|
15
|
+
const servers = {};
|
|
16
|
+
for (const { id, folder } of installedMcpAddons()) {
|
|
17
|
+
const mcp = APPS.find((a) => a.id === id)?.mcp;
|
|
18
|
+
if (mcp)
|
|
19
|
+
servers[mcp.name] = addonMcpServer(folder, mcp);
|
|
20
|
+
}
|
|
21
|
+
const file = path.join(claudeDir, '.claude.json');
|
|
22
|
+
let state = {};
|
|
23
|
+
if (fs.existsSync(file)) {
|
|
24
|
+
try {
|
|
25
|
+
const text = fs.readFileSync(file, 'utf8');
|
|
26
|
+
state = JSON.parse(text.charCodeAt(0) === 0xfeff ? text.slice(1) : text);
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
// Never rewrite a file Claude Code could not read either: its login and state are in it.
|
|
30
|
+
warn('Claude Code MCP servers from Apps: ' + file + ' is not valid JSON, left as it is');
|
|
31
|
+
return [];
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
else if (!Object.keys(servers).length)
|
|
35
|
+
return [];
|
|
36
|
+
if (typeof state !== 'object' || state === null || Array.isArray(state))
|
|
37
|
+
return [];
|
|
38
|
+
const current = state.mcpServers;
|
|
39
|
+
const before = current && typeof current === 'object' && !Array.isArray(current) ? current : {};
|
|
40
|
+
const after = withAddonMcpServers(before, servers, ADDON_MCP_NAMES);
|
|
41
|
+
if (JSON.stringify(before) !== JSON.stringify(after)) {
|
|
42
|
+
fs.mkdirSync(claudeDir, { recursive: true });
|
|
43
|
+
writeJsonAtomic(file, { ...state, mcpServers: after });
|
|
44
|
+
}
|
|
45
|
+
const names = Object.keys(servers);
|
|
46
|
+
if (names.length)
|
|
47
|
+
log('Claude Code MCP servers from Apps: ' + names.join(', '));
|
|
48
|
+
return names;
|
|
49
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** The add-on's program when it is chosen and installed, else null. */
|
|
2
|
+
export declare function installedAddon(id: string): string | null;
|
|
3
|
+
/** rtk.exe when rtk is chosen and installed, else null. */
|
|
4
|
+
export declare function installedRtk(): string | null;
|
|
5
|
+
/** The add-ons with an MCP server that are chosen and installed: their folder and server. */
|
|
6
|
+
export declare function installedMcpAddons(): {
|
|
7
|
+
id: string;
|
|
8
|
+
folder: string;
|
|
9
|
+
}[];
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// Agent add-ons in this sandbox: an add-on is plugged into the agents only while it is chosen (apps.install) AND its
|
|
2
|
+
// program is really there (a failed download adds no hook, proxy or MCP server that would fail on every use).
|
|
3
|
+
import { APPS, appById } from '../../shared/apps/index.js';
|
|
4
|
+
import { findExe } from '../apps/find-exe.js';
|
|
5
|
+
import { dryRun } from '../runtime/paths.js';
|
|
6
|
+
import { session } from '../runtime/session.js';
|
|
7
|
+
import { tool } from '../toolchains/tool-env.js';
|
|
8
|
+
/** The add-on's program when it is chosen and installed, else null. */
|
|
9
|
+
export function installedAddon(id) {
|
|
10
|
+
const entry = appById(id);
|
|
11
|
+
if (dryRun || !entry || !(session.apps?.install ?? []).includes(id))
|
|
12
|
+
return null;
|
|
13
|
+
return findExe(tool('apps', id), entry.exe);
|
|
14
|
+
}
|
|
15
|
+
/** rtk.exe when rtk is chosen and installed, else null. */
|
|
16
|
+
export function installedRtk() {
|
|
17
|
+
return installedAddon('rtk');
|
|
18
|
+
}
|
|
19
|
+
/** The add-ons with an MCP server that are chosen and installed: their folder and server. */
|
|
20
|
+
export function installedMcpAddons() {
|
|
21
|
+
return APPS.filter((a) => a.mcp && installedAddon(a.id)).map((a) => ({ id: a.id, folder: tool('apps', a.id) }));
|
|
22
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** The proxy's log (it redacts keys and tokens in what it logs). */
|
|
2
|
+
export declare const HEADROOM_LOG: string;
|
|
3
|
+
/**
|
|
4
|
+
* Starts the proxy when Headroom is chosen and installed; its base URL once it answers, else null (Claude Code then
|
|
5
|
+
* talks to the API directly).
|
|
6
|
+
*/
|
|
7
|
+
export declare function startHeadroom(env: NodeJS.ProcessEnv): Promise<string | null>;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// The Headroom agent add-on: its proxy runs detached on 127.0.0.1 for the sandbox's life, and Claude Code is pointed at
|
|
2
|
+
// it (managed-policy.ts) only once it answers as Headroom. It forwards only to Anthropic's API, starts without the
|
|
3
|
+
// agents' keys in its environment and with HEADROOM_OFFLINE (no telemetry, update checks or model downloads).
|
|
4
|
+
import fs from 'node:fs';
|
|
5
|
+
import http from 'node:http';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import { spawn } from 'node:child_process';
|
|
8
|
+
import { HEADROOM_PORT, headroomArgs, headroomEnv, headroomUrl } from '../../shared/apps/index.js';
|
|
9
|
+
import { logsDir } from '../runtime/paths.js';
|
|
10
|
+
import { portOpen, waitForPort } from '../runtime/ports.js';
|
|
11
|
+
import { log } from '../runtime/log.js';
|
|
12
|
+
import { installedAddon } from './agent-addons.js';
|
|
13
|
+
/** The proxy's log (it redacts keys and tokens in what it logs). */
|
|
14
|
+
export const HEADROOM_LOG = path.join(logsDir, 'headroom.log');
|
|
15
|
+
/** Whether Headroom's own health check answers on the port (not some other program that took it). */
|
|
16
|
+
function answersAsHeadroom(port) {
|
|
17
|
+
return new Promise((resolve) => {
|
|
18
|
+
const req = http.get({ host: '127.0.0.1', port, path: '/livez', timeout: 3000 }, (res) => {
|
|
19
|
+
let body = '';
|
|
20
|
+
res.setEncoding('utf8');
|
|
21
|
+
res.on('data', (chunk) => {
|
|
22
|
+
if (body.length < 4096)
|
|
23
|
+
body += chunk;
|
|
24
|
+
});
|
|
25
|
+
res.on('end', () => resolve(res.statusCode === 200 && /"service"\s*:\s*"headroom-proxy"/.test(body)));
|
|
26
|
+
});
|
|
27
|
+
req.on('timeout', () => req.destroy());
|
|
28
|
+
req.on('error', () => resolve(false));
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Starts the proxy when Headroom is chosen and installed; its base URL once it answers, else null (Claude Code then
|
|
33
|
+
* talks to the API directly).
|
|
34
|
+
*/
|
|
35
|
+
export async function startHeadroom(env) {
|
|
36
|
+
const exe = installedAddon('headroom');
|
|
37
|
+
if (!exe)
|
|
38
|
+
return null;
|
|
39
|
+
if (env.ANTHROPIC_BASE_URL) {
|
|
40
|
+
log('Headroom: not used, ANTHROPIC_BASE_URL is set in the agent environment (' + env.ANTHROPIC_BASE_URL + ')');
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
const port = HEADROOM_PORT;
|
|
44
|
+
if (!(await portOpen(port))) {
|
|
45
|
+
const out = fs.openSync(HEADROOM_LOG, 'a');
|
|
46
|
+
spawn(exe, headroomArgs(port), {
|
|
47
|
+
detached: true,
|
|
48
|
+
stdio: ['ignore', out, out],
|
|
49
|
+
windowsHide: true,
|
|
50
|
+
env: headroomEnv(env),
|
|
51
|
+
}).unref();
|
|
52
|
+
fs.closeSync(out);
|
|
53
|
+
if (!(await waitForPort(port, 90)))
|
|
54
|
+
throw new Error('the Headroom proxy did not start on port ' + port + '; see ' + HEADROOM_LOG);
|
|
55
|
+
}
|
|
56
|
+
if (!(await answersAsHeadroom(port)))
|
|
57
|
+
throw new Error('port ' + port + ' is taken by another program; Claude Code talks to the API directly');
|
|
58
|
+
log('Headroom: Claude Code reaches the API through ' + headroomUrl(port) + ' (log: ' + HEADROOM_LOG + ')');
|
|
59
|
+
return headroomUrl(port);
|
|
60
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
import type { GitAccess } from '../../core/types.js';
|
|
2
2
|
export declare function managedSettingsFile(): string;
|
|
3
|
-
|
|
3
|
+
/** `headroom`: the proxy's base URL when it answered at this boot (null: Claude Code talks to the API directly). */
|
|
4
|
+
export declare function writeManagedSettings(access: GitAccess, headroom?: string | null): void;
|
|
@@ -1,20 +1,29 @@
|
|
|
1
1
|
// Claude Code managed settings for the sandbox (C:\Program Files\ClaudeCode\managed-settings.json). Managed settings
|
|
2
|
-
// have the highest precedence and cannot be overridden by user or project settings, so they carry the hard rules
|
|
3
|
-
//
|
|
2
|
+
// have the highest precedence and cannot be overridden by user or project settings, so they carry the hard rules (the
|
|
3
|
+
// git access mode), the sandbox's own hooks (rtk, when it is installed) and the Headroom proxy's address while it runs
|
|
4
|
+
// (env.ANTHROPIC_BASE_URL). Rewritten every boot; removed when there is nothing to write. Your Claude settings from
|
|
5
|
+
// this PC are never changed for this.
|
|
4
6
|
import fs from 'node:fs';
|
|
5
7
|
import path from 'node:path';
|
|
8
|
+
import { claudeManagedSettings } from '../../shared/apps/index.js';
|
|
6
9
|
import { gitDenyRules } from './git-rules.js';
|
|
7
10
|
import { managedClaudeMd } from '../boot/guide.js';
|
|
11
|
+
import { installedRtk } from './agent-addons.js';
|
|
12
|
+
import { log } from '../runtime/log.js';
|
|
8
13
|
export function managedSettingsFile() {
|
|
9
14
|
return path.join(path.dirname(managedClaudeMd()), 'managed-settings.json');
|
|
10
15
|
}
|
|
11
|
-
|
|
16
|
+
/** `headroom`: the proxy's base URL when it answered at this boot (null: Claude Code talks to the API directly). */
|
|
17
|
+
export function writeManagedSettings(access, headroom = null) {
|
|
12
18
|
const file = managedSettingsFile();
|
|
13
|
-
const
|
|
14
|
-
|
|
19
|
+
const rtk = installedRtk();
|
|
20
|
+
const settings = claudeManagedSettings(gitDenyRules(access), rtk, headroom);
|
|
21
|
+
if (!settings) {
|
|
15
22
|
fs.rmSync(file, { force: true });
|
|
16
23
|
return;
|
|
17
24
|
}
|
|
18
25
|
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
19
|
-
fs.writeFileSync(file, JSON.stringify(
|
|
26
|
+
fs.writeFileSync(file, JSON.stringify(settings, null, 2) + '\n', 'utf8');
|
|
27
|
+
if (rtk)
|
|
28
|
+
log("rtk: Claude Code's Bash commands go through it (hook in " + file + ')');
|
|
20
29
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { appsNeedMesa, setupApps } from '../apps/index.js';
|
|
1
|
+
import { appsNeedMesa, appsNeedUv, setupApps } from '../apps/index.js';
|
|
2
2
|
import { dryRun } from '../runtime/paths.js';
|
|
3
3
|
import { wanted } from './detect.js';
|
|
4
4
|
import { DATABASE_NAMES, setupDatabase } from '../databases/index.js';
|
|
@@ -85,7 +85,12 @@ export function plan(config, versions, o) {
|
|
|
85
85
|
},
|
|
86
86
|
{ name: 'Deno', enabled: wanted(config.deno.enabled, 'deno'), install: (env) => setupDeno(env) },
|
|
87
87
|
{ name: 'Bun', enabled: wanted(config.bun.enabled, 'bun'), install: (env) => setupBun(env) },
|
|
88
|
-
|
|
88
|
+
// Headroom (an agent add-on) installs with uv: the uv step runs for it too, before the apps.
|
|
89
|
+
{
|
|
90
|
+
name: 'uv',
|
|
91
|
+
enabled: wanted(config.uv.enabled, 'uv') || appsNeedUv(o.apps),
|
|
92
|
+
install: (env) => setupUv(env),
|
|
93
|
+
},
|
|
89
94
|
{ name: 'PowerShell 7', enabled: wanted(config.pwsh.enabled, 'pwsh'), install: (env) => setupPwsh(env, o.refresh) },
|
|
90
95
|
// The openssl tool comes from Git's install: after Git.
|
|
91
96
|
{
|
|
@@ -111,7 +116,7 @@ export function plan(config, versions, o) {
|
|
|
111
116
|
name: 'Apps',
|
|
112
117
|
enabled: apps,
|
|
113
118
|
shared: true,
|
|
114
|
-
after: ['Mesa 3D', 'Visual C++ runtime'],
|
|
119
|
+
after: ['Mesa 3D', 'Visual C++ runtime', 'uv'],
|
|
115
120
|
install: (env) => setupApps(env, o.apps, { gitAccess: o.gitAccess, refresh: o.refresh }),
|
|
116
121
|
},
|
|
117
122
|
{
|
|
@@ -17,6 +17,9 @@ export async function setupToolchains(config, versions, options, run = {}) {
|
|
|
17
17
|
env.summary.push(...part.summary);
|
|
18
18
|
}
|
|
19
19
|
Object.assign(env.vars, config.environment);
|
|
20
|
+
// The git guard's variables (AGENT_GIT_ACCESS, GIT_DIR, GIT_CONFIG_*, BASH_ENV) always win over
|
|
21
|
+
// toolchains.environment.
|
|
22
|
+
Object.assign(env.vars, envs.get('Git')?.vars);
|
|
20
23
|
log('Set up ' + steps.length + ' toolchains and databases.');
|
|
21
24
|
return env;
|
|
22
25
|
}
|