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
package/dist/core/schema/web.js
CHANGED
|
@@ -1,6 +1,50 @@
|
|
|
1
|
-
// The web UI settings (sandbox web): not part of LocalOverrides, a repo never turns a way into your agents on.
|
|
1
|
+
// The web UI settings (sandbox web): not part of LocalOverrides, a repo never turns a way into your agents on. The
|
|
2
|
+
// remote group (other Windows users of this PC, devices on your network) is off by default: design/remote-access.md.
|
|
2
3
|
import { z } from 'zod';
|
|
3
4
|
export const WEB_DEFAULT_PORT = 47800;
|
|
5
|
+
export const WEB_LAN_DEFAULT_PORT = 47801;
|
|
6
|
+
export const REMOTE_ROLES = ['view', 'control'];
|
|
7
|
+
/** A Windows account name: "bob", "PC\bob" or "DOMAIN\bob" (no paths, quotes or wildcards). */
|
|
8
|
+
export const WindowsUserNameSchema = z
|
|
9
|
+
.string()
|
|
10
|
+
.trim()
|
|
11
|
+
.min(1)
|
|
12
|
+
.max(120)
|
|
13
|
+
.regex(/^(?:[A-Za-z0-9][A-Za-z0-9._ -]{0,62}\\)?[^\\/"[\]:;|=,+*?<>@]{1,64}$/, 'a Windows user name (name or DOMAIN\\name)');
|
|
14
|
+
export const RemoteUserSchema = z.strictObject({
|
|
15
|
+
name: WindowsUserNameSchema.describe('The Windows user (name, or DOMAIN\\name)'),
|
|
16
|
+
role: z
|
|
17
|
+
.enum(REMOTE_ROLES)
|
|
18
|
+
.describe('view: sees everything, terminals read-only. control: everything but remote access, logins and tokens.'),
|
|
19
|
+
});
|
|
20
|
+
export const WebLanSchema = z.strictObject({
|
|
21
|
+
enabled: z
|
|
22
|
+
.boolean()
|
|
23
|
+
.describe('(Beta) Devices on your network (a phone): an HTTPS address on this PC for devices you pair with a one-time code (sandbox web pair). Needs a firewall rule (sandbox web lan on asks Windows once). Off: every paired device is signed out.'),
|
|
24
|
+
port: z.number().int().min(1024).max(65535).describe('HTTPS port for paired devices on your network.'),
|
|
25
|
+
address: z
|
|
26
|
+
.string()
|
|
27
|
+
.trim()
|
|
28
|
+
.max(15)
|
|
29
|
+
.regex(/^$|^\d{1,3}(\.\d{1,3}){3}$/, 'an IPv4 address or empty')
|
|
30
|
+
.describe("This PC's private address to listen on. Empty: the one of its network adapter (never a public one)."),
|
|
31
|
+
});
|
|
32
|
+
export const WebRemoteSchema = z.strictObject({
|
|
33
|
+
windowsSignIn: z
|
|
34
|
+
.boolean()
|
|
35
|
+
.describe('(Beta) Other Windows users of this PC named in web.remote.users sign in to your web UI (and sandbox remote) with their own Windows account: no password, no link.'),
|
|
36
|
+
users: z
|
|
37
|
+
.array(RemoteUserSchema)
|
|
38
|
+
.max(32)
|
|
39
|
+
.describe('Who else may sign in with Windows, and their role (view or control).'),
|
|
40
|
+
rememberDays: z
|
|
41
|
+
.number()
|
|
42
|
+
.int()
|
|
43
|
+
.min(1)
|
|
44
|
+
.max(90)
|
|
45
|
+
.describe('How many days a paired device or a Windows sign-in stays signed in (from when it signed in, not renewed).'),
|
|
46
|
+
lan: WebLanSchema,
|
|
47
|
+
});
|
|
4
48
|
export const WebSchema = z.strictObject({
|
|
5
49
|
enabled: z
|
|
6
50
|
.boolean()
|
|
@@ -14,4 +58,5 @@ export const WebSchema = z.strictObject({
|
|
|
14
58
|
openOnLaunch: z
|
|
15
59
|
.boolean()
|
|
16
60
|
.describe('(Beta) With the web UI on: open it in your browser, already signed in, every time a sandbox starts.'),
|
|
61
|
+
remote: WebRemoteSchema.describe('(Beta) Access for other Windows users of this PC and for paired devices.'),
|
|
17
62
|
});
|
package/dist/core/util/fs.d.ts
CHANGED
|
@@ -3,6 +3,11 @@ export declare function dirExists(p: string): boolean;
|
|
|
3
3
|
export declare function fileExists(p: string): boolean;
|
|
4
4
|
/** fileExists without blocking the program (the menu looks for programs while it stays responsive). */
|
|
5
5
|
export declare function fileExistsAsync(p: string): Promise<boolean>;
|
|
6
|
+
/**
|
|
7
|
+
* A file the sandbox or a repo may have written, read through one handle and only if that handle is a regular file of
|
|
8
|
+
* at most `maxBytes` (null otherwise): a multi-gigabyte .nvmrc must not stop a launch.
|
|
9
|
+
*/
|
|
10
|
+
export declare function readSmallFile(file: string, maxBytes?: number): string | null;
|
|
6
11
|
export declare function readJson<T>(p: string): T;
|
|
7
12
|
export declare function tryReadJson<T>(p: string): T | null;
|
|
8
13
|
/** Atomic (a crash or a second command never leaves half a file); refused outside temp during the self-test. */
|
package/dist/core/util/fs.js
CHANGED
|
@@ -28,6 +28,29 @@ export async function fileExistsAsync(p) {
|
|
|
28
28
|
return false;
|
|
29
29
|
}
|
|
30
30
|
}
|
|
31
|
+
/**
|
|
32
|
+
* A file the sandbox or a repo may have written, read through one handle and only if that handle is a regular file of
|
|
33
|
+
* at most `maxBytes` (null otherwise): a multi-gigabyte .nvmrc must not stop a launch.
|
|
34
|
+
*/
|
|
35
|
+
export function readSmallFile(file, maxBytes = 256 * 1024) {
|
|
36
|
+
let fd = null;
|
|
37
|
+
try {
|
|
38
|
+
fd = fs.openSync(file, 'r');
|
|
39
|
+
const stat = fs.fstatSync(fd);
|
|
40
|
+
if (!stat.isFile() || stat.size > maxBytes)
|
|
41
|
+
return null;
|
|
42
|
+
const buffer = Buffer.alloc(stat.size);
|
|
43
|
+
fs.readSync(fd, buffer, 0, stat.size, 0);
|
|
44
|
+
return buffer.toString('utf8');
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
finally {
|
|
50
|
+
if (fd !== null)
|
|
51
|
+
fs.closeSync(fd);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
31
54
|
export function readJson(p) {
|
|
32
55
|
return JSON.parse(fs.readFileSync(p, 'utf8').replace(/^\uFEFF/, ''));
|
|
33
56
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export { isWin, sleep, captureOutput } from './process.js';
|
|
2
2
|
export { UserError, fail, errMsg } from './errors.js';
|
|
3
3
|
export { parseJsonc, readJsonc } from './jsonc.js';
|
|
4
|
-
export { ensureDir, dirExists, fileExists, fileExistsAsync, readJson, tryReadJson, writeJson, realPath } from './fs.js';
|
|
4
|
+
export { ensureDir, dirExists, fileExists, fileExistsAsync, readJson, readSmallFile, tryReadJson, writeJson, realPath, } from './fs.js';
|
|
5
5
|
export { xmlEscape, safeText, plainText } from './text.js';
|
|
6
6
|
export { homeDir, expandPath, expandVariables, isRootPath, isUserProfile, isInsideOrEqual, findOnPath, } from './paths.js';
|
|
7
7
|
export { isLink, removeLink, assertNoLinks } from './links.js';
|
package/dist/core/util/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export { isWin, sleep, captureOutput } from './process.js';
|
|
2
2
|
export { UserError, fail, errMsg } from './errors.js';
|
|
3
3
|
export { parseJsonc, readJsonc } from './jsonc.js';
|
|
4
|
-
export { ensureDir, dirExists, fileExists, fileExistsAsync, readJson, tryReadJson, writeJson, realPath } from './fs.js';
|
|
4
|
+
export { ensureDir, dirExists, fileExists, fileExistsAsync, readJson, readSmallFile, tryReadJson, writeJson, realPath, } from './fs.js';
|
|
5
5
|
export { xmlEscape, safeText, plainText } from './text.js';
|
|
6
6
|
export { homeDir, expandPath, expandVariables, isRootPath, isUserProfile, isInsideOrEqual, findOnPath, } from './paths.js';
|
|
7
7
|
export { isLink, removeLink, assertNoLinks } from './links.js';
|
|
@@ -5,12 +5,13 @@ import path from 'node:path';
|
|
|
5
5
|
import { appsToInstall } from '../../shared/apps/index.js';
|
|
6
6
|
import { downloader, moveIntoPlace, stagingFolder } from '../../shared/download/index.js';
|
|
7
7
|
import { safeRemove } from '../../shared/io/safe-remove.js';
|
|
8
|
-
import { dryRun } from '../runtime/paths.js';
|
|
8
|
+
import { dryRun, launchDir } from '../runtime/paths.js';
|
|
9
9
|
import { execAsync } from '../runtime/process.js';
|
|
10
10
|
import { exists, mkdirp } from '../runtime/fs.js';
|
|
11
11
|
import { log, warn } from '../runtime/log.js';
|
|
12
12
|
import { errMsg } from '../../core/util/errors.js';
|
|
13
13
|
import { tool } from '../toolchains/tool-env.js';
|
|
14
|
+
import { uvVars } from '../toolchains/runtimes.js';
|
|
14
15
|
import { afterInstall } from './after-install.js';
|
|
15
16
|
import { findExe } from './find-exe.js';
|
|
16
17
|
import { shortcuts } from './shortcuts.js';
|
|
@@ -58,9 +59,10 @@ export class AppInstaller {
|
|
|
58
59
|
if (!exe)
|
|
59
60
|
throw new Error(entry.exe + ' not found in the download');
|
|
60
61
|
await afterInstall(entry, exe);
|
|
61
|
-
|
|
62
|
+
// A proxy or MCP server (agent add-ons) is started by the sandbox or the agent: no PATH entry, no shortcut.
|
|
63
|
+
if (!entry.service && (entry.cli || entry.id === 'sysinternals'))
|
|
62
64
|
this.env.prepend.push(path.dirname(exe));
|
|
63
|
-
if (!entry.cli)
|
|
65
|
+
if (!entry.cli && !entry.service)
|
|
64
66
|
this.gui.push({ name: entry.name, exe });
|
|
65
67
|
this.installed.push(entry.name);
|
|
66
68
|
}
|
|
@@ -74,7 +76,11 @@ export class AppInstaller {
|
|
|
74
76
|
const work = stagingFolder(dest);
|
|
75
77
|
const out = path.join(work, 'out');
|
|
76
78
|
try {
|
|
77
|
-
if ('
|
|
79
|
+
if ('locked' in entry.source) {
|
|
80
|
+
log('Installing ' + entry.name + ' ' + entry.source.version + ' (pinned, every file checked) ...');
|
|
81
|
+
await installLocked(entry.id, entry.source.locked, mkdirp(out));
|
|
82
|
+
}
|
|
83
|
+
else if ('npm' in entry.source) {
|
|
78
84
|
const npmCli = path.join(path.dirname(process.execPath), 'node_modules', 'npm', 'bin', 'npm-cli.js');
|
|
79
85
|
log('Installing ' + entry.name + ' (npm) ...');
|
|
80
86
|
await execAsync(process.execPath, [
|
|
@@ -103,6 +109,46 @@ export class AppInstaller {
|
|
|
103
109
|
}
|
|
104
110
|
}
|
|
105
111
|
}
|
|
112
|
+
/** The Python the uv lock files were made for (sandbox\addons\<id>\requirements.txt). */
|
|
113
|
+
const LOCKED_PYTHON = '3.13';
|
|
114
|
+
/**
|
|
115
|
+
* Installs a pinned package from its lock file in the launch folder (sandbox\addons\<id>) into `out`. npm: `npm ci`
|
|
116
|
+
* checks every tarball against the lock file's integrity, and runs no install scripts. uv: a relocatable venv (the
|
|
117
|
+
* folder moves into place afterwards) and `--require-hashes`, so every wheel must match its SHA-256 in the file; no
|
|
118
|
+
* source builds.
|
|
119
|
+
*/
|
|
120
|
+
async function installLocked(id, kind, out) {
|
|
121
|
+
const lock = path.join(launchDir, 'sandbox', 'addons', id);
|
|
122
|
+
if (kind === 'npm') {
|
|
123
|
+
for (const f of ['package.json', 'package-lock.json'])
|
|
124
|
+
fs.copyFileSync(path.join(lock, f), path.join(out, f));
|
|
125
|
+
const npmCli = path.join(path.dirname(process.execPath), 'node_modules', 'npm', 'bin', 'npm-cli.js');
|
|
126
|
+
await execAsync(process.execPath, [npmCli, 'ci', '--ignore-scripts', '--no-audit', '--no-fund', '--loglevel=error'], { cwd: out });
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
const uv = findExe(tool('uv'), 'uv.exe');
|
|
130
|
+
if (!uv)
|
|
131
|
+
throw new Error('uv is not installed (the uv step failed)');
|
|
132
|
+
const env = { ...process.env, ...uvVars() };
|
|
133
|
+
const venv = path.join(out, 'venv');
|
|
134
|
+
await execAsync(uv, ['venv', '--relocatable', '--python', LOCKED_PYTHON, '--python-preference', 'only-managed', venv], {
|
|
135
|
+
env,
|
|
136
|
+
});
|
|
137
|
+
await execAsync(uv, [
|
|
138
|
+
'pip',
|
|
139
|
+
'install',
|
|
140
|
+
'--python',
|
|
141
|
+
path.join(venv, 'Scripts', 'python.exe'),
|
|
142
|
+
'--require-hashes',
|
|
143
|
+
'--no-deps',
|
|
144
|
+
'--only-binary',
|
|
145
|
+
':all:',
|
|
146
|
+
'--link-mode',
|
|
147
|
+
'copy',
|
|
148
|
+
'-r',
|
|
149
|
+
path.join(lock, 'requirements.txt'),
|
|
150
|
+
], { env });
|
|
151
|
+
}
|
|
106
152
|
/** Installs apps.install and apps.custom (skipped ones are listed in the summary). */
|
|
107
153
|
export async function setupApps(env, apps, o) {
|
|
108
154
|
const { entries, skipped } = appsToInstall(apps, o.gitAccess);
|
package/dist/guest/apps/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Installs the apps you chose (apps.install from the catalogue, apps.custom of your own) into C:\AgentData\tools\apps.
|
|
2
2
|
// Installers are never run: archives are unpacked, NSIS installers opened with 7-Zip and MSIs extracted (msiexec /a),
|
|
3
3
|
// so nothing is registered and nothing is lost when the sandbox closes.
|
|
4
|
-
export { appsNeedMesa } from '../../shared/apps/index.js';
|
|
4
|
+
export { appsNeedMesa, appsNeedUv } from '../../shared/apps/index.js';
|
|
5
5
|
export { setupApps } from './app-installer.js';
|
|
6
6
|
export { findExe } from './find-exe.js';
|
|
@@ -9,7 +9,7 @@ export async function resolve(entry) {
|
|
|
9
9
|
}
|
|
10
10
|
if ('url' in s) {
|
|
11
11
|
const name = decodeURIComponent(new URL(s.url).pathname.split('/').pop() || entry.id);
|
|
12
|
-
return { url: s.url, fileName: /\.\w{2,5}$/.test(name) ? name : name + '.zip' };
|
|
12
|
+
return { url: s.url, fileName: /\.\w{2,5}$/.test(name) ? name : name + '.zip', sha256: s.sha256 };
|
|
13
13
|
}
|
|
14
14
|
if ('resolve' in s && RESOLVERS[s.resolve])
|
|
15
15
|
return RESOLVERS[s.resolve]();
|
|
@@ -1,7 +1,37 @@
|
|
|
1
1
|
/** Background programs of Windows itself: never what an agent ran. */
|
|
2
2
|
export declare const NOISE: Set<string>;
|
|
3
|
-
/**
|
|
4
|
-
export
|
|
3
|
+
/** A session's ptyhost: everything it starts (and what those start) is that session's. */
|
|
4
|
+
export interface SessionRoot {
|
|
5
|
+
readonly session: string;
|
|
6
|
+
readonly pid: number;
|
|
7
|
+
/** When the ptyhost started and ended (ms); a reused process id outside this window is not the session's. */
|
|
8
|
+
readonly from: number;
|
|
9
|
+
readonly to?: number;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Follows the process tree through the events themselves: a program belongs to a session when its parent is that
|
|
13
|
+
* session's ptyhost, or a program that belongs to it. Every new process id is recorded (as no session's when its
|
|
14
|
+
* parent is not one's), so an id Windows gives to another process later never inherits a session. A program whose
|
|
15
|
+
* parent was never seen is no session's: nothing is guessed.
|
|
16
|
+
*/
|
|
17
|
+
export declare class Attributor {
|
|
18
|
+
private readonly owner;
|
|
19
|
+
/** The session of a new process (pid, started by ppid at time t), or undefined. */
|
|
20
|
+
attribute(e: {
|
|
21
|
+
readonly pid: number;
|
|
22
|
+
readonly ppid: number;
|
|
23
|
+
readonly t: number;
|
|
24
|
+
}, roots: readonly SessionRoot[]): string | undefined;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* The events in wevtutil's XML output as audit lines, with the newest record id seen. With an attributor and the
|
|
28
|
+
* sessions' roots, each line says which session started it (s), when that is known. Every event goes through the
|
|
29
|
+
* attributor, Windows' own background programs too (they are left out of the lines only afterwards).
|
|
30
|
+
*/
|
|
31
|
+
export declare function parseProcessEvents(xml: string, attribution?: {
|
|
32
|
+
readonly attributor: Attributor;
|
|
33
|
+
readonly roots: readonly SessionRoot[];
|
|
34
|
+
}): {
|
|
5
35
|
lines: string[];
|
|
6
36
|
last: number;
|
|
7
37
|
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
// Reading Windows process-creation events (Security log, 4688, as wevtutil prints them) into audit lines
|
|
2
|
-
// the self-test can check it on the PC side.
|
|
1
|
+
// Reading Windows process-creation events (Security log, 4688, as wevtutil prints them) into audit lines, and which
|
|
2
|
+
// agent session each program belongs to. Pure, so the self-test can check it on the PC side.
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
/** Background programs of Windows itself: never what an agent ran. */
|
|
5
5
|
export const NOISE = new Set([
|
|
@@ -28,6 +28,29 @@ export const NOISE = new Set([
|
|
|
28
28
|
'usoclient.exe',
|
|
29
29
|
'musnotification.exe',
|
|
30
30
|
]);
|
|
31
|
+
/** Windows' clocks and ours differ by a moment: a root counts this long around its window. */
|
|
32
|
+
const ROOT_SLACK_MS = 5000;
|
|
33
|
+
/** Most process ids remembered (the oldest are forgotten first). */
|
|
34
|
+
const MAX_KNOWN = 50000;
|
|
35
|
+
/**
|
|
36
|
+
* Follows the process tree through the events themselves: a program belongs to a session when its parent is that
|
|
37
|
+
* session's ptyhost, or a program that belongs to it. Every new process id is recorded (as no session's when its
|
|
38
|
+
* parent is not one's), so an id Windows gives to another process later never inherits a session. A program whose
|
|
39
|
+
* parent was never seen is no session's: nothing is guessed.
|
|
40
|
+
*/
|
|
41
|
+
export class Attributor {
|
|
42
|
+
owner = new Map();
|
|
43
|
+
/** The session of a new process (pid, started by ppid at time t), or undefined. */
|
|
44
|
+
attribute(e, roots) {
|
|
45
|
+
const root = roots.find((r) => r.pid === e.ppid && e.t >= r.from - ROOT_SLACK_MS && (r.to === undefined || e.t <= r.to + ROOT_SLACK_MS));
|
|
46
|
+
const session = root?.session ?? this.owner.get(e.ppid) ?? undefined;
|
|
47
|
+
this.owner.delete(e.pid);
|
|
48
|
+
this.owner.set(e.pid, session ?? null);
|
|
49
|
+
if (this.owner.size > MAX_KNOWN)
|
|
50
|
+
this.owner.delete(this.owner.keys().next().value);
|
|
51
|
+
return session;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
31
54
|
const decode = (s) => s
|
|
32
55
|
.replace(/</g, '<')
|
|
33
56
|
.replace(/>/g, '>')
|
|
@@ -35,8 +58,17 @@ const decode = (s) => s
|
|
|
35
58
|
.replace(/'/g, "'")
|
|
36
59
|
.replace(/&/g, '&');
|
|
37
60
|
const field = (event, name) => decode(new RegExp("<Data Name='" + name + "'>([^<]*)</Data>").exec(event)?.[1] ?? '');
|
|
38
|
-
/**
|
|
39
|
-
|
|
61
|
+
/** A process id as the event writes it (0x1a2c). */
|
|
62
|
+
const pidOf = (text) => {
|
|
63
|
+
const n = /^0x[0-9a-f]{1,8}$/i.test(text) ? parseInt(text, 16) : /^\d{1,10}$/.test(text) ? Number(text) : 0;
|
|
64
|
+
return Number.isSafeInteger(n) ? n : 0;
|
|
65
|
+
};
|
|
66
|
+
/**
|
|
67
|
+
* The events in wevtutil's XML output as audit lines, with the newest record id seen. With an attributor and the
|
|
68
|
+
* sessions' roots, each line says which session started it (s), when that is known. Every event goes through the
|
|
69
|
+
* attributor, Windows' own background programs too (they are left out of the lines only afterwards).
|
|
70
|
+
*/
|
|
71
|
+
export function parseProcessEvents(xml, attribution) {
|
|
40
72
|
const lines = [];
|
|
41
73
|
let last = 0;
|
|
42
74
|
for (const event of xml.split('</Event>')) {
|
|
@@ -45,15 +77,21 @@ export function parseProcessEvents(xml) {
|
|
|
45
77
|
continue;
|
|
46
78
|
last = Math.max(last, id);
|
|
47
79
|
const program = field(event, 'NewProcessName');
|
|
80
|
+
const t = /SystemTime='([^']+)'/.exec(event)?.[1] ?? new Date().toISOString();
|
|
81
|
+
const pid = pidOf(field(event, 'NewProcessId'));
|
|
82
|
+
const ppid = pidOf(field(event, 'ProcessId'));
|
|
83
|
+
const session = attribution && pid && ppid
|
|
84
|
+
? attribution.attributor.attribute({ pid, ppid, t: Date.parse(t) || Date.now() }, attribution.roots)
|
|
85
|
+
: undefined;
|
|
48
86
|
if (!program || NOISE.has(path.win32.basename(program).toLowerCase()))
|
|
49
87
|
continue;
|
|
50
|
-
const t = /SystemTime='([^']+)'/.exec(event)?.[1] ?? new Date().toISOString();
|
|
51
88
|
lines.push(JSON.stringify({
|
|
52
89
|
t,
|
|
53
90
|
k: 'proc',
|
|
54
91
|
p: program.slice(0, 1024),
|
|
55
92
|
c: field(event, 'CommandLine').slice(0, 2000),
|
|
56
93
|
pp: field(event, 'ParentProcessName').slice(0, 1024),
|
|
94
|
+
...(session ? { s: session } : {}),
|
|
57
95
|
}));
|
|
58
96
|
}
|
|
59
97
|
return { lines, last };
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import { type SessionRoot } from './parse.js';
|
|
1
2
|
/** Turns on process-creation auditing with command lines (the registry is new every boot). */
|
|
2
3
|
export declare function enableProcessAudit(): void;
|
|
3
4
|
/**
|
|
4
5
|
* Reads new process events every 15 s for the daemon's life (from the moment it started: older ones are boot noise).
|
|
6
|
+
* `roots` gives the sessions' ptyhosts, so each program is linked to the session that started it (parse.ts).
|
|
5
7
|
*/
|
|
6
|
-
export declare function startProcessCollector(): void;
|
|
8
|
+
export declare function startProcessCollector(roots?: () => readonly SessionRoot[]): void;
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
// audit.processes, sandbox side: Windows records every program started (Security log, event 4688, with its command
|
|
2
|
-
// line); the daemon reads the new events every 15 s
|
|
3
|
-
//
|
|
4
|
-
//
|
|
2
|
+
// line and process ids); the daemon reads the new events every 15 s, links each to the agent session whose ptyhost
|
|
3
|
+
// started it (directly or further down), and leaves them in run\audit\ as small .jsonl files, which the host takes
|
|
4
|
+
// into its own log (and deletes). Windows' own background programs are left out. The host treats all of it as
|
|
5
|
+
// untrusted (it is checked line by line there, and a session it does not know is dropped).
|
|
5
6
|
import fs from 'node:fs';
|
|
6
7
|
import path from 'node:path';
|
|
7
8
|
import { spawnSync } from 'node:child_process';
|
|
8
9
|
import { SYSTEM } from '../../shared/system.js';
|
|
9
|
-
import { parseProcessEvents } from './parse.js';
|
|
10
|
+
import { Attributor, parseProcessEvents } from './parse.js';
|
|
10
11
|
import { dryRun, isWin, run } from '../runtime/paths.js';
|
|
11
12
|
import { log, warn } from '../runtime/log.js';
|
|
12
13
|
import { mkdirp } from '../runtime/fs.js';
|
|
@@ -44,11 +45,13 @@ const newestRecord = () => {
|
|
|
44
45
|
};
|
|
45
46
|
/**
|
|
46
47
|
* Reads new process events every 15 s for the daemon's life (from the moment it started: older ones are boot noise).
|
|
48
|
+
* `roots` gives the sessions' ptyhosts, so each program is linked to the session that started it (parse.ts).
|
|
47
49
|
*/
|
|
48
|
-
export function startProcessCollector() {
|
|
50
|
+
export function startProcessCollector(roots = () => []) {
|
|
49
51
|
if (!auditOn())
|
|
50
52
|
return;
|
|
51
53
|
let last = newestRecord();
|
|
54
|
+
const attributor = new Attributor();
|
|
52
55
|
const pass = () => {
|
|
53
56
|
try {
|
|
54
57
|
const r = spawnSync(wevtutil(), [
|
|
@@ -58,7 +61,7 @@ export function startProcessCollector() {
|
|
|
58
61
|
'/f:xml',
|
|
59
62
|
'/c:' + MAX_PER_PASS,
|
|
60
63
|
], { encoding: 'utf8', windowsHide: true, maxBuffer: 64 * 1024 * 1024 });
|
|
61
|
-
const { lines, last: newest } = parseProcessEvents(r.stdout ?? '');
|
|
64
|
+
const { lines, last: newest } = parseProcessEvents(r.stdout ?? '', { attributor, roots: roots() });
|
|
62
65
|
if (newest)
|
|
63
66
|
last = newest;
|
|
64
67
|
if (lines.length)
|
package/dist/guest/boot/boot.js
CHANGED
|
@@ -16,6 +16,8 @@ import { runHooks } from '../setup/hooks.js';
|
|
|
16
16
|
import { applyAllowOnly, proxyEnv } from '../network/allow-only.js';
|
|
17
17
|
import { applyNetworkPolicy, networkProblems } from '../network/firewall.js';
|
|
18
18
|
import { writeManagedSettings } from '../setup/managed-policy.js';
|
|
19
|
+
import { writeAddonMcpServers } from '../setup/addon-mcp.js';
|
|
20
|
+
import { startHeadroom } from '../setup/headroom.js';
|
|
19
21
|
import { applyPersonalization } from '../setup/desktop.js';
|
|
20
22
|
import { setupGitConfig } from '../setup/git-identity.js';
|
|
21
23
|
import { loginGithub, prepareGithubFolder } from '../setup/github-token.js';
|
|
@@ -144,7 +146,7 @@ export class Boot {
|
|
|
144
146
|
const { tools, env } = await this.developerTools();
|
|
145
147
|
await this.vpn();
|
|
146
148
|
await this.gitIdentity(env);
|
|
147
|
-
await this.claude(tools);
|
|
149
|
+
await this.claude(tools, env);
|
|
148
150
|
await this.agent(env);
|
|
149
151
|
this.preAgentHooks(env);
|
|
150
152
|
await this.start(env);
|
|
@@ -230,15 +232,22 @@ export class Boot {
|
|
|
230
232
|
await soft('GitHub token', () => loginGithub(token, env));
|
|
231
233
|
}
|
|
232
234
|
}
|
|
233
|
-
async claude(tools) {
|
|
235
|
+
async claude(tools, env) {
|
|
234
236
|
step(6, 'Claude settings, history and claude-mem');
|
|
235
237
|
if (session.harnessName === 'claude')
|
|
236
238
|
await soft('Claude settings', () => syncClaudeProfile(this.secrets.claudeCredentials));
|
|
237
239
|
await soft('claude-mem restore', restoreClaudeMem);
|
|
238
240
|
await soft('Agent files of the last sandbox', resetMachineBound);
|
|
239
241
|
await soft('Agent databases restore', restoreLocalState);
|
|
242
|
+
// Agent add-ons: MCP servers in the sandbox's Claude Code, and the Headroom proxy before the managed settings point
|
|
243
|
+
// Claude Code at it (the guide reads them back, so it comes last).
|
|
244
|
+
let headroom = null;
|
|
245
|
+
await soft('MCP servers from Apps', writeAddonMcpServers);
|
|
246
|
+
await soft('Headroom proxy', async () => {
|
|
247
|
+
headroom = await startHeadroom(env);
|
|
248
|
+
});
|
|
249
|
+
await soft('Claude managed settings', () => writeManagedSettings(this.gitAccess, headroom));
|
|
240
250
|
await soft('Sandbox guide for the agent', () => writeGuides(tools.summary, this.lan));
|
|
241
|
-
await soft('Claude managed settings', () => writeManagedSettings(this.gitAccess));
|
|
242
251
|
}
|
|
243
252
|
async agent(env) {
|
|
244
253
|
step(7, 'Agent: ' + session.harnessName);
|
|
@@ -3,7 +3,7 @@ export interface GuideProject {
|
|
|
3
3
|
guest: string;
|
|
4
4
|
host: string;
|
|
5
5
|
}
|
|
6
|
-
export declare function sandboxGuide(toolSummary: string[], lan?: 'blocked' | 'open' | 'offline', projects?: GuideProject[]): string;
|
|
6
|
+
export declare function sandboxGuide(toolSummary: string[], lan?: 'blocked' | 'open' | 'offline', projects?: GuideProject[], addons?: string[]): string;
|
|
7
7
|
/** Inserts or replaces the generated section, keeping whatever else the file contains (the host's CLAUDE.md). */
|
|
8
8
|
export declare function upsertGuide(file: string, guide: string): void;
|
|
9
9
|
/**
|
package/dist/guest/boot/guide.js
CHANGED
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
import fs from 'node:fs';
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import { claudeDir, session, stateDir, workspace } from '../runtime/session.js';
|
|
6
|
-
import { dataDir, launchDir, toolsDir } from '../runtime/paths.js';
|
|
6
|
+
import { dataDir, launchDir, logsDir, toolsDir } from '../runtime/paths.js';
|
|
7
|
+
import { addonGuideLine, APPS, headroomGuideLine, RTK_GUIDE_LINE } from '../../shared/apps/index.js';
|
|
8
|
+
import { installedAddon, installedRtk } from '../setup/agent-addons.js';
|
|
7
9
|
const BEGIN = '<!-- agent-sandbox:begin (generated every boot, do not edit) -->';
|
|
8
10
|
const END = '<!-- agent-sandbox:end -->';
|
|
9
11
|
/** Drives other than C: are subst drives onto C:\Mirror\<letter> (mapped folders shown at their PC paths). */
|
|
@@ -20,7 +22,7 @@ const mirrorLine = () => {
|
|
|
20
22
|
'`; real-path tools may print that form).',
|
|
21
23
|
];
|
|
22
24
|
};
|
|
23
|
-
export function sandboxGuide(toolSummary, lan = 'blocked', projects = []) {
|
|
25
|
+
export function sandboxGuide(toolSummary, lan = 'blocked', projects = [], addons = []) {
|
|
24
26
|
return [
|
|
25
27
|
BEGIN,
|
|
26
28
|
'## Sandbox manifest — you are in a DISPOSABLE Windows Sandbox',
|
|
@@ -45,6 +47,8 @@ export function sandboxGuide(toolSummary, lan = 'blocked', projects = []) {
|
|
|
45
47
|
'- RO: `' + launchDir + "` (this boot's scripts) · lost on close: everything else, incl. `C:\\nvm` (Node symlink)",
|
|
46
48
|
...mirrorLine(),
|
|
47
49
|
'- Tools: ' + toolSummary.join(' · '),
|
|
50
|
+
// Agent add-ons that change how the agent works (rtk, Headroom, MCP servers, ccusage).
|
|
51
|
+
...addons,
|
|
48
52
|
'- Persistent installs: `npm i -g` (per nvm version), `pip`, `cargo install`, `dotnet tool -g`, `uv tool`, `bun add -g`. Real installers must re-run each boot via a hook (ask the user).',
|
|
49
53
|
'- Mapped folders (workspace, C:\\AgentData) allow NO symlinks or junctions: pnpm runs hoisted (set), npm workspaces / `file:` deps and deno node_modules dirs fail there (use a folder on C:\\ outside them), bun cannot save its lockfile there (`bun install --no-save`).',
|
|
50
54
|
'- Network: ' +
|
|
@@ -75,8 +79,28 @@ export function upsertGuide(file, guide) {
|
|
|
75
79
|
export function managedClaudeMd() {
|
|
76
80
|
return path.join(process.env.AGENT_MANAGED_DIR ?? path.join(process.env.ProgramFiles ?? 'C:\\Program Files', 'ClaudeCode'), 'CLAUDE.md');
|
|
77
81
|
}
|
|
82
|
+
/** Whether the managed settings point Claude Code at the Headroom proxy (written just before the guide). */
|
|
83
|
+
const headroomRuns = () => {
|
|
84
|
+
try {
|
|
85
|
+
const file = path.join(path.dirname(managedClaudeMd()), 'managed-settings.json');
|
|
86
|
+
const settings = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
87
|
+
return typeof settings.env?.ANTHROPIC_BASE_URL === 'string';
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
return false; // no managed settings: nothing points at the proxy
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
/** The lines for the agent add-ons that are on and installed (rtk, Headroom, MCP servers, ccusage). */
|
|
94
|
+
function addonLines() {
|
|
95
|
+
const mcp = APPS.flatMap((a) => (a.mcp && installedAddon(a.id) ? [a.mcp.name] : []));
|
|
96
|
+
return [
|
|
97
|
+
...(installedRtk() ? [RTK_GUIDE_LINE] : []),
|
|
98
|
+
...(installedAddon('headroom') && headroomRuns() ? [headroomGuideLine(path.join(logsDir, 'headroom.log'))] : []),
|
|
99
|
+
...addonGuideLine(mcp, Boolean(installedAddon('ccusage'))),
|
|
100
|
+
];
|
|
101
|
+
}
|
|
78
102
|
export function writeGuides(toolSummary, lan, projects = []) {
|
|
79
|
-
const guide = sandboxGuide(toolSummary, lan, projects);
|
|
103
|
+
const guide = sandboxGuide(toolSummary, lan, projects, addonLines());
|
|
80
104
|
const managed = managedClaudeMd();
|
|
81
105
|
fs.mkdirSync(path.dirname(managed), { recursive: true });
|
|
82
106
|
fs.writeFileSync(managed, guide + '\n', 'utf8');
|
|
@@ -54,6 +54,8 @@ export class Daemon {
|
|
|
54
54
|
return execCommand(req);
|
|
55
55
|
case 'restart':
|
|
56
56
|
return sessions.restart(req);
|
|
57
|
+
case 'close':
|
|
58
|
+
return sessions.close(req);
|
|
57
59
|
case 'pressure':
|
|
58
60
|
// No response: the host repeats it every few seconds while its memory stays low.
|
|
59
61
|
return governor.hostPressure(req.seconds, req.reason, req.level ?? 'slow');
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
// A headless sandbox with nothing running (or starting) only wastes RAM, so it shuts itself down.
|
|
1
|
+
// A headless sandbox with nothing running (or starting) only wastes RAM, so it shuts itself down. Every open session
|
|
2
|
+
// counts, a plain shell as much as an agent: whether it is busy cannot be told from outside, and closing it says so.
|
|
2
3
|
import { spawnSync } from 'node:child_process';
|
|
3
4
|
import { SYSTEM } from '../../shared/system.js';
|
|
4
5
|
import { saveClaudeMem } from '../setup/claude-mem.js';
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { GuestRequest } from '../../core/types.js';
|
|
2
|
+
import type { SessionRoot } from '../audit/parse.js';
|
|
2
3
|
type SessionRequest = Extract<GuestRequest, {
|
|
3
4
|
kind: 'session';
|
|
4
5
|
}>;
|
|
@@ -14,9 +15,18 @@ export declare class SessionManager {
|
|
|
14
15
|
/** Sessions and projects being set up right now (a headless sandbox must not shut down under them). */
|
|
15
16
|
private starting;
|
|
16
17
|
private readonly idle;
|
|
18
|
+
/** Each session's ptyhost (process id and when it ran), for linking the programs it starts to it (audit). */
|
|
19
|
+
private readonly ptyHosts;
|
|
17
20
|
/** How many sessions are running. */
|
|
18
21
|
get count(): number;
|
|
19
|
-
/**
|
|
22
|
+
/** The sessions' ptyhosts, running or ended within ROOTS_KEPT_MS (audit events arrive after they happen). */
|
|
23
|
+
roots(): SessionRoot[];
|
|
24
|
+
private rootStarted;
|
|
25
|
+
private rootEnded;
|
|
26
|
+
/**
|
|
27
|
+
* `session` requests: an agent (or a plain shell) in its own ptyhost, in the launch folder's project or a registered
|
|
28
|
+
* one.
|
|
29
|
+
*/
|
|
20
30
|
start(req: SessionRequest): Promise<void>;
|
|
21
31
|
/**
|
|
22
32
|
* `project` requests: provision a registered project without an agent (sandbox start / exec / db from its folder).
|
|
@@ -24,10 +34,21 @@ export declare class SessionManager {
|
|
|
24
34
|
prepare(req: Extract<GuestRequest, {
|
|
25
35
|
kind: 'project';
|
|
26
36
|
}>): Promise<void>;
|
|
27
|
-
/**
|
|
37
|
+
/**
|
|
38
|
+
* `restart` requests: the matching sessions restart their agent (continuing the conversation where it can). Shells
|
|
39
|
+
* are left alone: restarting one would only throw away what it was doing.
|
|
40
|
+
*/
|
|
28
41
|
restart(req: Extract<GuestRequest, {
|
|
29
42
|
kind: 'restart';
|
|
30
43
|
}>): Promise<void>;
|
|
44
|
+
/**
|
|
45
|
+
* `close` requests: the session's ptyhost ends its program and everything it started (ProcessTree), then exits as
|
|
46
|
+
* if the program had. One that does not end in time has its ptyhost ended instead (closing the pseudo console ends
|
|
47
|
+
* what is attached to it).
|
|
48
|
+
*/
|
|
49
|
+
close(req: Extract<GuestRequest, {
|
|
50
|
+
kind: 'close';
|
|
51
|
+
}>): Promise<void>;
|
|
31
52
|
/**
|
|
32
53
|
* Finds out which of Claude's conversation files this session writes (it appears with the first message) and puts
|
|
33
54
|
* it in the status, so the host can reopen this very conversation. A reopened session starts with the one it was
|