@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.
@@ -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
  }
@@ -95,7 +95,7 @@ async function resolveClaude(userEnv, ports) {
95
95
  return {
96
96
  env: { PATHGRADE_CLAUDE_LOCAL_OAUTH: '1' },
97
97
  setupCommands: [],
98
- copyFromHome: ['.claude.json'],
98
+ copyFromHome: [],
99
99
  linkFromHome: ['Library/Keychains'],
100
100
  };
101
101
  }
@@ -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 = path.join(os.tmpdir(), `pathgrade-${Math.random().toString(36).substring(7)}`);
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) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wix/pathgrade",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "packageManager": "yarn@4.12.0",
5
5
  "description": "Evaluate whether AI agents discover and use your skills correctly",
6
6
  "repository": {