windows-development-sandbox 0.0.0-stage → 1.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.sandbox-config.example.json +14 -0
- package/README.md +78 -2
- package/config.example.json +332 -0
- package/dist/api.d.ts +31 -0
- package/dist/api.js +30 -0
- package/dist/bin/agent-sandbox.d.ts +2 -0
- package/dist/bin/agent-sandbox.js +3 -0
- package/dist/bin/claude-host.d.ts +2 -0
- package/dist/bin/claude-host.js +5 -0
- package/dist/bin/claude-sbx.d.ts +2 -0
- package/dist/bin/claude-sbx.js +3 -0
- package/dist/bin/codex-sbx.d.ts +2 -0
- package/dist/bin/codex-sbx.js +3 -0
- package/dist/bin/gemini-sbx.d.ts +2 -0
- package/dist/bin/gemini-sbx.js +3 -0
- package/dist/bin/sandbox.d.ts +2 -0
- package/dist/bin/sandbox.js +4 -0
- package/dist/bin/sbx-hide.d.ts +2 -0
- package/dist/bin/sbx-hide.js +3 -0
- package/dist/bin/sbx-show.d.ts +2 -0
- package/dist/bin/sbx-show.js +3 -0
- package/dist/cli/args.d.ts +32 -0
- package/dist/cli/args.js +111 -0
- package/dist/cli/commands/accounts.d.ts +2 -0
- package/dist/cli/commands/accounts.js +52 -0
- package/dist/cli/commands/context.d.ts +21 -0
- package/dist/cli/commands/context.js +54 -0
- package/dist/cli/commands/data.d.ts +46 -0
- package/dist/cli/commands/data.js +249 -0
- package/dist/cli/commands/index.d.ts +8 -0
- package/dist/cli/commands/index.js +8 -0
- package/dist/cli/commands/launch.d.ts +5 -0
- package/dist/cli/commands/launch.js +152 -0
- package/dist/cli/commands/maintenance.d.ts +8 -0
- package/dist/cli/commands/maintenance.js +125 -0
- package/dist/cli/commands/repair.d.ts +59 -0
- package/dist/cli/commands/repair.js +332 -0
- package/dist/cli/commands/sessions.d.ts +11 -0
- package/dist/cli/commands/sessions.js +113 -0
- package/dist/cli/commands/setup.d.ts +45 -0
- package/dist/cli/commands/setup.js +152 -0
- package/dist/cli/index.d.ts +14 -0
- package/dist/cli/index.js +96 -0
- package/dist/cli/manifest.d.ts +95 -0
- package/dist/cli/manifest.js +87 -0
- package/dist/cli/menu.d.ts +22 -0
- package/dist/cli/menu.js +187 -0
- package/dist/cli/settings.d.ts +11 -0
- package/dist/cli/settings.js +156 -0
- package/dist/core/config.d.ts +17 -0
- package/dist/core/config.js +103 -0
- package/dist/core/detect-databases.d.ts +15 -0
- package/dist/core/detect-databases.js +147 -0
- package/dist/core/detect-pwsh.d.ts +18 -0
- package/dist/core/detect-pwsh.js +111 -0
- package/dist/core/detect-toolchains.d.ts +30 -0
- package/dist/core/detect-toolchains.js +58 -0
- package/dist/core/local-config.d.ts +12 -0
- package/dist/core/local-config.js +64 -0
- package/dist/core/schema.d.ts +604 -0
- package/dist/core/schema.js +264 -0
- package/dist/core/types.d.ts +162 -0
- package/dist/core/types.js +1 -0
- package/dist/core/util.d.ts +53 -0
- package/dist/core/util.js +145 -0
- package/dist/core/workspace.d.ts +23 -0
- package/dist/core/workspace.js +162 -0
- package/dist/guest/daemon.d.ts +1 -0
- package/dist/guest/daemon.js +186 -0
- package/dist/guest/db.d.ts +1 -0
- package/dist/guest/db.js +60 -0
- package/dist/guest/env.d.ts +49 -0
- package/dist/guest/env.js +99 -0
- package/dist/guest/gitguard.d.ts +7 -0
- package/dist/guest/gitguard.js +60 -0
- package/dist/guest/gitrules.d.ts +2 -0
- package/dist/guest/gitrules.js +11 -0
- package/dist/guest/guide.d.ts +12 -0
- package/dist/guest/guide.js +68 -0
- package/dist/guest/main.d.ts +1 -0
- package/dist/guest/main.js +120 -0
- package/dist/guest/ptyhost.d.ts +40 -0
- package/dist/guest/ptyhost.js +94 -0
- package/dist/guest/setup/agents.d.ts +8 -0
- package/dist/guest/setup/agents.js +73 -0
- package/dist/guest/setup/claude.d.ts +21 -0
- package/dist/guest/setup/claude.js +138 -0
- package/dist/guest/setup/environment.d.ts +6 -0
- package/dist/guest/setup/environment.js +65 -0
- package/dist/guest/setup/hooks.d.ts +1 -0
- package/dist/guest/setup/hooks.js +36 -0
- package/dist/guest/setup/network.d.ts +10 -0
- package/dist/guest/setup/network.js +99 -0
- package/dist/guest/setup/policy.d.ts +3 -0
- package/dist/guest/setup/policy.js +18 -0
- package/dist/guest/setup/user.d.ts +7 -0
- package/dist/guest/setup/user.js +47 -0
- package/dist/guest/toolchains/binstall.d.ts +7 -0
- package/dist/guest/toolchains/binstall.js +55 -0
- package/dist/guest/toolchains/catalogue.d.ts +51 -0
- package/dist/guest/toolchains/catalogue.js +31 -0
- package/dist/guest/toolchains/cli.d.ts +10 -0
- package/dist/guest/toolchains/cli.js +64 -0
- package/dist/guest/toolchains/common.d.ts +37 -0
- package/dist/guest/toolchains/common.js +124 -0
- package/dist/guest/toolchains/databases.d.ts +7 -0
- package/dist/guest/toolchains/databases.js +226 -0
- package/dist/guest/toolchains/git.d.ts +3 -0
- package/dist/guest/toolchains/git.js +112 -0
- package/dist/guest/toolchains/go.d.ts +3 -0
- package/dist/guest/toolchains/go.js +33 -0
- package/dist/guest/toolchains/index.d.ts +13 -0
- package/dist/guest/toolchains/index.js +122 -0
- package/dist/guest/toolchains/java.d.ts +8 -0
- package/dist/guest/toolchains/java.js +96 -0
- package/dist/guest/toolchains/managers.d.ts +16 -0
- package/dist/guest/toolchains/managers.js +82 -0
- package/dist/guest/toolchains/msvc.d.ts +10 -0
- package/dist/guest/toolchains/msvc.js +216 -0
- package/dist/guest/toolchains/native.d.ts +12 -0
- package/dist/guest/toolchains/native.js +220 -0
- package/dist/guest/toolchains/node.d.ts +3 -0
- package/dist/guest/toolchains/node.js +132 -0
- package/dist/guest/toolchains/pwsh.d.ts +4 -0
- package/dist/guest/toolchains/pwsh.js +14 -0
- package/dist/guest/toolchains/python.d.ts +3 -0
- package/dist/guest/toolchains/python.js +47 -0
- package/dist/guest/toolchains/runtimes.d.ts +17 -0
- package/dist/guest/toolchains/runtimes.js +40 -0
- package/dist/guest/toolchains/scripting.d.ts +10 -0
- package/dist/guest/toolchains/scripting.js +137 -0
- package/dist/guest/toolchains/versions.d.ts +47 -0
- package/dist/guest/toolchains/versions.js +77 -0
- package/dist/guest/toolchains/vscode.d.ts +2 -0
- package/dist/guest/toolchains/vscode.js +53 -0
- package/dist/guest/view.d.ts +1 -0
- package/dist/guest/view.js +22 -0
- package/dist/host/accounts.d.ts +32 -0
- package/dist/host/accounts.js +119 -0
- package/dist/host/channel.d.ts +37 -0
- package/dist/host/channel.js +180 -0
- package/dist/host/claude-profile.d.ts +15 -0
- package/dist/host/claude-profile.js +103 -0
- package/dist/host/cleanup.d.ts +23 -0
- package/dist/host/cleanup.js +66 -0
- package/dist/host/generate.d.ts +15 -0
- package/dist/host/generate.js +185 -0
- package/dist/host/personalize.d.ts +10 -0
- package/dist/host/personalize.js +97 -0
- package/dist/host/platform.d.ts +17 -0
- package/dist/host/platform.js +114 -0
- package/dist/host/project-scan.d.ts +23 -0
- package/dist/host/project-scan.js +173 -0
- package/dist/host/repo-guard.d.ts +23 -0
- package/dist/host/repo-guard.js +128 -0
- package/dist/host/secrets.d.ts +14 -0
- package/dist/host/secrets.js +133 -0
- package/dist/host/terminal.d.ts +32 -0
- package/dist/host/terminal.js +127 -0
- package/dist/host/window.d.ts +4 -0
- package/dist/host/window.js +68 -0
- package/dist/runtime/daemon.js +20291 -0
- package/dist/runtime/db.js +20423 -0
- package/dist/runtime/gitguard.js +84 -0
- package/dist/runtime/main.js +22398 -0
- package/dist/runtime/view.js +19966 -0
- package/dist/selftest/cli.test.d.ts +2 -0
- package/dist/selftest/cli.test.js +30 -0
- package/dist/selftest/config.test.d.ts +3 -0
- package/dist/selftest/config.test.js +74 -0
- package/dist/selftest/data.test.d.ts +2 -0
- package/dist/selftest/data.test.js +73 -0
- package/dist/selftest/detect.test.d.ts +2 -0
- package/dist/selftest/detect.test.js +42 -0
- package/dist/selftest/fixtures.d.ts +232 -0
- package/dist/selftest/fixtures.js +63 -0
- package/dist/selftest/guest.test.d.ts +3 -0
- package/dist/selftest/guest.test.js +61 -0
- package/dist/selftest/harness.d.ts +14 -0
- package/dist/selftest/harness.js +36 -0
- package/dist/selftest/helpers.test.d.ts +2 -0
- package/dist/selftest/helpers.test.js +125 -0
- package/dist/selftest/host.test.d.ts +2 -0
- package/dist/selftest/host.test.js +71 -0
- package/dist/selftest/index.d.ts +1 -0
- package/dist/selftest/index.js +56 -0
- package/dist/selftest/live.test.d.ts +7 -0
- package/dist/selftest/live.test.js +127 -0
- package/dist/selftest/mirror.test.d.ts +3 -0
- package/dist/selftest/mirror.test.js +74 -0
- package/dist/selftest/security.test.d.ts +3 -0
- package/dist/selftest/security.test.js +143 -0
- package/dist/selftest/setup.test.d.ts +2 -0
- package/dist/selftest/setup.test.js +23 -0
- package/dist/selftest/sync.test.d.ts +3 -0
- package/dist/selftest/sync.test.js +82 -0
- package/dist/shared/download.d.ts +58 -0
- package/dist/shared/download.js +200 -0
- package/dist/shared/guest-paths.d.ts +30 -0
- package/dist/shared/guest-paths.js +61 -0
- package/dist/shared/netrules.d.ts +18 -0
- package/dist/shared/netrules.js +98 -0
- package/dist/shared/paths.d.ts +9 -0
- package/dist/shared/paths.js +22 -0
- package/dist/shared/pty-client.d.ts +86 -0
- package/dist/shared/pty-client.js +227 -0
- package/dist/shared/rundir.d.ts +30 -0
- package/dist/shared/rundir.js +86 -0
- package/dist/shared/safe-remove.d.ts +10 -0
- package/dist/shared/safe-remove.js +55 -0
- package/dist/shared/schema.d.ts +84 -0
- package/dist/shared/schema.js +51 -0
- package/dist/shared/system.d.ts +15 -0
- package/dist/shared/system.js +19 -0
- package/dist/sync/claude-mem.d.ts +8 -0
- package/dist/sync/claude-mem.js +77 -0
- package/dist/sync/history.d.ts +30 -0
- package/dist/sync/history.js +129 -0
- package/dist/sync/watcher.d.ts +13 -0
- package/dist/sync/watcher.js +116 -0
- package/hooks/post-agent/README.txt +1 -0
- package/hooks/pre-provision/README.txt +1 -0
- package/package.json +91 -4
- package/sandbox/bootstrap.ps1 +258 -0
- package/sandbox/gitshim.cs +35 -0
- package/sandbox/logon.cmd +16 -0
- package/sandbox/ptyhost.cs +603 -0
- package/sandbox/trust.cmd +35 -0
- package/sandbox/vswhere.cs +83 -0
- package/schemas/config.schema.json +1062 -0
- package/schemas/sandbox-config.schema.json +952 -0
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// The one way this tool deletes a folder tree. A recursive delete that resolved to the wrong place (an empty
|
|
2
|
+
// variable, a relative path, a planted link) must never be able to take a user profile, a drive or Windows with it.
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
const norm = (p) => path.resolve(p).replace(/[\\/]+$/, '').toLowerCase();
|
|
7
|
+
const isAncestorOrSame = (parent, child) => { const a = norm(parent); const b = norm(child); return a === b || b.startsWith(a + path.sep); };
|
|
8
|
+
/** Folders no delete may ever reach, or contain: profiles, Windows, program folders. */
|
|
9
|
+
const protectedFolders = () => [
|
|
10
|
+
os.homedir(), process.env.USERPROFILE, process.env.APPDATA, process.env.LOCALAPPDATA, path.join(os.homedir(), 'Documents'), path.join(os.homedir(), 'Desktop'),
|
|
11
|
+
process.env.SystemRoot, process.env.WINDIR, process.env.ProgramFiles, process.env['ProgramFiles(x86)'], process.env.ProgramData, 'C:\\Users', 'C:\\Windows',
|
|
12
|
+
].filter((p) => typeof p === 'string' && p.trim() !== '');
|
|
13
|
+
/**
|
|
14
|
+
* Why `target` may not be deleted, or null when it may. Refused: a relative path, a drive root, a protected folder or
|
|
15
|
+
* any folder that contains one, and anything that is not strictly inside `root`.
|
|
16
|
+
*/
|
|
17
|
+
export const removeRefusal = (target, root) => {
|
|
18
|
+
if (!target || !path.isAbsolute(target) || !root || !path.isAbsolute(root))
|
|
19
|
+
return 'not an absolute path';
|
|
20
|
+
const t = norm(target);
|
|
21
|
+
if (t === norm(path.parse(t).root))
|
|
22
|
+
return 'a drive root';
|
|
23
|
+
const hit = protectedFolders().find((p) => isAncestorOrSame(t, p));
|
|
24
|
+
if (hit)
|
|
25
|
+
return 'it is, or contains, ' + hit;
|
|
26
|
+
if (!t.startsWith(norm(root) + path.sep))
|
|
27
|
+
return 'it is not inside ' + root;
|
|
28
|
+
return null;
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* Deletes `target` (a folder tree or file) only when it is strictly inside `root` and nothing protected; a link is
|
|
32
|
+
* removed itself, never followed. Throws instead of deleting anything else.
|
|
33
|
+
*/
|
|
34
|
+
export const safeRemove = (target, root) => {
|
|
35
|
+
const refusal = removeRefusal(target, root);
|
|
36
|
+
if (refusal)
|
|
37
|
+
throw new Error('Refusing to delete ' + target + ': ' + refusal);
|
|
38
|
+
let stat;
|
|
39
|
+
try {
|
|
40
|
+
stat = fs.lstatSync(target);
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
if (stat.isSymbolicLink()) {
|
|
46
|
+
try {
|
|
47
|
+
fs.unlinkSync(target);
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
fs.rmdirSync(target);
|
|
51
|
+
}
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
fs.rmSync(target, { recursive: true, force: true, maxRetries: 2 });
|
|
55
|
+
};
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export declare const Id: z.ZodString;
|
|
3
|
+
export declare const AttachEndpoint: z.ZodObject<{
|
|
4
|
+
addresses: z.ZodArray<z.ZodString>;
|
|
5
|
+
port: z.ZodNumber;
|
|
6
|
+
token: z.ZodString;
|
|
7
|
+
}, z.core.$strict>;
|
|
8
|
+
/** C:\AgentData\run\sandbox.json: boot progress of the sandbox as a whole. */
|
|
9
|
+
export declare const SandboxStatus: z.ZodObject<{
|
|
10
|
+
launchId: z.ZodString;
|
|
11
|
+
phase: z.ZodEnum<{
|
|
12
|
+
provisioning: "provisioning";
|
|
13
|
+
booting: "booting";
|
|
14
|
+
ready: "ready";
|
|
15
|
+
failed: "failed";
|
|
16
|
+
}>;
|
|
17
|
+
message: z.ZodString;
|
|
18
|
+
updatedAt: z.ZodString;
|
|
19
|
+
workspaceHost: z.ZodString;
|
|
20
|
+
guestWorkspace: z.ZodString;
|
|
21
|
+
headless: z.ZodBoolean;
|
|
22
|
+
}, z.core.$strip>;
|
|
23
|
+
/** C:\AgentData\run\requests\<id>.json: the ONLY things the host can ask the sandbox to do. */
|
|
24
|
+
export declare const GuestRequest: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
25
|
+
kind: z.ZodLiteral<"session">;
|
|
26
|
+
id: z.ZodString;
|
|
27
|
+
harness: z.ZodString;
|
|
28
|
+
args: z.ZodArray<z.ZodString>;
|
|
29
|
+
cwd: z.ZodString;
|
|
30
|
+
createdAt: z.ZodString;
|
|
31
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
32
|
+
kind: z.ZodLiteral<"exec">;
|
|
33
|
+
id: z.ZodString;
|
|
34
|
+
command: z.ZodArray<z.ZodString>;
|
|
35
|
+
cwd: z.ZodString;
|
|
36
|
+
timeoutSeconds: z.ZodNumber;
|
|
37
|
+
createdAt: z.ZodString;
|
|
38
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
39
|
+
kind: z.ZodLiteral<"restart">;
|
|
40
|
+
id: z.ZodString;
|
|
41
|
+
harness: z.ZodNullable<z.ZodString>;
|
|
42
|
+
createdAt: z.ZodString;
|
|
43
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
44
|
+
kind: z.ZodLiteral<"ping">;
|
|
45
|
+
id: z.ZodString;
|
|
46
|
+
createdAt: z.ZodString;
|
|
47
|
+
}, z.core.$strict>], "kind">;
|
|
48
|
+
/** C:\AgentData\run\responses\<id>.json: the guest's reply. */
|
|
49
|
+
export declare const GuestResponse: z.ZodObject<{
|
|
50
|
+
id: z.ZodString;
|
|
51
|
+
ok: z.ZodBoolean;
|
|
52
|
+
message: z.ZodString;
|
|
53
|
+
exitCode: z.ZodOptional<z.ZodNumber>;
|
|
54
|
+
stdout: z.ZodOptional<z.ZodString>;
|
|
55
|
+
stderr: z.ZodOptional<z.ZodString>;
|
|
56
|
+
finishedAt: z.ZodString;
|
|
57
|
+
}, z.core.$strip>;
|
|
58
|
+
/** C:\AgentData\run\sessions\<id>.json: one agent session (its own pseudo console and terminal tab). */
|
|
59
|
+
export declare const SessionStatus: z.ZodObject<{
|
|
60
|
+
id: z.ZodString;
|
|
61
|
+
launchId: z.ZodString;
|
|
62
|
+
harness: z.ZodString;
|
|
63
|
+
cwd: z.ZodString;
|
|
64
|
+
phase: z.ZodEnum<{
|
|
65
|
+
failed: "failed";
|
|
66
|
+
starting: "starting";
|
|
67
|
+
running: "running";
|
|
68
|
+
exited: "exited";
|
|
69
|
+
}>;
|
|
70
|
+
message: z.ZodString;
|
|
71
|
+
updatedAt: z.ZodString;
|
|
72
|
+
startedAt: z.ZodString;
|
|
73
|
+
exitCode: z.ZodOptional<z.ZodNumber>;
|
|
74
|
+
attach: z.ZodOptional<z.ZodObject<{
|
|
75
|
+
addresses: z.ZodArray<z.ZodString>;
|
|
76
|
+
port: z.ZodNumber;
|
|
77
|
+
token: z.ZodString;
|
|
78
|
+
}, z.core.$strict>>;
|
|
79
|
+
}, z.core.$strip>;
|
|
80
|
+
export type AttachEndpointT = z.output<typeof AttachEndpoint>;
|
|
81
|
+
export type SandboxStatusT = z.output<typeof SandboxStatus>;
|
|
82
|
+
export type GuestRequestT = z.output<typeof GuestRequest>;
|
|
83
|
+
export type GuestResponseT = z.output<typeof GuestResponse>;
|
|
84
|
+
export type SessionStatusT = z.output<typeof SessionStatus>;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// The host <-> sandbox channel messages, defined once with zod. Both sides parse what they read: the guest only acts on
|
|
2
|
+
// these request kinds, and the host treats everything the guest writes as untrusted (shapes checked, sizes capped).
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
export const Id = z.string().regex(/^[A-Za-z0-9_-]{1,64}$/, 'invalid id');
|
|
5
|
+
const Text = (max = 4096) => z.string().max(max);
|
|
6
|
+
const Time = Text(64);
|
|
7
|
+
export const AttachEndpoint = z.strictObject({
|
|
8
|
+
addresses: z.array(Text(64)).max(16),
|
|
9
|
+
port: z.number().int().min(1024).max(65535),
|
|
10
|
+
token: Text(256),
|
|
11
|
+
});
|
|
12
|
+
/** C:\AgentData\run\sandbox.json: boot progress of the sandbox as a whole. */
|
|
13
|
+
export const SandboxStatus = z.object({
|
|
14
|
+
launchId: Text(64),
|
|
15
|
+
phase: z.enum(['booting', 'provisioning', 'ready', 'failed']),
|
|
16
|
+
message: Text(),
|
|
17
|
+
updatedAt: Time,
|
|
18
|
+
workspaceHost: Text(),
|
|
19
|
+
guestWorkspace: Text(),
|
|
20
|
+
headless: z.boolean(),
|
|
21
|
+
});
|
|
22
|
+
/** C:\AgentData\run\requests\<id>.json: the ONLY things the host can ask the sandbox to do. */
|
|
23
|
+
export const GuestRequest = z.discriminatedUnion('kind', [
|
|
24
|
+
z.strictObject({ kind: z.literal('session'), id: Id, harness: Text(64), args: z.array(Text()).max(256), cwd: Text(), createdAt: Time }),
|
|
25
|
+
z.strictObject({ kind: z.literal('exec'), id: Id, command: z.array(Text()).min(1).max(256), cwd: Text(), timeoutSeconds: z.number().positive().max(86400), createdAt: Time }),
|
|
26
|
+
z.strictObject({ kind: z.literal('restart'), id: Id, harness: Text(64).nullable(), createdAt: Time }),
|
|
27
|
+
z.strictObject({ kind: z.literal('ping'), id: Id, createdAt: Time }),
|
|
28
|
+
]);
|
|
29
|
+
/** C:\AgentData\run\responses\<id>.json: the guest's reply. */
|
|
30
|
+
export const GuestResponse = z.object({
|
|
31
|
+
id: Id,
|
|
32
|
+
ok: z.boolean(),
|
|
33
|
+
message: Text(65536),
|
|
34
|
+
exitCode: z.number().int().optional(),
|
|
35
|
+
stdout: Text(4 * 1024 * 1024).optional(),
|
|
36
|
+
stderr: Text(4 * 1024 * 1024).optional(),
|
|
37
|
+
finishedAt: Time,
|
|
38
|
+
});
|
|
39
|
+
/** C:\AgentData\run\sessions\<id>.json: one agent session (its own pseudo console and terminal tab). */
|
|
40
|
+
export const SessionStatus = z.object({
|
|
41
|
+
id: Id,
|
|
42
|
+
launchId: Text(64),
|
|
43
|
+
harness: Text(64),
|
|
44
|
+
cwd: Text(),
|
|
45
|
+
phase: z.enum(['starting', 'running', 'exited', 'failed']),
|
|
46
|
+
message: Text(),
|
|
47
|
+
updatedAt: Time,
|
|
48
|
+
startedAt: Time,
|
|
49
|
+
exitCode: z.number().int().optional(),
|
|
50
|
+
attach: AttachEndpoint.optional(),
|
|
51
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export declare const system32: (exe: string) => string;
|
|
2
|
+
export declare const SYSTEM: {
|
|
3
|
+
readonly cmd: () => string;
|
|
4
|
+
readonly powershell: () => string;
|
|
5
|
+
readonly tasklist: () => string;
|
|
6
|
+
readonly tar: () => string;
|
|
7
|
+
readonly reg: () => string;
|
|
8
|
+
readonly netsh: () => string;
|
|
9
|
+
readonly netstat: () => string;
|
|
10
|
+
readonly route: () => string;
|
|
11
|
+
readonly ipconfig: () => string;
|
|
12
|
+
readonly robocopy: () => string;
|
|
13
|
+
readonly shutdown: () => string;
|
|
14
|
+
readonly dism: () => string;
|
|
15
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// Absolute paths for the Windows programs agent-sandbox runs. Never spawn them by bare name: Windows looks in the
|
|
2
|
+
// current folder before PATH, and the current folder is often the workspace, which the sandbox can write.
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
const systemRoot = () => process.env.SystemRoot ?? process.env.WINDIR ?? 'C:\\Windows';
|
|
5
|
+
export const system32 = (exe) => path.join(systemRoot(), 'System32', exe);
|
|
6
|
+
export const SYSTEM = {
|
|
7
|
+
cmd: () => system32('cmd.exe'),
|
|
8
|
+
powershell: () => system32('WindowsPowerShell\\v1.0\\powershell.exe'),
|
|
9
|
+
tasklist: () => system32('tasklist.exe'),
|
|
10
|
+
tar: () => system32('tar.exe'),
|
|
11
|
+
reg: () => system32('reg.exe'),
|
|
12
|
+
netsh: () => system32('netsh.exe'),
|
|
13
|
+
netstat: () => system32('NETSTAT.EXE'),
|
|
14
|
+
route: () => system32('ROUTE.EXE'),
|
|
15
|
+
ipconfig: () => system32('ipconfig.exe'),
|
|
16
|
+
robocopy: () => system32('Robocopy.exe'),
|
|
17
|
+
shutdown: () => system32('shutdown.exe'),
|
|
18
|
+
dism: () => system32('Dism.exe'),
|
|
19
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { SyncState } from './history.js';
|
|
2
|
+
export type MemResult = 'none' | 'copied-in' | 'copied-out' | 'unchanged' | 'conflict';
|
|
3
|
+
export declare const hostClaudeMemDir: () => string;
|
|
4
|
+
/**
|
|
5
|
+
* Direction is decided by which side's database changed since the last sync. If BOTH changed, the side about to be
|
|
6
|
+
* overwritten is kept as `<dir>.conflict-<time>` first, so nothing is ever lost.
|
|
7
|
+
*/
|
|
8
|
+
export declare const syncClaudeMem: (hostDir: string, store: string, state: SyncState, direction: "in" | "out") => MemResult;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
// claude-mem data (~/.claude-mem) between this PC and the sandbox's persistent store.
|
|
2
|
+
//
|
|
3
|
+
// IN (this PC -> sandbox): the whole folder is mirrored; nothing flows to your PC in that direction.
|
|
4
|
+
// OUT (sandbox -> this PC): ONLY the SQLite database files come back. Never settings, never the Chroma vector store:
|
|
5
|
+
// Chroma keeps Python pickle files, and a crafted pickle runs code when loaded, so copying it back would let a
|
|
6
|
+
// compromised sandbox execute code on your PC. claude-mem rebuilds its vector index from the database by itself.
|
|
7
|
+
import fs from 'node:fs';
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
import _ from 'lodash';
|
|
10
|
+
import { dirExists, homeDir, isLink } from '../core/util.js';
|
|
11
|
+
import { safeRemove } from '../shared/safe-remove.js';
|
|
12
|
+
const SKIP_IN = /(^logs$|\.pid$|\.port$|\.lock$|^worker.*\.json$|\.incoming$|\.conflict-\d+$)/i;
|
|
13
|
+
/** The only files that may come back from the sandbox. */
|
|
14
|
+
const DATABASE = /^claude-mem\.db(-wal|-shm)?$/i;
|
|
15
|
+
const MAX_DB_BYTES = 2 * 1024 * 1024 * 1024;
|
|
16
|
+
export const hostClaudeMemDir = () => process.env.CLAUDE_MEM_DATA_DIR ?? path.join(homeDir(), '.claude-mem');
|
|
17
|
+
/** Plain database files at the top of a claude-mem folder (links and anything else are ignored). */
|
|
18
|
+
const databaseFiles = (dir) => dirExists(dir) && !isLink(dir) ? fs.readdirSync(dir).filter((f) => DATABASE.test(f) && fs.lstatSync(path.join(dir, f)).isFile()) : [];
|
|
19
|
+
/** Newest database file time: the only thing that matters for deciding the direction. */
|
|
20
|
+
const stampOf = (dir) => _.max(databaseFiles(dir).map((f) => fs.statSync(path.join(dir, f)).mtimeMs)) ?? 0;
|
|
21
|
+
/** Host -> sandbox: replace the sandbox store with a copy of this PC's folder (staging folder, then swap). */
|
|
22
|
+
const mirrorIn = (hostDir, store) => {
|
|
23
|
+
const incoming = store + '.incoming';
|
|
24
|
+
safeRemove(incoming, path.dirname(store));
|
|
25
|
+
fs.cpSync(hostDir, incoming, { recursive: true, filter: (src) => src === hostDir || !SKIP_IN.test(path.basename(src)) });
|
|
26
|
+
safeRemove(store, path.dirname(store));
|
|
27
|
+
fs.renameSync(incoming, store);
|
|
28
|
+
};
|
|
29
|
+
/** Sandbox -> host: copy only the database files, each via a temp file + rename; everything else on this PC stays. */
|
|
30
|
+
const copyDatabaseOut = (store, hostDir) => {
|
|
31
|
+
fs.mkdirSync(hostDir, { recursive: true });
|
|
32
|
+
for (const name of databaseFiles(store)) {
|
|
33
|
+
const source = path.join(store, name);
|
|
34
|
+
if (fs.statSync(source).size > MAX_DB_BYTES)
|
|
35
|
+
throw new Error('claude-mem database in the sandbox is suspiciously large; not copied back: ' + source);
|
|
36
|
+
const temp = path.join(hostDir, name + '.from-sandbox');
|
|
37
|
+
fs.copyFileSync(source, temp);
|
|
38
|
+
fs.renameSync(temp, path.join(hostDir, name));
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
/** Keeps a copy of this PC's databases before they are replaced after both sides changed. */
|
|
42
|
+
const parkHostDatabases = (hostDir) => {
|
|
43
|
+
const parked = hostDir + '.conflict-' + Date.now();
|
|
44
|
+
fs.mkdirSync(parked, { recursive: true });
|
|
45
|
+
for (const name of databaseFiles(hostDir))
|
|
46
|
+
fs.copyFileSync(path.join(hostDir, name), path.join(parked, name));
|
|
47
|
+
};
|
|
48
|
+
/**
|
|
49
|
+
* Direction is decided by which side's database changed since the last sync. If BOTH changed, the side about to be
|
|
50
|
+
* overwritten is kept as `<dir>.conflict-<time>` first, so nothing is ever lost.
|
|
51
|
+
*/
|
|
52
|
+
export const syncClaudeMem = (hostDir, store, state, direction) => {
|
|
53
|
+
if (isLink(store))
|
|
54
|
+
throw new Error('Refusing to sync claude-mem: ' + store + ' is a link.');
|
|
55
|
+
const hostStamp = stampOf(hostDir);
|
|
56
|
+
const guestStamp = stampOf(store);
|
|
57
|
+
if (hostStamp === 0 && guestStamp === 0 && !dirExists(hostDir))
|
|
58
|
+
return 'none';
|
|
59
|
+
const hostChanged = hostStamp > (state.memHostStamp ?? 0) + 1;
|
|
60
|
+
const guestChanged = state.memGuestStamp !== undefined && guestStamp > state.memGuestStamp + 1;
|
|
61
|
+
let result = 'unchanged';
|
|
62
|
+
if (direction === 'in' && dirExists(hostDir) && (hostChanged || guestStamp === 0)) {
|
|
63
|
+
if (guestChanged)
|
|
64
|
+
fs.cpSync(store, store + '.conflict-' + Date.now(), { recursive: true });
|
|
65
|
+
mirrorIn(hostDir, store);
|
|
66
|
+
result = guestChanged ? 'conflict' : 'copied-in';
|
|
67
|
+
}
|
|
68
|
+
else if (direction === 'out' && guestChanged) {
|
|
69
|
+
if (hostChanged)
|
|
70
|
+
parkHostDatabases(hostDir);
|
|
71
|
+
copyDatabaseOut(store, hostDir);
|
|
72
|
+
result = hostChanged ? 'conflict' : 'copied-out';
|
|
73
|
+
}
|
|
74
|
+
state.memHostStamp = stampOf(hostDir);
|
|
75
|
+
state.memGuestStamp = stampOf(store);
|
|
76
|
+
return result;
|
|
77
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { projectFolderName, translatePaths } from '../shared/paths.js';
|
|
2
|
+
export { projectFolderName, translatePaths };
|
|
3
|
+
/** Host-only record of the last launch: which folders map to each other and what syncs (from the effective config). */
|
|
4
|
+
export interface SyncState {
|
|
5
|
+
hostClaudeDir: string;
|
|
6
|
+
hostWorkspace: string;
|
|
7
|
+
guestWorkspace: string;
|
|
8
|
+
syncHistory?: boolean;
|
|
9
|
+
syncClaudeMem?: boolean;
|
|
10
|
+
/** Newest claude-mem database time on each side right after the last sync (detects edits on both sides). */
|
|
11
|
+
memHostStamp?: number;
|
|
12
|
+
memGuestStamp?: number;
|
|
13
|
+
}
|
|
14
|
+
export interface SyncPaths {
|
|
15
|
+
hostClaudeDir: string;
|
|
16
|
+
guestClaudeDir: string;
|
|
17
|
+
hostWorkspace: string;
|
|
18
|
+
guestWorkspace: string;
|
|
19
|
+
}
|
|
20
|
+
/** Brings this PC's history into the sandbox state (run before the sandbox starts). Returns files copied. */
|
|
21
|
+
export declare const syncHistoryIn: (p: SyncPaths) => number;
|
|
22
|
+
/** Copies new sandbox history back to this PC (safe while the sandbox runs: transcripts are append-only). */
|
|
23
|
+
export declare const syncHistoryOut: (p: SyncPaths) => number;
|
|
24
|
+
/**
|
|
25
|
+
* Host-only: <storage>\sync\<dataName>.json. It records which folder on this PC history flows back into, so it must
|
|
26
|
+
* never live in the data folder the sandbox can write (a forged copy could redirect writes anywhere on this PC).
|
|
27
|
+
*/
|
|
28
|
+
export declare const syncStatePath: (dataPath: string) => string;
|
|
29
|
+
export declare const readSyncState: (dataPath: string) => SyncState | null;
|
|
30
|
+
export declare const writeSyncState: (dataPath: string, state: SyncState) => void;
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
// Two-way sync of Claude Code history between this PC and the sandbox's persistent state.
|
|
2
|
+
//
|
|
3
|
+
// Claude keeps transcripts per working folder (~/.claude/projects/<folder with every non-alphanumeric char as '-'>)
|
|
4
|
+
// and writes absolute paths inside them. The sandbox sees the workspace under a different path, so the current
|
|
5
|
+
// project's folder is renamed and paths inside its files are translated in both directions. Everything else is
|
|
6
|
+
// copied as-is, newest file wins, and modification times are preserved so repeated syncs are cheap.
|
|
7
|
+
//
|
|
8
|
+
// Safety (the sandbox side is untrusted):
|
|
9
|
+
// - links are never followed or written through, on either side;
|
|
10
|
+
// - what comes BACK is limited to transcripts (.jsonl), prompt history and to-do lists (.json), each capped in
|
|
11
|
+
// size, and never Claude's memory folders. Nothing executable or instruction-like flows to this PC.
|
|
12
|
+
import fs from 'node:fs';
|
|
13
|
+
import path from 'node:path';
|
|
14
|
+
import _ from 'lodash';
|
|
15
|
+
import { guestWorkspaceAliases, projectFolderName, translatePaths } from '../shared/paths.js';
|
|
16
|
+
import { dirExists, ensureDir, fileExists, isLink, removeLink, tryReadJson, writeJson } from '../core/util.js';
|
|
17
|
+
export { projectFolderName, translatePaths };
|
|
18
|
+
const SESSION_DIRS = ['todos', 'plans', 'file-history'];
|
|
19
|
+
const DATA_FILE = /\.(jsonl?|txt|md)$/i;
|
|
20
|
+
const MAX_FILE_BYTES = 100 * 1024 * 1024;
|
|
21
|
+
const anyFile = () => true;
|
|
22
|
+
const transcript = (name) => name.endsWith('.jsonl');
|
|
23
|
+
const json = (name) => name.endsWith('.json');
|
|
24
|
+
/** Claude's auto-memory is loaded into every host session, so the sandbox must never write it. */
|
|
25
|
+
const NO_MEMORY = /^memory$/i;
|
|
26
|
+
const newerThan = (source, destination) => {
|
|
27
|
+
try {
|
|
28
|
+
return source.mtimeMs > fs.statSync(destination).mtimeMs + 1;
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
/** Writes a file, replacing a link at that path instead of writing through it. */
|
|
35
|
+
const writeSafely = (destination, write) => {
|
|
36
|
+
if (isLink(destination))
|
|
37
|
+
removeLink(destination);
|
|
38
|
+
write();
|
|
39
|
+
};
|
|
40
|
+
/** Copies files that are newer in `from`, optionally translating text content, and keeps the source mtime. */
|
|
41
|
+
const copyNewer = (from, to, options = {}) => {
|
|
42
|
+
if (!dirExists(from) || isLink(from) || isLink(to))
|
|
43
|
+
return 0;
|
|
44
|
+
const accept = options.accept ?? anyFile;
|
|
45
|
+
return _.sumBy(fs.readdirSync(from, { withFileTypes: true }), (entry) => {
|
|
46
|
+
const source = path.join(from, entry.name);
|
|
47
|
+
const destination = path.join(to, entry.name);
|
|
48
|
+
if (entry.isDirectory())
|
|
49
|
+
return options.skipDirs?.test(entry.name) ? 0 : copyNewer(source, destination, options);
|
|
50
|
+
if (!entry.isFile() || !accept(entry.name))
|
|
51
|
+
return 0;
|
|
52
|
+
const stat = fs.statSync(source);
|
|
53
|
+
if (stat.size > MAX_FILE_BYTES || !newerThan(stat, destination))
|
|
54
|
+
return 0;
|
|
55
|
+
ensureDir(to);
|
|
56
|
+
writeSafely(destination, () => {
|
|
57
|
+
if (!options.transform || !DATA_FILE.test(entry.name)) {
|
|
58
|
+
fs.copyFileSync(source, destination);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
let text = fs.readFileSync(source, 'utf8');
|
|
62
|
+
// A transcript can be mid-write; only take complete lines, the rest arrives with the next sync.
|
|
63
|
+
if (entry.name.endsWith('.jsonl') && !text.endsWith('\n'))
|
|
64
|
+
text = text.slice(0, text.lastIndexOf('\n') + 1);
|
|
65
|
+
fs.writeFileSync(destination, options.transform(text), 'utf8');
|
|
66
|
+
});
|
|
67
|
+
fs.utimesSync(destination, stat.atime, stat.mtime);
|
|
68
|
+
return 1;
|
|
69
|
+
});
|
|
70
|
+
};
|
|
71
|
+
const readLines = (file) => fileExists(file) && !isLink(file) && fs.statSync(file).size <= MAX_FILE_BYTES ? fs.readFileSync(file, 'utf8').split(/\r?\n/).filter((l) => l.trim() !== '') : [];
|
|
72
|
+
const lineTime = (line) => { try {
|
|
73
|
+
return Number(JSON.parse(line).timestamp) || 0;
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
return 0;
|
|
77
|
+
} };
|
|
78
|
+
/** Brings this PC's history into the sandbox state (run before the sandbox starts). Returns files copied. */
|
|
79
|
+
export const syncHistoryIn = (p) => {
|
|
80
|
+
const hostProject = projectFolderName(p.hostWorkspace);
|
|
81
|
+
const guestProject = projectFolderName(p.guestWorkspace);
|
|
82
|
+
const transform = (t) => translatePaths(t, p.hostWorkspace, p.guestWorkspace);
|
|
83
|
+
const hostProjects = path.join(p.hostClaudeDir, 'projects');
|
|
84
|
+
const names = dirExists(hostProjects) ? fs.readdirSync(hostProjects).filter((name) => name !== guestProject || hostProject === guestProject) : [];
|
|
85
|
+
const projects = _.sumBy(names, (name) => name === hostProject
|
|
86
|
+
? copyNewer(path.join(hostProjects, name), path.join(p.guestClaudeDir, 'projects', guestProject), { transform })
|
|
87
|
+
: copyNewer(path.join(hostProjects, name), path.join(p.guestClaudeDir, 'projects', name)));
|
|
88
|
+
const sessions = _.sumBy(SESSION_DIRS, (dir) => copyNewer(path.join(p.hostClaudeDir, dir), path.join(p.guestClaudeDir, dir)));
|
|
89
|
+
// Prompt history: rebuild the sandbox copy from this PC's file plus anything only the sandbox has.
|
|
90
|
+
const target = path.join(p.guestClaudeDir, 'history.jsonl');
|
|
91
|
+
const hostLines = readLines(path.join(p.hostClaudeDir, 'history.jsonl')).map(transform);
|
|
92
|
+
const known = new Set(hostLines);
|
|
93
|
+
const merged = _.sortBy([...hostLines, ...readLines(target).filter((l) => !known.has(l))], lineTime);
|
|
94
|
+
if (merged.length) {
|
|
95
|
+
ensureDir(p.guestClaudeDir);
|
|
96
|
+
writeSafely(target, () => fs.writeFileSync(target, merged.join('\n') + '\n', 'utf8'));
|
|
97
|
+
}
|
|
98
|
+
return projects + sessions;
|
|
99
|
+
};
|
|
100
|
+
/** Copies new sandbox history back to this PC (safe while the sandbox runs: transcripts are append-only). */
|
|
101
|
+
export const syncHistoryOut = (p) => {
|
|
102
|
+
const hostProject = projectFolderName(p.hostWorkspace);
|
|
103
|
+
// A mirrored workspace on F: really lives at C:\Mirror\F\...; tools that resolve real paths record that one.
|
|
104
|
+
const guestNames = guestWorkspaceAliases(p.guestWorkspace);
|
|
105
|
+
const guestProjects = new Set(guestNames.map(projectFolderName));
|
|
106
|
+
const transform = (t) => guestNames.reduce((out, guest) => translatePaths(out, guest, p.hostWorkspace), t);
|
|
107
|
+
const guestProjectsDir = path.join(p.guestClaudeDir, 'projects');
|
|
108
|
+
const names = dirExists(guestProjectsDir) && !isLink(guestProjectsDir) ? fs.readdirSync(guestProjectsDir) : [];
|
|
109
|
+
const projects = _.sumBy(names, (name) => guestProjects.has(name)
|
|
110
|
+
? copyNewer(path.join(guestProjectsDir, name), path.join(p.hostClaudeDir, 'projects', hostProject), { accept: transcript, skipDirs: NO_MEMORY, transform })
|
|
111
|
+
: copyNewer(path.join(guestProjectsDir, name), path.join(p.hostClaudeDir, 'projects', name), { accept: transcript, skipDirs: NO_MEMORY }));
|
|
112
|
+
// Only to-do lists come back besides transcripts; plans and file backups stay in the sandbox.
|
|
113
|
+
const sessions = copyNewer(path.join(p.guestClaudeDir, 'todos'), path.join(p.hostClaudeDir, 'todos'), { accept: json });
|
|
114
|
+
const hostFile = path.join(p.hostClaudeDir, 'history.jsonl');
|
|
115
|
+
const known = new Set(readLines(hostFile));
|
|
116
|
+
const missing = readLines(path.join(p.guestClaudeDir, 'history.jsonl')).map(transform).filter((l) => !known.has(l));
|
|
117
|
+
if (missing.length) {
|
|
118
|
+
ensureDir(p.hostClaudeDir);
|
|
119
|
+
fs.appendFileSync(hostFile, missing.join('\n') + '\n', 'utf8');
|
|
120
|
+
}
|
|
121
|
+
return projects + sessions + missing.length;
|
|
122
|
+
};
|
|
123
|
+
/**
|
|
124
|
+
* Host-only: <storage>\sync\<dataName>.json. It records which folder on this PC history flows back into, so it must
|
|
125
|
+
* never live in the data folder the sandbox can write (a forged copy could redirect writes anywhere on this PC).
|
|
126
|
+
*/
|
|
127
|
+
export const syncStatePath = (dataPath) => path.join(path.dirname(path.dirname(dataPath)), 'sync', path.basename(dataPath) + '.json');
|
|
128
|
+
export const readSyncState = (dataPath) => tryReadJson(syncStatePath(dataPath));
|
|
129
|
+
export const writeSyncState = (dataPath, state) => writeJson(syncStatePath(dataPath), state);
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Config } from '../core/types.js';
|
|
2
|
+
import { type MemResult } from './claude-mem.js';
|
|
3
|
+
export interface SyncSummary {
|
|
4
|
+
history: number;
|
|
5
|
+
claudeMem: MemResult | 'disabled';
|
|
6
|
+
}
|
|
7
|
+
/** Before launch: finish syncing the previous run out, then bring this PC's history (and claude-mem) in. */
|
|
8
|
+
export declare const syncBeforeLaunch: (config: Config, dataPath: string, hostWorkspace: string) => SyncSummary;
|
|
9
|
+
/** Syncs the sandbox's history (and, when `final`, claude-mem) back to this PC, as configured for the last launch. */
|
|
10
|
+
export declare const syncBack: (dataPath: string, final: boolean) => SyncSummary;
|
|
11
|
+
export declare const watcherRunning: (dataPath: string) => boolean;
|
|
12
|
+
/** Body of the detached watcher process (`agent-sandbox _watch`). */
|
|
13
|
+
export declare const watch: (dataPath: string) => Promise<void>;
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
// When history and claude-mem move: in before every launch, out every minute while the sandbox runs (history) and
|
|
2
|
+
// once more when it closes (history + claude-mem). The watcher runs detached on the host for the sandbox's lifetime.
|
|
3
|
+
// Its pid file and log live in host-only storage, never in the data folder the sandbox can write.
|
|
4
|
+
import fs from 'node:fs';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { assertNoLinks, errMsg, sleep } from '../core/util.js';
|
|
7
|
+
import { hostClaudeDir } from '../host/claude-profile.js';
|
|
8
|
+
import { checkRepo } from '../host/repo-guard.js';
|
|
9
|
+
import { isSandboxRunning } from '../host/platform.js';
|
|
10
|
+
import { hostClaudeMemDir, syncClaudeMem } from './claude-mem.js';
|
|
11
|
+
import { readSyncState, syncHistoryIn, syncHistoryOut, writeSyncState } from './history.js';
|
|
12
|
+
const WATCH_INTERVAL_MS = 60 * 1000;
|
|
13
|
+
const storageOf = (dataPath) => path.dirname(path.dirname(dataPath));
|
|
14
|
+
const guestClaude = (dataPath) => path.join(dataPath, 'state', 'claude');
|
|
15
|
+
const guestMem = (dataPath) => path.join(dataPath, 'state', 'claude-mem');
|
|
16
|
+
/** Sync paths for the sandbox side, after checking nothing on the way there is a link planted by the sandbox. */
|
|
17
|
+
const toPaths = (state, dataPath) => {
|
|
18
|
+
assertNoLinks(dataPath, guestClaude(dataPath));
|
|
19
|
+
return { hostClaudeDir: state.hostClaudeDir, guestClaudeDir: guestClaude(dataPath), hostWorkspace: state.hostWorkspace, guestWorkspace: state.guestWorkspace };
|
|
20
|
+
};
|
|
21
|
+
const memSync = (dataPath, state, direction) => {
|
|
22
|
+
assertNoLinks(dataPath, guestMem(dataPath));
|
|
23
|
+
return syncClaudeMem(hostClaudeMemDir(), guestMem(dataPath), state, direction);
|
|
24
|
+
};
|
|
25
|
+
/** Before launch: finish syncing the previous run out, then bring this PC's history (and claude-mem) in. */
|
|
26
|
+
export const syncBeforeLaunch = (config, dataPath, hostWorkspace) => {
|
|
27
|
+
const profile = config.claudeProfile;
|
|
28
|
+
const summary = { history: 0, claudeMem: 'disabled' };
|
|
29
|
+
if (!config.storage.persistAgentState)
|
|
30
|
+
return summary;
|
|
31
|
+
const previous = readSyncState(dataPath);
|
|
32
|
+
if (previous?.syncHistory)
|
|
33
|
+
syncHistoryOut(toPaths(previous, dataPath));
|
|
34
|
+
const state = { ...previous, hostClaudeDir: hostClaudeDir(profile), hostWorkspace, guestWorkspace: config.guestWorkspace, syncHistory: profile.syncHistory, syncClaudeMem: profile.syncClaudeMem };
|
|
35
|
+
if (profile.syncHistory)
|
|
36
|
+
summary.history = syncHistoryIn(toPaths(state, dataPath));
|
|
37
|
+
if (profile.syncClaudeMem) {
|
|
38
|
+
if (previous?.syncClaudeMem)
|
|
39
|
+
memSync(dataPath, state, 'out');
|
|
40
|
+
summary.claudeMem = memSync(dataPath, state, 'in');
|
|
41
|
+
}
|
|
42
|
+
writeSyncState(dataPath, state);
|
|
43
|
+
return summary;
|
|
44
|
+
};
|
|
45
|
+
/** Syncs the sandbox's history (and, when `final`, claude-mem) back to this PC, as configured for the last launch. */
|
|
46
|
+
export const syncBack = (dataPath, final) => {
|
|
47
|
+
const state = readSyncState(dataPath);
|
|
48
|
+
const summary = { history: 0, claudeMem: 'disabled' };
|
|
49
|
+
if (!state)
|
|
50
|
+
return summary;
|
|
51
|
+
if (state.syncHistory)
|
|
52
|
+
summary.history = syncHistoryOut(toPaths(state, dataPath));
|
|
53
|
+
if (final && state.syncClaudeMem)
|
|
54
|
+
summary.claudeMem = memSync(dataPath, state, 'out');
|
|
55
|
+
writeSyncState(dataPath, state);
|
|
56
|
+
return summary;
|
|
57
|
+
};
|
|
58
|
+
const hostRunDir = (dataPath) => path.join(storageOf(dataPath), 'run');
|
|
59
|
+
const watcherPidFile = (dataPath) => path.join(hostRunDir(dataPath), 'watcher-' + path.basename(dataPath) + '.pid');
|
|
60
|
+
const watcherLog = (dataPath) => path.join(storageOf(dataPath), 'logs', 'host-sync-' + path.basename(dataPath) + '.log');
|
|
61
|
+
const alive = (pid) => { try {
|
|
62
|
+
process.kill(pid, 0);
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
return false;
|
|
67
|
+
} };
|
|
68
|
+
export const watcherRunning = (dataPath) => {
|
|
69
|
+
try {
|
|
70
|
+
return alive(Number(fs.readFileSync(watcherPidFile(dataPath), 'utf8')));
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
/** Body of the detached watcher process (`agent-sandbox _watch`). */
|
|
77
|
+
export const watch = async (dataPath) => {
|
|
78
|
+
if (watcherRunning(dataPath))
|
|
79
|
+
return;
|
|
80
|
+
fs.mkdirSync(hostRunDir(dataPath), { recursive: true });
|
|
81
|
+
fs.mkdirSync(path.dirname(watcherLog(dataPath)), { recursive: true });
|
|
82
|
+
fs.writeFileSync(watcherPidFile(dataPath), String(process.pid));
|
|
83
|
+
const note = (message) => { try {
|
|
84
|
+
fs.appendFileSync(watcherLog(dataPath), new Date().toISOString() + ' ' + message + '\n');
|
|
85
|
+
}
|
|
86
|
+
catch { /* ignore */ } };
|
|
87
|
+
const sync = (final) => {
|
|
88
|
+
try {
|
|
89
|
+
for (const finding of checkRepo(dataPath))
|
|
90
|
+
note('SECURITY: neutralised a git ' + finding.kind + ' added inside the sandbox: ' + finding.detail);
|
|
91
|
+
}
|
|
92
|
+
catch (e) {
|
|
93
|
+
note('repo guard failed: ' + errMsg(e));
|
|
94
|
+
}
|
|
95
|
+
try {
|
|
96
|
+
const s = syncBack(dataPath, final);
|
|
97
|
+
if (s.history || final)
|
|
98
|
+
note((final ? 'sandbox closed; final sync: ' : '') + 'history ' + s.history + ', claude-mem ' + s.claudeMem);
|
|
99
|
+
}
|
|
100
|
+
catch (e) {
|
|
101
|
+
note('sync failed: ' + errMsg(e));
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
try {
|
|
105
|
+
await sleep(30000);
|
|
106
|
+
while (isSandboxRunning()) {
|
|
107
|
+
sync(false);
|
|
108
|
+
await sleep(WATCH_INTERVAL_MS);
|
|
109
|
+
}
|
|
110
|
+
await sleep(3000);
|
|
111
|
+
sync(true);
|
|
112
|
+
}
|
|
113
|
+
finally {
|
|
114
|
+
fs.rmSync(watcherPidFile(dataPath), { force: true });
|
|
115
|
+
}
|
|
116
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Put .ps1 / .mjs / .cmd files here to run inside the sandbox AFTER the agent exits (name order).
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Put .ps1 / .mjs / .cmd files here to run inside the sandbox BEFORE tools are provisioned (name order).
|