pi-crew 0.11.44 → 0.11.46
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 +67 -1
- package/dist/index.mjs +510 -510
- package/package.json +1 -1
- package/src/config/env-allowlist-registry.ts +4 -0
- package/src/config/env-vars.ts +4 -0
- package/src/extension/cross-extension-rpc.ts +15 -6
- package/src/prompt/prompt-runtime.ts +16 -3
- package/src/runtime/async-runner.ts +5 -7
- package/src/runtime/broker/crew-broker-child.ts +58 -0
- package/src/runtime/broker/crew-broker-delegate-request.ts +381 -0
- package/src/runtime/broker/crew-broker.ts +57 -327
- package/src/runtime/broker/socket-stop.ts +147 -0
- package/src/runtime/broker/stdin-handshake.ts +11 -0
- package/src/runtime/child-pi/child-pi-kill.ts +44 -0
- package/src/runtime/child-pi/child-pi-spawn.ts +53 -4
- package/src/runtime/child-pi/child-pi.ts +32 -1
- package/src/runtime/child-pi/inference-broker.ts +40 -8
- package/src/runtime/goal-workflow/goal-achievement.ts +10 -1
- package/src/runtime/pi-spawn.ts +10 -1
- package/src/runtime/task-runner/post-execution.ts +36 -2
- package/src/utils/socket-path.ts +54 -0
- package/src/worktree/branch-freshness.ts +9 -22
- package/src/worktree/cleanup.ts +78 -65
- package/src/worktree/git-hardening.ts +507 -0
- package/src/worktree/worktree-manager.ts +176 -129
|
@@ -11,6 +11,14 @@ import { WINDOWS_ESSENTIAL_ENV_VARS } from "../utils/env-allowlist.ts";
|
|
|
11
11
|
import { sanitizeEnvSecrets } from "../utils/env-filter.ts";
|
|
12
12
|
import { logInternalError } from "../utils/internal-error.ts";
|
|
13
13
|
import { projectCrewRoot } from "../utils/paths.ts";
|
|
14
|
+
import {
|
|
15
|
+
assertWorktreePointerTrusted,
|
|
16
|
+
buildGitSafeEnv,
|
|
17
|
+
hardenGitArgs,
|
|
18
|
+
resolveTrustedGitDir,
|
|
19
|
+
verifyWorktreeGitdirPointer,
|
|
20
|
+
worktreeTrustCheck,
|
|
21
|
+
} from "./git-hardening.ts";
|
|
14
22
|
|
|
15
23
|
export interface PreparedTaskWorkspace {
|
|
16
24
|
cwd: string;
|
|
@@ -46,85 +54,29 @@ export interface WorktreeDiffStat {
|
|
|
46
54
|
function git(cwd: string, args: string[]): string {
|
|
47
55
|
// SECURITY: PI_* and PI_CREW_* wildcards removed — they could match secret vars like PI_PASSWORD.
|
|
48
56
|
// Git operations do not need PI_CREW_* execution-control vars.
|
|
49
|
-
|
|
57
|
+
// MED-1: hardened — config-injection overrides via hardenGitArgs (git-hardening.ts).
|
|
58
|
+
// R-A3: re-verify the gitdir pointer before EVERY command whose cwd sits
|
|
59
|
+
// inside a linked worktree (TOCTOU: a live worker can flip the `.git`
|
|
60
|
+
// pointer after an entry-level check passed). The assert throws OUTSIDE the
|
|
61
|
+
// execFile try so callers land in their "worktree corrupted" handling.
|
|
62
|
+
assertWorktreePointerTrusted(cwd);
|
|
63
|
+
return execFileSync("git", hardenGitArgs(args), {
|
|
50
64
|
cwd,
|
|
51
65
|
encoding: "utf-8",
|
|
52
66
|
stdio: ["ignore", "pipe", "pipe"],
|
|
53
|
-
env:
|
|
54
|
-
...sanitizeEnvSecrets(process.env, {
|
|
55
|
-
allowList: [
|
|
56
|
-
"PATH",
|
|
57
|
-
"HOME",
|
|
58
|
-
"USER",
|
|
59
|
-
...WINDOWS_ESSENTIAL_ENV_VARS,
|
|
60
|
-
"SHELL",
|
|
61
|
-
"TERM",
|
|
62
|
-
"LANG",
|
|
63
|
-
"LC_ALL",
|
|
64
|
-
"LC_COLLATE",
|
|
65
|
-
"LC_CTYPE",
|
|
66
|
-
"LC_MESSAGES",
|
|
67
|
-
"XDG_CONFIG_HOME",
|
|
68
|
-
"XDG_DATA_HOME",
|
|
69
|
-
"XDG_CACHE_HOME",
|
|
70
|
-
"NVM_BIN",
|
|
71
|
-
"NVM_DIR",
|
|
72
|
-
"NODE_PATH",
|
|
73
|
-
"GIT_CONFIG_GLOBAL",
|
|
74
|
-
"GIT_CONFIG_SYSTEM",
|
|
75
|
-
"GIT_AUTHOR_NAME",
|
|
76
|
-
"GIT_AUTHOR_EMAIL",
|
|
77
|
-
"GIT_COMMITTER_NAME",
|
|
78
|
-
"GIT_COMMITTER_EMAIL",
|
|
79
|
-
],
|
|
80
|
-
}),
|
|
81
|
-
LANG: "en_US.UTF-8",
|
|
82
|
-
LC_ALL: "en_US.UTF-8",
|
|
83
|
-
},
|
|
67
|
+
env: buildGitSafeEnv("en_US.UTF-8"),
|
|
84
68
|
windowsHide: true,
|
|
85
69
|
}).trim();
|
|
86
70
|
}
|
|
87
71
|
|
|
88
|
-
/** Build the sanitized env object shared by all git operations. */
|
|
89
|
-
function gitEnv(): Record<string, string> {
|
|
90
|
-
return {
|
|
91
|
-
...sanitizeEnvSecrets(process.env, {
|
|
92
|
-
allowList: [
|
|
93
|
-
"PATH",
|
|
94
|
-
"HOME",
|
|
95
|
-
"USER",
|
|
96
|
-
...WINDOWS_ESSENTIAL_ENV_VARS,
|
|
97
|
-
"SHELL",
|
|
98
|
-
"TERM",
|
|
99
|
-
"LANG",
|
|
100
|
-
"LC_ALL",
|
|
101
|
-
"LC_COLLATE",
|
|
102
|
-
"LC_CTYPE",
|
|
103
|
-
"LC_MESSAGES",
|
|
104
|
-
"XDG_CONFIG_HOME",
|
|
105
|
-
"XDG_DATA_HOME",
|
|
106
|
-
"XDG_CACHE_HOME",
|
|
107
|
-
"NVM_BIN",
|
|
108
|
-
"NVM_DIR",
|
|
109
|
-
"NODE_PATH",
|
|
110
|
-
"GIT_CONFIG_GLOBAL",
|
|
111
|
-
"GIT_CONFIG_SYSTEM",
|
|
112
|
-
"GIT_AUTHOR_NAME",
|
|
113
|
-
"GIT_AUTHOR_EMAIL",
|
|
114
|
-
"GIT_COMMITTER_NAME",
|
|
115
|
-
"GIT_COMMITTER_EMAIL",
|
|
116
|
-
],
|
|
117
|
-
}),
|
|
118
|
-
LANG: "en_US.UTF-8",
|
|
119
|
-
LC_ALL: "en_US.UTF-8",
|
|
120
|
-
};
|
|
121
|
-
}
|
|
122
|
-
|
|
123
72
|
async function gitAsync(cwd: string, args: string[]): Promise<string> {
|
|
124
|
-
|
|
73
|
+
// MED-1: same hardening as the sync git() helper (see git-hardening.ts).
|
|
74
|
+
// R-A3: per-command gitdir re-verify (see git() above).
|
|
75
|
+
assertWorktreePointerTrusted(cwd);
|
|
76
|
+
const { stdout } = await execFileAsync("git", hardenGitArgs(args), {
|
|
125
77
|
cwd,
|
|
126
78
|
encoding: "utf-8",
|
|
127
|
-
env:
|
|
79
|
+
env: buildGitSafeEnv("en_US.UTF-8"),
|
|
128
80
|
windowsHide: true,
|
|
129
81
|
});
|
|
130
82
|
return stdout.trim();
|
|
@@ -481,14 +433,11 @@ function branchExists(repoRoot: string, branch: string): { local: boolean; remot
|
|
|
481
433
|
logInternalError("worktree.branch-local.sync", error, `branch=${branch}`, "debug");
|
|
482
434
|
}
|
|
483
435
|
if (local) return { local: true, remoteOnly: false };
|
|
484
|
-
// Check remote-tracking branch
|
|
436
|
+
// Check remote-tracking branch (R-A7: routed through the hardened git()
|
|
437
|
+
// helper — sanitized env + hardenGitArgs uniformity; repoRoot cwd is a
|
|
438
|
+
// trusted main-repo context, no exec vector was reachable raw either).
|
|
485
439
|
try {
|
|
486
|
-
const out =
|
|
487
|
-
cwd: repoRoot,
|
|
488
|
-
encoding: "utf-8",
|
|
489
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
490
|
-
windowsHide: true,
|
|
491
|
-
}).trim();
|
|
440
|
+
const out = git(repoRoot, ["for-each-ref", "--format=%(refname)", `refs/remotes/*/${branch}`]);
|
|
492
441
|
return { local: false, remoteOnly: out.length > 0 };
|
|
493
442
|
} catch {
|
|
494
443
|
return { local: false, remoteOnly: false };
|
|
@@ -497,10 +446,9 @@ function branchExists(repoRoot: string, branch: string): { local: boolean; remot
|
|
|
497
446
|
|
|
498
447
|
function pruneStaleWorktrees(repoRoot: string): void {
|
|
499
448
|
try {
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
});
|
|
449
|
+
// R-A7 (uniformity): previously a raw execFileSync with the FULL
|
|
450
|
+
// process.env — now the hardened helper (sanitized env; repoRoot cwd).
|
|
451
|
+
git(repoRoot, ["worktree", "prune"]);
|
|
504
452
|
} catch {
|
|
505
453
|
/* best-effort */
|
|
506
454
|
}
|
|
@@ -534,15 +482,9 @@ async function branchExistsAsync(repoRoot: string, branch: string): Promise<{ lo
|
|
|
534
482
|
logInternalError("worktree.branch-local.async", error, `branch=${branch}`, "debug");
|
|
535
483
|
}
|
|
536
484
|
if (local) return { local: true, remoteOnly: false };
|
|
537
|
-
// Check remote-tracking branch
|
|
485
|
+
// Check remote-tracking branch (R-A7: hardened helper, see branchExists).
|
|
538
486
|
try {
|
|
539
|
-
const out = (
|
|
540
|
-
await execFileAsync("git", ["for-each-ref", "--format=%(refname)", `refs/remotes/*/${branch}`], {
|
|
541
|
-
cwd: repoRoot,
|
|
542
|
-
encoding: "utf-8",
|
|
543
|
-
windowsHide: true,
|
|
544
|
-
})
|
|
545
|
-
).stdout.trim();
|
|
487
|
+
const out = await gitAsync(repoRoot, ["for-each-ref", "--format=%(refname)", `refs/remotes/*/${branch}`]);
|
|
546
488
|
return { local: false, remoteOnly: out.length > 0 };
|
|
547
489
|
} catch {
|
|
548
490
|
return { local: false, remoteOnly: false };
|
|
@@ -565,10 +507,8 @@ async function pruneStaleWorktreesAsync(repoRoot: string): Promise<void> {
|
|
|
565
507
|
if (inFlight) return inFlight;
|
|
566
508
|
const p = (async () => {
|
|
567
509
|
try {
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
windowsHide: true,
|
|
571
|
-
});
|
|
510
|
+
// R-A7 (uniformity): sanitized env via the hardened helper.
|
|
511
|
+
await gitAsync(repoRoot, ["worktree", "prune"]);
|
|
572
512
|
} catch {
|
|
573
513
|
/* best-effort */
|
|
574
514
|
}
|
|
@@ -1045,6 +985,21 @@ export function prepareTaskWorkspace(
|
|
|
1045
985
|
worktreeExists = false;
|
|
1046
986
|
}
|
|
1047
987
|
if (worktreeExists) {
|
|
988
|
+
// MED-1 gitdir-hijack guard: before ANY git runs with cwd inside this
|
|
989
|
+
// worktree, its `.git` pointer must resolve to the git-REGISTERED admin dir
|
|
990
|
+
// (registry: <commonGitDir>/worktrees/<name>/gitdir — trusted main-repo state).
|
|
991
|
+
// A worker with write access to the worktree can repoint the pointer at a
|
|
992
|
+
// poisoned gitdir; fail closed instead of executing its config.
|
|
993
|
+
const commonGitDir = resolveTrustedGitDir(repoRoot);
|
|
994
|
+
if (!commonGitDir) {
|
|
995
|
+
throw new Error(`Cannot resolve the trusted git dir for ${repoRoot}; refusing to reuse worktree at ${worktreePath}`);
|
|
996
|
+
}
|
|
997
|
+
const gitdirCheck = verifyWorktreeGitdirPointer(worktreePath, commonGitDir);
|
|
998
|
+
if (!gitdirCheck.ok) {
|
|
999
|
+
throw new Error(
|
|
1000
|
+
`Refusing to reuse worktree at ${worktreePath}: gitdir pointer check failed (${gitdirCheck.reason}) — possible tampering. Remove the worktree manually or run cleanup with force=true.`,
|
|
1001
|
+
);
|
|
1002
|
+
}
|
|
1048
1003
|
let currentBranch: string;
|
|
1049
1004
|
try {
|
|
1050
1005
|
currentBranch = git(worktreePath, ["rev-parse", "--abbrev-ref", "HEAD"]);
|
|
@@ -1228,6 +1183,18 @@ export async function prepareTaskWorkspaceAsync(
|
|
|
1228
1183
|
worktreeExists = false;
|
|
1229
1184
|
}
|
|
1230
1185
|
if (worktreeExists) {
|
|
1186
|
+
// MED-1 gitdir-hijack guard (async mirror): verify the `.git` pointer
|
|
1187
|
+
// against the trusted worktree registry BEFORE any cwd-inside git call.
|
|
1188
|
+
const commonGitDir = resolveTrustedGitDir(repoRoot);
|
|
1189
|
+
if (!commonGitDir) {
|
|
1190
|
+
throw new Error(`Cannot resolve the trusted git dir for ${repoRoot}; refusing to reuse worktree at ${worktreePath}`);
|
|
1191
|
+
}
|
|
1192
|
+
const gitdirCheck = verifyWorktreeGitdirPointer(worktreePath, commonGitDir);
|
|
1193
|
+
if (!gitdirCheck.ok) {
|
|
1194
|
+
throw new Error(
|
|
1195
|
+
`Refusing to reuse worktree at ${worktreePath}: gitdir pointer check failed (${gitdirCheck.reason}) — possible tampering. Remove the worktree manually or run cleanup with force=true.`,
|
|
1196
|
+
);
|
|
1197
|
+
}
|
|
1231
1198
|
let currentBranch: string;
|
|
1232
1199
|
try {
|
|
1233
1200
|
currentBranch = await gitAsync(worktreePath, ["rev-parse", "--abbrev-ref", "HEAD"]);
|
|
@@ -1344,7 +1311,32 @@ export async function prepareTaskWorkspaceAsync(
|
|
|
1344
1311
|
}
|
|
1345
1312
|
}
|
|
1346
1313
|
|
|
1314
|
+
/** R-A1 (MED-1 follow-up): fail-closed capture guard. `git diff` (and
|
|
1315
|
+
* `--stat`/`--numstat`) runs `filter.<x>.clean` when it hashes modified
|
|
1316
|
+
* content whose `.gitattributes` assigns a filter, so a worker planting
|
|
1317
|
+
* `.gitattributes filter=fx` plus a flipped `.git` pointer to a poisoned
|
|
1318
|
+
* gitdir config could execute the filter payload in the orchestrator context
|
|
1319
|
+
* (hardenGitArgs does NOT neutralize filter definitions — there is no
|
|
1320
|
+
* --no-filters for diff). The pointer guard therefore runs INSIDE every
|
|
1321
|
+
* capture function so every caller is covered; on mismatch NO git runs and
|
|
1322
|
+
* the fail-safe result (empty diff / zero stat) is returned. */
|
|
1323
|
+
function captureGuardOk(worktreePath: string, fn: string): boolean {
|
|
1324
|
+
const trust = worktreeTrustCheck(worktreePath);
|
|
1325
|
+
if (trust.kind === "worktree" && !trust.ok) {
|
|
1326
|
+
logInternalError(
|
|
1327
|
+
"worktree.capture.gitdirMismatch",
|
|
1328
|
+
new Error(`worktree corrupted — skipping ${fn} for ${worktreePath}: ${trust.reason}`),
|
|
1329
|
+
`worktreePath=${worktreePath}`,
|
|
1330
|
+
);
|
|
1331
|
+
return false;
|
|
1332
|
+
}
|
|
1333
|
+
return true;
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1347
1336
|
export function captureWorktreeDiffStat(worktreePath: string): WorktreeDiffStat {
|
|
1337
|
+
if (!captureGuardOk(worktreePath, "captureWorktreeDiffStat")) {
|
|
1338
|
+
return { filesChanged: 0, insertions: 0, deletions: 0, diffStat: "" };
|
|
1339
|
+
}
|
|
1348
1340
|
try {
|
|
1349
1341
|
const diffStat = git(worktreePath, ["diff", "--stat"]);
|
|
1350
1342
|
const numstat = git(worktreePath, ["diff", "--numstat"]);
|
|
@@ -1364,6 +1356,9 @@ export function captureWorktreeDiffStat(worktreePath: string): WorktreeDiffStat
|
|
|
1364
1356
|
}
|
|
1365
1357
|
|
|
1366
1358
|
export async function captureWorktreeDiffStatAsync(worktreePath: string): Promise<WorktreeDiffStat> {
|
|
1359
|
+
if (!captureGuardOk(worktreePath, "captureWorktreeDiffStatAsync")) {
|
|
1360
|
+
return { filesChanged: 0, insertions: 0, deletions: 0, diffStat: "" };
|
|
1361
|
+
}
|
|
1367
1362
|
try {
|
|
1368
1363
|
const diffStat = await gitAsync(worktreePath, ["diff", "--stat"]);
|
|
1369
1364
|
const numstat = await gitAsync(worktreePath, ["diff", "--numstat"]);
|
|
@@ -1383,6 +1378,9 @@ export async function captureWorktreeDiffStatAsync(worktreePath: string): Promis
|
|
|
1383
1378
|
}
|
|
1384
1379
|
|
|
1385
1380
|
export function captureWorktreeDiff(worktreePath: string): string {
|
|
1381
|
+
if (!captureGuardOk(worktreePath, "captureWorktreeDiff")) {
|
|
1382
|
+
return "";
|
|
1383
|
+
}
|
|
1386
1384
|
try {
|
|
1387
1385
|
return git(worktreePath, ["diff", "--stat"]) + "\n\n" + git(worktreePath, ["diff"]);
|
|
1388
1386
|
} catch (error) {
|
|
@@ -1392,6 +1390,9 @@ export function captureWorktreeDiff(worktreePath: string): string {
|
|
|
1392
1390
|
}
|
|
1393
1391
|
|
|
1394
1392
|
export async function captureWorktreeDiffAsync(worktreePath: string): Promise<string> {
|
|
1393
|
+
if (!captureGuardOk(worktreePath, "captureWorktreeDiffAsync")) {
|
|
1394
|
+
return "";
|
|
1395
|
+
}
|
|
1395
1396
|
try {
|
|
1396
1397
|
const [stat, full] = await Promise.all([gitAsync(worktreePath, ["diff", "--stat"]), gitAsync(worktreePath, ["diff"])]);
|
|
1397
1398
|
return stat + "\n\n" + full;
|
|
@@ -1524,27 +1525,50 @@ function captureAgentWorktreeRecovery(manifest: TeamRunManifest, worktreePath: s
|
|
|
1524
1525
|
* worktree/branch leak never crashes a workflow.
|
|
1525
1526
|
*/
|
|
1526
1527
|
export function cleanupAgentWorktree(manifest: TeamRunManifest, worktreePath: string, branch?: string): void {
|
|
1527
|
-
//
|
|
1528
|
+
// MED-1 gitdir-hijack guard: run git with cwd INSIDE the worktree only when its
|
|
1529
|
+
// `.git` pointer matches the trusted registry (see git-hardening.ts). On mismatch
|
|
1530
|
+
// skip the diff/status/recovery capture (log loudly) — removal below still
|
|
1531
|
+
// happens, but only via the trusted main-repo cwd.
|
|
1532
|
+
let gitdirOk = false;
|
|
1528
1533
|
try {
|
|
1529
|
-
const
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
content: diff,
|
|
1535
|
-
producer: "dynamic-workflow",
|
|
1536
|
-
});
|
|
1537
|
-
}
|
|
1538
|
-
} catch (error) {
|
|
1539
|
-
logInternalError("worktree.agent-cleanup.diff", error, `worktreePath=${worktreePath}`);
|
|
1534
|
+
const agentRepoRoot = findGitRoot(manifest.cwd);
|
|
1535
|
+
const agentCommonGitDir = resolveTrustedGitDir(agentRepoRoot);
|
|
1536
|
+
gitdirOk = agentCommonGitDir !== undefined && verifyWorktreeGitdirPointer(worktreePath, agentCommonGitDir).ok;
|
|
1537
|
+
} catch {
|
|
1538
|
+
gitdirOk = false;
|
|
1540
1539
|
}
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1540
|
+
if (!gitdirOk) {
|
|
1541
|
+
logInternalError(
|
|
1542
|
+
"worktree.agent-cleanup.gitdirMismatch",
|
|
1543
|
+
new Error(
|
|
1544
|
+
`gitdir pointer check failed for ${worktreePath} — skipping diff/recovery capture; removing via main-repo git only (MED-1)`,
|
|
1545
|
+
),
|
|
1546
|
+
`runId=${manifest.runId}`,
|
|
1547
|
+
);
|
|
1548
|
+
}
|
|
1549
|
+
if (gitdirOk) {
|
|
1550
|
+
// Capture diff as artifact (best-effort).
|
|
1551
|
+
try {
|
|
1552
|
+
const diff = captureWorktreeDiff(worktreePath);
|
|
1553
|
+
if (diff.trim() && !diff.startsWith("Failed to capture worktree diff")) {
|
|
1554
|
+
writeArtifact(manifest.artifactsRoot, {
|
|
1555
|
+
kind: "diff",
|
|
1556
|
+
relativePath: `wf/worktree-diff-${Date.now()}-${randomBytes(2).toString("hex")}.diff`,
|
|
1557
|
+
content: diff,
|
|
1558
|
+
producer: "dynamic-workflow",
|
|
1559
|
+
});
|
|
1560
|
+
}
|
|
1561
|
+
} catch (error) {
|
|
1562
|
+
logInternalError("worktree.agent-cleanup.diff", error, `worktreePath=${worktreePath}`);
|
|
1563
|
+
}
|
|
1564
|
+
// R36-M1: `git diff` above cannot see untracked files — capture them too, or
|
|
1565
|
+
// the force-remove below destroys the agent's new files with NO artifact.
|
|
1566
|
+
try {
|
|
1567
|
+
const dirtyStatus = git(worktreePath, ["-c", "core.quotePath=false", "status", "--porcelain", "-uall"]);
|
|
1568
|
+
if (/^\?\? /m.test(dirtyStatus)) captureAgentWorktreeRecovery(manifest, worktreePath, dirtyStatus);
|
|
1569
|
+
} catch (error) {
|
|
1570
|
+
logInternalError("worktree.agent-cleanup.status", error, `worktreePath=${worktreePath}`);
|
|
1571
|
+
}
|
|
1548
1572
|
}
|
|
1549
1573
|
// Remove worktree (best-effort). Try git first, then fall back to fs.rm.
|
|
1550
1574
|
try {
|
|
@@ -1579,26 +1603,49 @@ export function cleanupAgentWorktree(manifest: TeamRunManifest, worktreePath: st
|
|
|
1579
1603
|
|
|
1580
1604
|
/** Async version of cleanupAgentWorktree — caches findGitRoot and yields during git operations. */
|
|
1581
1605
|
export async function cleanupAgentWorktreeAsync(manifest: TeamRunManifest, worktreePath: string, branch?: string): Promise<void> {
|
|
1582
|
-
//
|
|
1606
|
+
// MED-1 gitdir-hijack guard: run git with cwd INSIDE the worktree only when its
|
|
1607
|
+
// `.git` pointer matches the trusted registry (see git-hardening.ts). On mismatch
|
|
1608
|
+
// skip the diff/status/recovery capture (log loudly) — removal below still
|
|
1609
|
+
// happens, but only via the trusted main-repo cwd.
|
|
1610
|
+
let gitdirOk = false;
|
|
1583
1611
|
try {
|
|
1584
|
-
const
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
content: diff,
|
|
1590
|
-
producer: "dynamic-workflow",
|
|
1591
|
-
});
|
|
1592
|
-
}
|
|
1593
|
-
} catch (error) {
|
|
1594
|
-
logInternalError("worktree.agent-cleanup.diff", error, `worktreePath=${worktreePath}`);
|
|
1612
|
+
const agentRepoRoot = await findGitRootAsync(manifest.cwd);
|
|
1613
|
+
const agentCommonGitDir = resolveTrustedGitDir(agentRepoRoot);
|
|
1614
|
+
gitdirOk = agentCommonGitDir !== undefined && verifyWorktreeGitdirPointer(worktreePath, agentCommonGitDir).ok;
|
|
1615
|
+
} catch {
|
|
1616
|
+
gitdirOk = false;
|
|
1595
1617
|
}
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1618
|
+
if (!gitdirOk) {
|
|
1619
|
+
logInternalError(
|
|
1620
|
+
"worktree.agent-cleanup.gitdirMismatch",
|
|
1621
|
+
new Error(
|
|
1622
|
+
`gitdir pointer check failed for ${worktreePath} — skipping diff/recovery capture; removing via main-repo git only (MED-1)`,
|
|
1623
|
+
),
|
|
1624
|
+
`runId=${manifest.runId}`,
|
|
1625
|
+
);
|
|
1626
|
+
}
|
|
1627
|
+
if (gitdirOk) {
|
|
1628
|
+
// Capture diff as artifact (best-effort).
|
|
1629
|
+
try {
|
|
1630
|
+
const diff = await captureWorktreeDiffAsync(worktreePath);
|
|
1631
|
+
if (diff.trim() && !diff.startsWith("Failed to capture worktree diff")) {
|
|
1632
|
+
writeArtifact(manifest.artifactsRoot, {
|
|
1633
|
+
kind: "diff",
|
|
1634
|
+
relativePath: `wf/worktree-diff-${Date.now()}-${randomBytes(2).toString("hex")}.diff`,
|
|
1635
|
+
content: diff,
|
|
1636
|
+
producer: "dynamic-workflow",
|
|
1637
|
+
});
|
|
1638
|
+
}
|
|
1639
|
+
} catch (error) {
|
|
1640
|
+
logInternalError("worktree.agent-cleanup.diff", error, `worktreePath=${worktreePath}`);
|
|
1641
|
+
}
|
|
1642
|
+
// R36-M1: async mirror — untracked files are invisible to `git diff`.
|
|
1643
|
+
try {
|
|
1644
|
+
const dirtyStatus = await gitAsync(worktreePath, ["-c", "core.quotePath=false", "status", "--porcelain", "-uall"]);
|
|
1645
|
+
if (/^\?\? /m.test(dirtyStatus)) captureAgentWorktreeRecovery(manifest, worktreePath, dirtyStatus);
|
|
1646
|
+
} catch (error) {
|
|
1647
|
+
logInternalError("worktree.agent-cleanup.status", error, `worktreePath=${worktreePath}`);
|
|
1648
|
+
}
|
|
1602
1649
|
}
|
|
1603
1650
|
// Resolve repoRoot once and reuse (cache optimization).
|
|
1604
1651
|
let repoRoot: string | undefined;
|