@wix/pathgrade 1.0.0 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agents/claude/sdk-options.js +6 -0
- package/dist/providers/credentials.js +1 -1
- package/dist/providers/sandbox-lifecycle.d.ts +22 -0
- package/dist/providers/sandbox-lifecycle.js +133 -0
- package/dist/providers/sandbox.js +2 -1
- package/dist/providers/workspace.d.ts +0 -6
- package/dist/providers/workspace.js +1 -31
- package/package.json +1 -1
|
@@ -78,6 +78,12 @@ export function buildClaudeSdkOptions(inputs) {
|
|
|
78
78
|
continue;
|
|
79
79
|
env[key] = value;
|
|
80
80
|
}
|
|
81
|
+
// Claude Code normally exposes claude.ai account-level MCP connectors to
|
|
82
|
+
// local OAuth sessions. Keep PathGrade trials isolated from those ambient
|
|
83
|
+
// connectors unless the caller explicitly opts in through their env.
|
|
84
|
+
if (useLocalOAuth && env.ENABLE_CLAUDEAI_MCP_SERVERS === undefined) {
|
|
85
|
+
env.ENABLE_CLAUDEAI_MCP_SERVERS = 'false';
|
|
86
|
+
}
|
|
81
87
|
if (!useLocalOAuth) {
|
|
82
88
|
env.CLAUDE_CONFIG_DIR = path.join(inputs.workspacePath, CLAUDE_CONFIG_SUBDIR);
|
|
83
89
|
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export declare const SANDBOX_PREFIX = "pathgrade-";
|
|
2
|
+
export declare const SANDBOX_MARKER = ".pathgrade-sandbox.json";
|
|
3
|
+
export declare const STALE_SANDBOX_AGE_MS: number;
|
|
4
|
+
interface RemoveSandboxRootOptions {
|
|
5
|
+
remove?: (target: string) => Promise<void>;
|
|
6
|
+
sleep?: (ms: number) => Promise<void>;
|
|
7
|
+
retryDelaysMs?: readonly number[];
|
|
8
|
+
}
|
|
9
|
+
export declare function removeSandboxRoot(rootDir: string, opts?: RemoveSandboxRootOptions): Promise<void>;
|
|
10
|
+
export interface StaleSandboxCleanupOptions extends RemoveSandboxRootOptions {
|
|
11
|
+
tempDir?: string;
|
|
12
|
+
now?: () => number;
|
|
13
|
+
isProcessAlive?: (pid: number) => boolean;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Removes old, crashed PathGrade sandboxes without examining anything outside
|
|
17
|
+
* the resolved operating-system temp directory. Every filesystem failure is
|
|
18
|
+
* intentionally ignored so a cleanup race never prevents a new trial.
|
|
19
|
+
*/
|
|
20
|
+
export declare function cleanupStaleSandboxes(opts?: StaleSandboxCleanupOptions): Promise<void>;
|
|
21
|
+
export declare function createSandboxRoot(): Promise<string>;
|
|
22
|
+
export {};
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import * as os from 'os';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
export const SANDBOX_PREFIX = 'pathgrade-';
|
|
5
|
+
export const SANDBOX_MARKER = '.pathgrade-sandbox.json';
|
|
6
|
+
export const STALE_SANDBOX_AGE_MS = 24 * 60 * 60 * 1_000;
|
|
7
|
+
let startupCleanup;
|
|
8
|
+
const SANDBOX_REMOVE_RETRY_DELAYS_MS = [
|
|
9
|
+
50,
|
|
10
|
+
100,
|
|
11
|
+
250,
|
|
12
|
+
500,
|
|
13
|
+
1_000,
|
|
14
|
+
2_000,
|
|
15
|
+
3_000,
|
|
16
|
+
5_000,
|
|
17
|
+
];
|
|
18
|
+
function isRetryableRemoveError(error) {
|
|
19
|
+
const code = error.code;
|
|
20
|
+
return code === 'ENOTEMPTY' || code === 'EBUSY' || code === 'EPERM';
|
|
21
|
+
}
|
|
22
|
+
export async function removeSandboxRoot(rootDir, opts = {}) {
|
|
23
|
+
const remove = opts.remove ?? ((target) => fs.remove(target));
|
|
24
|
+
const sleep = opts.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
25
|
+
const retryDelaysMs = opts.retryDelaysMs ?? SANDBOX_REMOVE_RETRY_DELAYS_MS;
|
|
26
|
+
for (let attempt = 0;; attempt++) {
|
|
27
|
+
try {
|
|
28
|
+
await remove(rootDir);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
if (!isRetryableRemoveError(error) || attempt >= retryDelaysMs.length) {
|
|
33
|
+
throw error;
|
|
34
|
+
}
|
|
35
|
+
await sleep(retryDelaysMs[attempt]);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
function isProcessAlive(pid) {
|
|
40
|
+
try {
|
|
41
|
+
process.kill(pid, 0);
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
// A process we cannot signal is still live. Unknown failures are also
|
|
46
|
+
// preserved: cleanup must fail safe rather than delete a live trial.
|
|
47
|
+
return error.code !== 'ESRCH';
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
async function readOwnedSandboxMarker(rootDir) {
|
|
51
|
+
const rootStat = await fs.lstat(rootDir);
|
|
52
|
+
if (!rootStat.isDirectory() || rootStat.isSymbolicLink())
|
|
53
|
+
return undefined;
|
|
54
|
+
const markerPath = path.join(rootDir, SANDBOX_MARKER);
|
|
55
|
+
const markerStat = await fs.lstat(markerPath);
|
|
56
|
+
if (!markerStat.isFile() || markerStat.isSymbolicLink())
|
|
57
|
+
return undefined;
|
|
58
|
+
const marker = JSON.parse(await fs.readFile(markerPath, 'utf8'));
|
|
59
|
+
const pid = marker.pid;
|
|
60
|
+
if (marker.version !== 1 || typeof pid !== 'number' || !Number.isSafeInteger(pid) || pid <= 0)
|
|
61
|
+
return undefined;
|
|
62
|
+
return { version: 1, pid };
|
|
63
|
+
}
|
|
64
|
+
async function isStaleOwnedSandbox(rootDir, now, staleAfterMs, processIsAlive) {
|
|
65
|
+
const marker = await readOwnedSandboxMarker(rootDir);
|
|
66
|
+
if (!marker)
|
|
67
|
+
return false;
|
|
68
|
+
const markerStat = await fs.lstat(path.join(rootDir, SANDBOX_MARKER));
|
|
69
|
+
if (now - markerStat.mtimeMs < staleAfterMs)
|
|
70
|
+
return false;
|
|
71
|
+
return !processIsAlive(marker.pid);
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Removes old, crashed PathGrade sandboxes without examining anything outside
|
|
75
|
+
* the resolved operating-system temp directory. Every filesystem failure is
|
|
76
|
+
* intentionally ignored so a cleanup race never prevents a new trial.
|
|
77
|
+
*/
|
|
78
|
+
export async function cleanupStaleSandboxes(opts = {}) {
|
|
79
|
+
let tempDir;
|
|
80
|
+
try {
|
|
81
|
+
tempDir = await fs.realpath(opts.tempDir ?? os.tmpdir());
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
const now = opts.now ?? Date.now;
|
|
87
|
+
const processIsAlive = opts.isProcessAlive ?? isProcessAlive;
|
|
88
|
+
let entries;
|
|
89
|
+
try {
|
|
90
|
+
entries = await fs.readdir(tempDir, { withFileTypes: true });
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
for (const entry of entries) {
|
|
96
|
+
if (!entry.isDirectory() || entry.isSymbolicLink() || !entry.name.startsWith(SANDBOX_PREFIX))
|
|
97
|
+
continue;
|
|
98
|
+
const rootDir = path.resolve(tempDir, entry.name);
|
|
99
|
+
if (path.dirname(rootDir) !== tempDir)
|
|
100
|
+
continue;
|
|
101
|
+
try {
|
|
102
|
+
if (!await isStaleOwnedSandbox(rootDir, now(), STALE_SANDBOX_AGE_MS, processIsAlive))
|
|
103
|
+
continue;
|
|
104
|
+
// Re-check ownership and liveness immediately before removal. This
|
|
105
|
+
// narrows the window where another process can replace a candidate.
|
|
106
|
+
if (!await isStaleOwnedSandbox(rootDir, now(), STALE_SANDBOX_AGE_MS, processIsAlive))
|
|
107
|
+
continue;
|
|
108
|
+
await removeSandboxRoot(rootDir, opts);
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
// A disappeared directory, a permission change, or a deletion race
|
|
112
|
+
// must never make trial creation fail.
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
export async function createSandboxRoot() {
|
|
117
|
+
// A process may create many trial sandboxes. Scan only once at startup so
|
|
118
|
+
// repeated trials do not pay to enumerate the entire operating-system temp
|
|
119
|
+
// directory, while concurrent creators share the same best-effort sweep.
|
|
120
|
+
startupCleanup ??= cleanupStaleSandboxes();
|
|
121
|
+
await startupCleanup;
|
|
122
|
+
const tempDir = await fs.realpath(os.tmpdir());
|
|
123
|
+
const rootDir = await fs.mkdtemp(path.join(tempDir, SANDBOX_PREFIX));
|
|
124
|
+
const marker = { version: 1, pid: process.pid };
|
|
125
|
+
try {
|
|
126
|
+
await fs.writeFile(path.join(rootDir, SANDBOX_MARKER), JSON.stringify(marker), 'utf8');
|
|
127
|
+
return rootDir;
|
|
128
|
+
}
|
|
129
|
+
catch (error) {
|
|
130
|
+
await fs.remove(rootDir).catch(() => { });
|
|
131
|
+
throw error;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
@@ -2,11 +2,12 @@ import fs from 'fs-extra';
|
|
|
2
2
|
import * as path from 'path';
|
|
3
3
|
import * as os from 'os';
|
|
4
4
|
import { DEFAULT_COPY_IGNORE, createCopyFilter, isPortableCopyEntry } from './copy-filter.js';
|
|
5
|
+
import { createSandboxRoot } from './sandbox-lifecycle.js';
|
|
5
6
|
export const SAFE_HOST_VARS = [
|
|
6
7
|
'PATH', 'SHELL', 'LANG', 'LC_ALL', 'LC_CTYPE', 'TERM', 'USER', 'LOGNAME',
|
|
7
8
|
];
|
|
8
9
|
export async function createSandbox(spec) {
|
|
9
|
-
const rootDir =
|
|
10
|
+
const rootDir = await createSandboxRoot();
|
|
10
11
|
const workspacePath = path.join(rootDir, 'workspace');
|
|
11
12
|
const homePath = path.join(rootDir, 'home');
|
|
12
13
|
const tmpPath = path.join(rootDir, 'tmp');
|
|
@@ -11,11 +11,5 @@ export interface Workspace {
|
|
|
11
11
|
}): Promise<CommandResult>;
|
|
12
12
|
dispose(): Promise<void>;
|
|
13
13
|
}
|
|
14
|
-
interface RemoveSandboxRootOptions {
|
|
15
|
-
remove?: (target: string) => Promise<void>;
|
|
16
|
-
sleep?: (ms: number) => Promise<void>;
|
|
17
|
-
retryDelaysMs?: readonly number[];
|
|
18
|
-
}
|
|
19
|
-
export declare function removeSandboxRoot(rootDir: string, opts?: RemoveSandboxRootOptions): Promise<void>;
|
|
20
14
|
export declare function linkPathsFromHostHome(pathsToLink: string[], sandboxHomePath: string): Promise<void>;
|
|
21
15
|
export declare function prepareWorkspace(spec: SandboxConfig): Promise<Workspace>;
|
|
@@ -2,41 +2,11 @@ import fs from 'fs-extra';
|
|
|
2
2
|
import * as os from 'os';
|
|
3
3
|
import * as path from 'path';
|
|
4
4
|
import { createSandbox } from './sandbox.js';
|
|
5
|
+
import { removeSandboxRoot } from './sandbox-lifecycle.js';
|
|
5
6
|
import { stageMcpConfig } from './mcp-config.js';
|
|
6
7
|
import { sandboxExec } from './sandbox-exec.js';
|
|
7
8
|
import { resolveCredentials } from './credentials.js';
|
|
8
9
|
import { isPortableCopyEntry } from './copy-filter.js';
|
|
9
|
-
const SANDBOX_REMOVE_RETRY_DELAYS_MS = [
|
|
10
|
-
50,
|
|
11
|
-
100,
|
|
12
|
-
250,
|
|
13
|
-
500,
|
|
14
|
-
1_000,
|
|
15
|
-
2_000,
|
|
16
|
-
3_000,
|
|
17
|
-
5_000,
|
|
18
|
-
];
|
|
19
|
-
function isRetryableRemoveError(error) {
|
|
20
|
-
const code = error.code;
|
|
21
|
-
return code === 'ENOTEMPTY' || code === 'EBUSY' || code === 'EPERM';
|
|
22
|
-
}
|
|
23
|
-
export async function removeSandboxRoot(rootDir, opts = {}) {
|
|
24
|
-
const remove = opts.remove ?? ((target) => fs.remove(target));
|
|
25
|
-
const sleep = opts.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
26
|
-
const retryDelaysMs = opts.retryDelaysMs ?? SANDBOX_REMOVE_RETRY_DELAYS_MS;
|
|
27
|
-
for (let attempt = 0;; attempt++) {
|
|
28
|
-
try {
|
|
29
|
-
await remove(rootDir);
|
|
30
|
-
return;
|
|
31
|
-
}
|
|
32
|
-
catch (error) {
|
|
33
|
-
if (!isRetryableRemoveError(error) || attempt >= retryDelaysMs.length) {
|
|
34
|
-
throw error;
|
|
35
|
-
}
|
|
36
|
-
await sleep(retryDelaysMs[attempt]);
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
10
|
async function copyPathsFromHostHome(pathsToCopy, sandboxHomePath) {
|
|
41
11
|
const realHome = os.homedir();
|
|
42
12
|
for (const relPath of pathsToCopy) {
|