pi-crew 0.9.0 → 0.9.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/CHANGELOG.md +95 -0
- package/package.json +1 -1
- package/src/runtime/async-runner.ts +3 -0
- package/src/runtime/child-pi.ts +3 -0
- package/src/runtime/iteration-hooks.ts +2 -1
- package/src/runtime/post-checks.ts +2 -1
- package/src/runtime/verification-gates.ts +3 -0
- package/src/state/worker-atomic-writer.ts +25 -11
- package/src/utils/env-allowlist.ts +30 -0
- package/src/utils/safe-paths.ts +55 -14
- package/src/worktree/cleanup.ts +2 -1
- package/src/worktree/worktree-manager.ts +4 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,100 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [v0.9.1] — Windows essentials fix + cross-platform CI green (2026-06-22)
|
|
4
|
+
|
|
5
|
+
Patch release. No new features. Fixes a real Windows bug reported by a user,
|
|
6
|
+
plus the cross-platform CI failures that followed.
|
|
7
|
+
|
|
8
|
+
### fix(windows): `${APPDATA}` npm-global resolution failure (root cause)
|
|
9
|
+
|
|
10
|
+
Reported symptom (Windows): running pi-crew created a phantom literal
|
|
11
|
+
`${APPDATA}/npm/` directory in the project root (containing `node_modules`,
|
|
12
|
+
`pi-crew`, `pi-crew.cmd`, `pi-crew.ps1`) and leaked a literal `${APPDATA}`
|
|
13
|
+
line into `.gitignore`.
|
|
14
|
+
|
|
15
|
+
Root cause: pi-crew's subprocess env sanitization used explicit allowlists
|
|
16
|
+
that stripped **all Windows-essential env vars** (`APPDATA`, `LOCALAPPDATA`,
|
|
17
|
+
`USERPROFILE`, `SystemRoot`, `ComSpec`, `TEMP`, `TMP`). When a child pi
|
|
18
|
+
process (or npm inside it) tried to resolve the npm-global prefix on Windows,
|
|
19
|
+
it used `%APPDATA%` (cmd expansion) / `${APPDATA}` (bash expansion), but
|
|
20
|
+
`APPDATA` was missing from the env — so the shell left the literal
|
|
21
|
+
`${APPDATA}` in place and operations created/ignored paths under that
|
|
22
|
+
literal name.
|
|
23
|
+
|
|
24
|
+
Fix: added the 7 Windows essentials to all 7 subprocess env allowlists
|
|
25
|
+
(child-pi, async-runner, verification-gates, post-checks, iteration-hooks,
|
|
26
|
+
worktree/cleanup, worktree/worktree-manager). (commit `a7ddc50`)
|
|
27
|
+
|
|
28
|
+
### refactor(env): centralize Windows essentials + regression guard
|
|
29
|
+
|
|
30
|
+
The same 7 vars were duplicated inline across 9 call sites — easy to forget
|
|
31
|
+
on a new allowlist, with nothing preventing a future site from omitting them.
|
|
32
|
+
|
|
33
|
+
- New single source of truth: `WINDOWS_ESSENTIAL_ENV_VARS` in
|
|
34
|
+
`src/utils/env-allowlist.ts` (with full root-cause documentation).
|
|
35
|
+
- All 9 call sites now spread the constant instead of inlining (net −42/+19
|
|
36
|
+
lines, behavior unchanged).
|
|
37
|
+
- New regression test `test/unit/env-allowlist.test.ts`: scans ALL
|
|
38
|
+
`src/**/*.ts` files and fails if any hardcodes the 7 vars inline (the only
|
|
39
|
+
allowed location is the constant file). This catches any new allowlist that
|
|
40
|
+
forgets the constant — the exact regression that caused the bug.
|
|
41
|
+
(commit `6a0284c`)
|
|
42
|
+
|
|
43
|
+
### fix(ci): cross-platform CI green (ubuntu + macOS + Windows)
|
|
44
|
+
|
|
45
|
+
Three distinct containment/path bugs that only surfaced on non-ubuntu CI:
|
|
46
|
+
|
|
47
|
+
1. **Windows 8.3 short-name paths** — `resolveWindowsCanonical()` used
|
|
48
|
+
non-native `realpathSync`, preserving the `RUNNER~1` vs `runneradmin`
|
|
49
|
+
form mismatch. A legitimately-contained dynamic workflow file was
|
|
50
|
+
rejected as "outside the allowed directories". Fixed by using
|
|
51
|
+
`realpathSync.native` (canonical long-name form) as the primary resolver.
|
|
52
|
+
(commit `e9e7137`)
|
|
53
|
+
|
|
54
|
+
2. **ESM `file://` URLs** — two integration tests passed raw Windows paths
|
|
55
|
+
(`D:\…`) to native `import()`, which Node rejects on Windows
|
|
56
|
+
(`ERR_UNSUPPORTED_ESM_URL_SCHEME: protocol 'd:'`). Wrapped with
|
|
57
|
+
`pathToFileURL(…).href`. (commit `e9e7137`)
|
|
58
|
+
|
|
59
|
+
3. **macOS symlink-ancestor** — `isSymlinkSafePath()` walked up the temp
|
|
60
|
+
path and hit `/var` (a symlink → `/private/var`). The old check compared
|
|
61
|
+
the resolved `/private/var` against the tmpdir
|
|
62
|
+
`/private/var/folders/…/T` — `/private/var` is an **ancestor**, not a
|
|
63
|
+
descendant, so it was wrongly rejected (5 macOS worker-atomic-writer
|
|
64
|
+
failures). Fixed by accepting a symlink whose target is a safe root, is
|
|
65
|
+
UNDER a safe root, OR is an ANCESTOR of a safe root. Added two behavioral
|
|
66
|
+
regression tests (symlink-ancestor accept + symlink-attack reject).
|
|
67
|
+
(commit `e9e7137`)
|
|
68
|
+
|
|
69
|
+
4. **macOS `/var` containment** — `resolveContainedPath()` only
|
|
70
|
+
canonicalized paths on win32; on POSIX it compared raw paths, so base
|
|
71
|
+
(`/private/var`) vs target (`/var`) diverged → false "outside" rejection
|
|
72
|
+
(macOS dwf-setresult failure). Added platform-agnostic
|
|
73
|
+
`resolveCanonicalPath()`. Added a darwin-only regression test for the
|
|
74
|
+
real `/var` divergence. (commit `4821bb1`)
|
|
75
|
+
|
|
76
|
+
5. **Windows wakeup timing** — `subagent-manager` polls the child run
|
|
77
|
+
manifest every 1000ms. On the slower Windows CI runner, child-process
|
|
78
|
+
spawn + first poll exceeded the test's 10s deadline (failed at 11.6s).
|
|
79
|
+
Bumped the mock-test deadline to 30s. (commit `4821bb1`)
|
|
80
|
+
|
|
81
|
+
### Verification
|
|
82
|
+
|
|
83
|
+
- tsc: 0
|
|
84
|
+
- Full test suite: 5207 tests, 0 fail on **all three** platforms (ubuntu,
|
|
85
|
+
macOS, Windows)
|
|
86
|
+
- CI run `27955398241`: success across ubuntu-latest, macos-latest,
|
|
87
|
+
windows-latest
|
|
88
|
+
- Regression tests added: env-allowlist scan, worker-atomic-writer symlink
|
|
89
|
+
ancestor/attack, safe-paths darwin `/var` divergence
|
|
90
|
+
|
|
91
|
+
### Breaking changes
|
|
92
|
+
|
|
93
|
+
None. All fixes are additive or behavior-preserving. Windows users who hit
|
|
94
|
+
the `${APPDATA}` bug should upgrade.
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
3
98
|
## [v0.9.0] — goal loops + dynamic workflows (2026-06-18)
|
|
4
99
|
|
|
5
100
|
Two new features, both built on a shared `runKind` background-dispatch discriminator.
|
package/package.json
CHANGED
|
@@ -3,6 +3,7 @@ import { createRequire } from "node:module";
|
|
|
3
3
|
import * as fs from "node:fs";
|
|
4
4
|
import * as path from "node:path";
|
|
5
5
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
6
|
+
import { WINDOWS_ESSENTIAL_ENV_VARS } from "../utils/env-allowlist.ts";
|
|
6
7
|
import { logInternalError } from "../utils/internal-error.ts";
|
|
7
8
|
import { appendEvent } from "../state/event-log.ts";
|
|
8
9
|
import { sanitizeEnvSecrets } from "../utils/env-filter.ts";
|
|
@@ -183,6 +184,8 @@ export async function spawnBackgroundTeamRun(manifest: TeamRunManifest): Promise
|
|
|
183
184
|
"XDG_DATA_HOME",
|
|
184
185
|
"XDG_CACHE_HOME",
|
|
185
186
|
"XDG_RUNTIME_DIR",
|
|
187
|
+
// Windows essentials — see WINDOWS_ESSENTIAL_ENV_VARS (src/utils/env-allowlist.ts).
|
|
188
|
+
...WINDOWS_ESSENTIAL_ENV_VARS,
|
|
186
189
|
"NVM_BIN",
|
|
187
190
|
"NVM_DIR",
|
|
188
191
|
"NVM_INC",
|
package/src/runtime/child-pi.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { spawn, type ChildProcess, type SpawnOptions } from "node:child_process";
|
|
2
2
|
import * as fs from "node:fs";
|
|
3
3
|
import * as path from "node:path";
|
|
4
|
+
import { WINDOWS_ESSENTIAL_ENV_VARS } from "../utils/env-allowlist.ts";
|
|
4
5
|
import type { AgentConfig } from "../agents/agent-config.ts";
|
|
5
6
|
import type { WorkerExitStatus } from "../state/types.ts";
|
|
6
7
|
import { buildPiWorkerArgs, checkCrewDepth, cleanupTempDir } from "./pi-args.ts";
|
|
@@ -245,6 +246,8 @@ export function buildChildPiSpawnOptions(cwd: string, env: NodeJS.ProcessEnv): S
|
|
|
245
246
|
"XDG_DATA_HOME",
|
|
246
247
|
"XDG_CACHE_HOME",
|
|
247
248
|
"XDG_RUNTIME_DIR",
|
|
249
|
+
// Windows essentials — see WINDOWS_ESSENTIAL_ENV_VARS (src/utils/env-allowlist.ts).
|
|
250
|
+
...WINDOWS_ESSENTIAL_ENV_VARS,
|
|
248
251
|
"NVM_BIN",
|
|
249
252
|
"NVM_DIR",
|
|
250
253
|
"NVM_INC",
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import { spawn } from "node:child_process";
|
|
8
8
|
import * as fs from "node:fs";
|
|
9
9
|
import * as path from "node:path";
|
|
10
|
+
import { WINDOWS_ESSENTIAL_ENV_VARS } from "../utils/env-allowlist.ts";
|
|
10
11
|
import { resolveShellForScript } from "../utils/resolve-shell.ts";
|
|
11
12
|
import { sanitizeEnvSecrets } from "../utils/env-filter.ts";
|
|
12
13
|
import { DENIED_METRIC_NAMES } from "./metric-parser.ts";
|
|
@@ -171,7 +172,7 @@ export async function runIterationHook(
|
|
|
171
172
|
const { command, args } = resolveShellForScript(resolvedScript);
|
|
172
173
|
const child = spawn(command, args, {
|
|
173
174
|
cwd: payload.cwd,
|
|
174
|
-
env: { ...sanitizeEnvSecrets(process.env, { allowList: ["PATH", "HOME", "USER",
|
|
175
|
+
env: { ...sanitizeEnvSecrets(process.env, { allowList: ["PATH", "HOME", "USER", ...WINDOWS_ESSENTIAL_ENV_VARS, "TMPDIR", "LANG", "LC_ALL", "PI_CREW_*"] }), PI_CREW_HOOK: "1" },
|
|
175
176
|
stdio: ["pipe", "pipe", "pipe"],
|
|
176
177
|
});
|
|
177
178
|
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { execFileSync } from "node:child_process";
|
|
8
8
|
import * as path from "node:path";
|
|
9
|
+
import { WINDOWS_ESSENTIAL_ENV_VARS } from "../utils/env-allowlist.ts";
|
|
9
10
|
import { resolveShellForScript } from "../utils/resolve-shell.ts";
|
|
10
11
|
import { sanitizeEnvSecrets } from "../utils/env-filter.ts";
|
|
11
12
|
|
|
@@ -94,7 +95,7 @@ export async function runPostCheck(config: PostCheckConfig, cwd: string): Promis
|
|
|
94
95
|
timeout: timeoutMs,
|
|
95
96
|
encoding: "utf-8",
|
|
96
97
|
maxBuffer: 10 * 1024 * 1024, // 10 MB
|
|
97
|
-
env: { ...sanitizeEnvSecrets(process.env, { allowList: ["PATH", "HOME", "USER",
|
|
98
|
+
env: { ...sanitizeEnvSecrets(process.env, { allowList: ["PATH", "HOME", "USER", ...WINDOWS_ESSENTIAL_ENV_VARS, "TMPDIR", "LANG", "LC_ALL", "PI_CREW_*"] }), PI_CREW_POST_CHECK: "1" },
|
|
98
99
|
});
|
|
99
100
|
|
|
100
101
|
const durationMs = Date.now() - startTime;
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
import { spawn } from "node:child_process";
|
|
13
13
|
import * as fs from "node:fs";
|
|
14
14
|
import * as path from "node:path";
|
|
15
|
+
import { WINDOWS_ESSENTIAL_ENV_VARS } from "../utils/env-allowlist.ts";
|
|
15
16
|
import { writeArtifact } from "../state/artifact-store.ts";
|
|
16
17
|
import { redactSecretString } from "../utils/redaction.ts";
|
|
17
18
|
import { sanitizeEnvSecrets } from "../utils/env-filter.ts";
|
|
@@ -48,6 +49,8 @@ const VERIFICATION_ENV_ALLOWLIST: readonly string[] = [
|
|
|
48
49
|
"XDG_DATA_HOME",
|
|
49
50
|
"XDG_CACHE_HOME",
|
|
50
51
|
"XDG_RUNTIME_DIR",
|
|
52
|
+
// Windows essentials — see WINDOWS_ESSENTIAL_ENV_VARS (src/utils/env-allowlist.ts).
|
|
53
|
+
...WINDOWS_ESSENTIAL_ENV_VARS,
|
|
51
54
|
"NVM_BIN",
|
|
52
55
|
"NVM_DIR",
|
|
53
56
|
"NVM_INC",
|
|
@@ -40,24 +40,38 @@ const crypto = require("node:crypto");
|
|
|
40
40
|
|
|
41
41
|
function isSymlinkSafePath(filePath) {
|
|
42
42
|
try {
|
|
43
|
-
|
|
43
|
+
// Safe roots: the system temp dir (resolved through any symlinks, e.g.
|
|
44
|
+
// macOS /var → /private/var) and the current project cwd.
|
|
45
|
+
var tmpReal;
|
|
46
|
+
try { tmpReal = fs.realpathSync(require("node:os").tmpdir()); } catch (e2) { tmpReal = require("node:os").tmpdir(); }
|
|
47
|
+
var cwd = process.cwd();
|
|
48
|
+
// Accept a symlink ancestor as safe if the resolved target (a) IS a safe
|
|
49
|
+
// root, (b) is UNDER a safe root (target is a descendant), or (c) is an
|
|
50
|
+
// ANCESTOR of a safe root (target is a parent dir of a safe root).
|
|
51
|
+
// Case (c) is essential on macOS: /var → /private/var is an ancestor of
|
|
52
|
+
// the tmpdir /private/var/folders/…, so it must be allowed.
|
|
53
|
+
var isSafeAncestor = function (real) {
|
|
54
|
+
var roots = [tmpReal, cwd];
|
|
55
|
+
return roots.some(function (root) {
|
|
56
|
+
return real === root
|
|
57
|
+
|| real.indexOf(root + path.sep) === 0
|
|
58
|
+
|| root.indexOf(real + path.sep) === 0;
|
|
59
|
+
});
|
|
60
|
+
};
|
|
61
|
+
var currentPath = filePath;
|
|
44
62
|
while (currentPath !== path.dirname(currentPath)) {
|
|
45
|
-
|
|
63
|
+
var dir = path.dirname(currentPath);
|
|
46
64
|
try {
|
|
47
|
-
|
|
65
|
+
var stat = fs.lstatSync(dir);
|
|
48
66
|
if (stat.isSymbolicLink()) {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
const real = fs.realpathSync(dir);
|
|
52
|
-
if (!real.startsWith("/tmp/") && !real.startsWith(process.cwd())) {
|
|
53
|
-
return false;
|
|
54
|
-
}
|
|
67
|
+
var real = fs.realpathSync(dir);
|
|
68
|
+
if (!isSafeAncestor(real)) return false;
|
|
55
69
|
}
|
|
56
|
-
} catch { /* not found — fine */ }
|
|
70
|
+
} catch (e3) { /* not found — fine */ }
|
|
57
71
|
currentPath = dir;
|
|
58
72
|
}
|
|
59
73
|
return true;
|
|
60
|
-
} catch { return true; }
|
|
74
|
+
} catch (e) { return true; }
|
|
61
75
|
}
|
|
62
76
|
|
|
63
77
|
function syncAtomicWriteFile(filePath, content) {
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Windows-essential environment variables that MUST be present in every
|
|
3
|
+
* subprocess env allowlist in pi-crew.
|
|
4
|
+
*
|
|
5
|
+
* Without them, child processes on Windows cannot locate the npm-global prefix
|
|
6
|
+
* (`%APPDATA%\npm`), the user profile, system DLLs (SystemRoot), cmd.exe
|
|
7
|
+
* (ComSpec), or a writable temp dir.
|
|
8
|
+
*
|
|
9
|
+
* Regression root cause (v0.9.0): env allowlists stripped these vars, so child
|
|
10
|
+
* pi/npm resolved the npm-global prefix via the literal `%APPDATA%` (cmd) /
|
|
11
|
+
* `${APPDATA}` (bash) expansion — but APPDATA was missing from the env, so the
|
|
12
|
+
* shell left the literal `${APPDATA}` in place. A phantom `${APPDATA}/npm`
|
|
13
|
+
* directory appeared in the project root and a literal `${APPDATA}` line leaked
|
|
14
|
+
* into `.gitignore`.
|
|
15
|
+
*
|
|
16
|
+
* USAGE: spread this constant into every `allowList` array:
|
|
17
|
+
* `allowList: ["PATH", "HOME", ...WINDOWS_ESSENTIAL_ENV_VARS, ...]`
|
|
18
|
+
*
|
|
19
|
+
* The regression test `test/unit/env-allowlist.test.ts` enforces that no
|
|
20
|
+
* `src/` file hardcodes these vars inline — all sites MUST use this constant.
|
|
21
|
+
*/
|
|
22
|
+
export const WINDOWS_ESSENTIAL_ENV_VARS: readonly string[] = [
|
|
23
|
+
"APPDATA",
|
|
24
|
+
"LOCALAPPDATA",
|
|
25
|
+
"USERPROFILE",
|
|
26
|
+
"SystemRoot",
|
|
27
|
+
"ComSpec",
|
|
28
|
+
"TEMP",
|
|
29
|
+
"TMP",
|
|
30
|
+
];
|
package/src/utils/safe-paths.ts
CHANGED
|
@@ -16,32 +16,73 @@ export function resolveContainedPath(baseDir: string, targetPath: string): strin
|
|
|
16
16
|
}
|
|
17
17
|
const base = path.resolve(baseDir);
|
|
18
18
|
const resolved = path.isAbsolute(targetPath) ? path.resolve(targetPath) : path.resolve(base, targetPath);
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
|
|
24
|
-
const
|
|
19
|
+
// Normalize BOTH paths to canonical form on ALL platforms. This resolves
|
|
20
|
+
// symlinks so that a base and target that refer to the same physical dir
|
|
21
|
+
// via different paths (Windows 8.3 short-name; macOS /var → /private/var)
|
|
22
|
+
// compare equal. Without this, a legitimately-contained target under an
|
|
23
|
+
// OS-managed symlink is wrongly rejected as "outside" the base.
|
|
24
|
+
const baseNorm = resolveCanonicalPath(base);
|
|
25
|
+
const resolvedNorm = resolveCanonicalPath(resolved);
|
|
25
26
|
const relative = process.platform === "win32"
|
|
26
27
|
? path.relative(baseNorm.toLowerCase(), resolvedNorm.toLowerCase())
|
|
27
|
-
: path.relative(
|
|
28
|
+
: path.relative(baseNorm, resolvedNorm);
|
|
28
29
|
if (relative.startsWith("..") || path.isAbsolute(relative)) throw new Error(`Path is outside ${baseDir}: ${targetPath}`);
|
|
29
30
|
return resolved;
|
|
30
31
|
}
|
|
31
32
|
|
|
33
|
+
/**
|
|
34
|
+
* Platform-agnostic canonical path resolution. Resolves symlinks (via
|
|
35
|
+
* realpathSync) to normalize paths so the same physical location always
|
|
36
|
+
* yields the same string regardless of how it was referenced.
|
|
37
|
+
*
|
|
38
|
+
* - Windows: delegates to resolveWindowsCanonical (canonical LONG-NAME form,
|
|
39
|
+
* resolving 8.3 short-name aliases like RUNNER~1 → runneradmin).
|
|
40
|
+
* - POSIX: uses realpathSync, which resolves system symlinks such as the
|
|
41
|
+
* macOS /var → /private/var mapping.
|
|
42
|
+
*
|
|
43
|
+
* For non-existent paths (write targets), walks up to the deepest existing
|
|
44
|
+
* ancestor and joins the remaining components, so the canonical prefix is
|
|
45
|
+
* still comparable.
|
|
46
|
+
*/
|
|
47
|
+
function resolveCanonicalPath(p: string): string {
|
|
48
|
+
if (process.platform === "win32") return resolveWindowsCanonical(p);
|
|
49
|
+
try {
|
|
50
|
+
return fs.realpathSync(p);
|
|
51
|
+
} catch {
|
|
52
|
+
const parts: string[] = [];
|
|
53
|
+
let current = p;
|
|
54
|
+
while (current !== path.dirname(current)) {
|
|
55
|
+
try {
|
|
56
|
+
const real = fs.realpathSync(current);
|
|
57
|
+
let acc = real;
|
|
58
|
+
for (let i = parts.length - 1; i >= 0; i--) acc = path.join(acc, parts[i]);
|
|
59
|
+
return acc;
|
|
60
|
+
} catch { /* keep walking up */ }
|
|
61
|
+
parts.push(path.basename(current));
|
|
62
|
+
current = path.dirname(current);
|
|
63
|
+
}
|
|
64
|
+
return p;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
32
68
|
/**
|
|
33
69
|
* On Windows, resolve a path to its canonical (long-name) form.
|
|
34
70
|
* Walks up ancestors until finding one that exists, then joins back down.
|
|
35
71
|
* This handles paths where intermediate directories don't exist yet but
|
|
36
72
|
* their ancestors do (and may use short-name aliases).
|
|
73
|
+
*
|
|
74
|
+
* Uses fs.realpathSync.native (canonical long-name form) as the primary
|
|
75
|
+
* resolver, falling back to non-native realpathSync if .native fails.
|
|
37
76
|
*/
|
|
38
77
|
function resolveWindowsCanonical(p: string): string {
|
|
39
78
|
try {
|
|
40
|
-
// Use
|
|
41
|
-
// On Windows
|
|
42
|
-
//
|
|
43
|
-
//
|
|
44
|
-
|
|
79
|
+
// Use the NATIVE realpath to resolve to canonical LONG-NAME form.
|
|
80
|
+
// On Windows, fs.realpathSync.native resolves 8.3 short-name aliases
|
|
81
|
+
// (e.g. RUNNER~1) to long-name (runneradmin). This is essential for
|
|
82
|
+
// containment checks: base (from cwd, often long-name) and target
|
|
83
|
+
// (from os.tmpdir()/mkdtempSync, often short-name) must normalize to
|
|
84
|
+
// the SAME form or a contained target is wrongly rejected as "outside".
|
|
85
|
+
let real = fs.realpathSync.native(p);
|
|
45
86
|
// Guard against NTFS internal paths (e.g. C:\$Extend\$Deleted)
|
|
46
87
|
if (real.includes("$Extend") || real.includes("$Deleted")) throw new Error("NTFS internal path");
|
|
47
88
|
return real;
|
|
@@ -56,8 +97,8 @@ function resolveWindowsCanonical(p: string): string {
|
|
|
56
97
|
let current = p;
|
|
57
98
|
while (current !== path.dirname(current)) {
|
|
58
99
|
try {
|
|
59
|
-
|
|
60
|
-
|
|
100
|
+
let real: string;
|
|
101
|
+
try { real = fs.realpathSync.native(current); } catch { real = fs.realpathSync(current); }
|
|
61
102
|
// Guard against NTFS internal paths
|
|
62
103
|
if (real.includes("$Extend") || real.includes("$Deleted")) throw new Error("NTFS internal path");
|
|
63
104
|
// Found existing ancestor — join with remaining parts in reverse order
|
package/src/worktree/cleanup.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { execFileSync } from "node:child_process";
|
|
2
2
|
import * as fs from "node:fs";
|
|
3
3
|
import * as path from "node:path";
|
|
4
|
+
import { WINDOWS_ESSENTIAL_ENV_VARS } from "../utils/env-allowlist.ts";
|
|
4
5
|
import type { TeamRunManifest } from "../state/types.ts";
|
|
5
6
|
import { writeArtifact } from "../state/artifact-store.ts";
|
|
6
7
|
import { projectCrewRoot } from "../utils/paths.ts";
|
|
@@ -18,7 +19,7 @@ export interface WorktreeCleanupResult {
|
|
|
18
19
|
|
|
19
20
|
// SECURITY: PI_* and PI_CREW_* wildcards removed — they could match secret vars like PI_PASSWORD.
|
|
20
21
|
// Git operations do not need PI_CREW_* execution-control vars.
|
|
21
|
-
const GIT_SAFE_ENV = { ...sanitizeEnvSecrets(process.env, { allowList: ["PATH", "HOME", "USER",
|
|
22
|
+
const GIT_SAFE_ENV = { ...sanitizeEnvSecrets(process.env, { allowList: ["PATH", "HOME", "USER", ...WINDOWS_ESSENTIAL_ENV_VARS, "SHELL", "TERM", "LANG", "LC_ALL", "LC_COLLATE", "LC_CTYPE", "LC_MESSAGES", "XDG_CONFIG_HOME", "XDG_DATA_HOME", "XDG_CACHE_HOME", "NVM_BIN", "NVM_DIR", "NODE_PATH", "GIT_CONFIG_GLOBAL", "GIT_CONFIG_SYSTEM", "GIT_AUTHOR_NAME", "GIT_AUTHOR_EMAIL", "GIT_COMMITTER_NAME", "GIT_COMMITTER_EMAIL"] }), LANG: "C", LC_ALL: "C" };
|
|
22
23
|
|
|
23
24
|
function sanitizeBranchPart(value: string): string {
|
|
24
25
|
return value.toLowerCase().replace(/[^a-z0-9._/-]+/g, "-").replace(/^-+|-+$/g, "") || "task";
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { execFileSync, spawnSync } from "node:child_process";
|
|
2
2
|
import * as fs from "node:fs";
|
|
3
3
|
import * as path from "node:path";
|
|
4
|
+
import { WINDOWS_ESSENTIAL_ENV_VARS } from "../utils/env-allowlist.ts";
|
|
4
5
|
import { loadConfig } from "../config/config.ts";
|
|
5
6
|
import { projectCrewRoot } from "../utils/paths.ts";
|
|
6
7
|
import { DEFAULT_PATHS } from "../config/defaults.ts";
|
|
@@ -27,7 +28,7 @@ export interface WorktreeDiffStat {
|
|
|
27
28
|
function git(cwd: string, args: string[]): string {
|
|
28
29
|
// SECURITY: PI_* and PI_CREW_* wildcards removed — they could match secret vars like PI_PASSWORD.
|
|
29
30
|
// Git operations do not need PI_CREW_* execution-control vars.
|
|
30
|
-
return execFileSync("git", args, { cwd, encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"], env: { ...sanitizeEnvSecrets(process.env, { allowList: ["PATH", "HOME", "USER",
|
|
31
|
+
return execFileSync("git", args, { cwd, encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"], env: { ...sanitizeEnvSecrets(process.env, { allowList: ["PATH", "HOME", "USER", ...WINDOWS_ESSENTIAL_ENV_VARS, "SHELL", "TERM", "LANG", "LC_ALL", "LC_COLLATE", "LC_CTYPE", "LC_MESSAGES", "XDG_CONFIG_HOME", "XDG_DATA_HOME", "XDG_CACHE_HOME", "NVM_BIN", "NVM_DIR", "NODE_PATH", "GIT_CONFIG_GLOBAL", "GIT_CONFIG_SYSTEM", "GIT_AUTHOR_NAME", "GIT_AUTHOR_EMAIL", "GIT_COMMITTER_NAME", "GIT_COMMITTER_EMAIL"] }), LANG: "en_US.UTF-8", LC_ALL: "en_US.UTF-8" }, windowsHide: true }).trim();
|
|
31
32
|
}
|
|
32
33
|
|
|
33
34
|
// Dots are removed from branch names since they are used in path construction,
|
|
@@ -182,7 +183,7 @@ function runSetupHook(manifest: TeamRunManifest, task: TeamTaskState, repoRoot:
|
|
|
182
183
|
timeout: cfg.setupHookTimeoutMs ?? 30_000,
|
|
183
184
|
shell: false, // cmd.exe /c handles batch files safely
|
|
184
185
|
env: sanitizeEnvSecrets(process.env, {
|
|
185
|
-
allowList: ["PATH", "HOME",
|
|
186
|
+
allowList: ["PATH", "HOME", ...WINDOWS_ESSENTIAL_ENV_VARS, "TMPDIR", "LANG", "LC_ALL"],
|
|
186
187
|
}),
|
|
187
188
|
windowsHide: true,
|
|
188
189
|
})
|
|
@@ -193,7 +194,7 @@ function runSetupHook(manifest: TeamRunManifest, task: TeamTaskState, repoRoot:
|
|
|
193
194
|
timeout: cfg.setupHookTimeoutMs ?? 30_000,
|
|
194
195
|
shell: false,
|
|
195
196
|
env: sanitizeEnvSecrets(process.env, {
|
|
196
|
-
allowList: ["PATH", "HOME",
|
|
197
|
+
allowList: ["PATH", "HOME", ...WINDOWS_ESSENTIAL_ENV_VARS, "TMPDIR", "LANG", "LC_ALL"],
|
|
197
198
|
}),
|
|
198
199
|
windowsHide: true,
|
|
199
200
|
});
|