pi-crew 0.11.44 → 0.11.45
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 +45 -1
- package/dist/index.mjs +504 -504
- package/package.json +1 -1
- package/src/extension/cross-extension-rpc.ts +15 -6
- package/src/runtime/broker/crew-broker.ts +40 -33
- package/src/runtime/broker/socket-stop.ts +147 -0
- package/src/runtime/child-pi/child-pi-kill.ts +44 -0
- 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
|
@@ -1,25 +1,12 @@
|
|
|
1
1
|
import { execFileSync } from "node:child_process";
|
|
2
|
-
import {
|
|
3
|
-
import { sanitizeEnvSecrets } from "../utils/env-filter.ts";
|
|
2
|
+
import { buildGitSafeEnv, hardenGitArgs } from "../worktree/git-hardening.ts";
|
|
4
3
|
|
|
5
|
-
// Read-only git operations only (rev-list, rev-parse, log).
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
"HOME",
|
|
12
|
-
"USER",
|
|
13
|
-
...WINDOWS_ESSENTIAL_ENV_VARS,
|
|
14
|
-
"SHELL",
|
|
15
|
-
"TERM",
|
|
16
|
-
"LANG",
|
|
17
|
-
"LC_ALL",
|
|
18
|
-
"GIT_CONFIG_GLOBAL",
|
|
19
|
-
"GIT_CONFIG_SYSTEM",
|
|
20
|
-
"GIT_EXEC_PATH",
|
|
21
|
-
],
|
|
22
|
-
});
|
|
4
|
+
// Read-only git operations only (rev-list, rev-parse, log). C-1 fix
|
|
5
|
+
// (code-review 2026-06-23): never inherit the full parent env — API keys
|
|
6
|
+
// must not reach git hooks/aliases/credential-helpers. R-A6 (MED-1
|
|
7
|
+
// uniformity): env + argv now come from the shared git-hardening helpers
|
|
8
|
+
// (sanitizeEnvSecrets allowList, GIT_CONFIG_NOSYSTEM=1, config-injection
|
|
9
|
+
// overrides) instead of a drifting local GIT_SAFE_ENV copy.
|
|
23
10
|
|
|
24
11
|
export type BranchFreshnessStatus = "fresh" | "stale" | "diverged" | "unknown";
|
|
25
12
|
export type StaleBranchPolicy = "warn" | "block" | "auto_rebase" | "auto_merge_forward";
|
|
@@ -36,11 +23,11 @@ export interface BranchFreshness {
|
|
|
36
23
|
}
|
|
37
24
|
|
|
38
25
|
function git(cwd: string, args: string[]): string {
|
|
39
|
-
return execFileSync("git", args, {
|
|
26
|
+
return execFileSync("git", hardenGitArgs(args), {
|
|
40
27
|
cwd,
|
|
28
|
+
env: buildGitSafeEnv(),
|
|
41
29
|
encoding: "utf-8",
|
|
42
30
|
stdio: ["ignore", "pipe", "pipe"],
|
|
43
|
-
env: GIT_SAFE_ENV,
|
|
44
31
|
windowsHide: true,
|
|
45
32
|
}).trim();
|
|
46
33
|
}
|
package/src/worktree/cleanup.ts
CHANGED
|
@@ -4,10 +4,15 @@ import * as path from "node:path";
|
|
|
4
4
|
import { DEFAULT_PATHS } from "../config/defaults.ts";
|
|
5
5
|
import { writeArtifact } from "../state/stores/artifact-store.ts";
|
|
6
6
|
import type { TeamRunManifest } from "../state/types.ts";
|
|
7
|
-
import { WINDOWS_ESSENTIAL_ENV_VARS } from "../utils/env-allowlist.ts";
|
|
8
|
-
import { sanitizeEnvSecrets } from "../utils/env-filter.ts";
|
|
9
7
|
import { logInternalError } from "../utils/internal-error.ts";
|
|
10
8
|
import { projectCrewRoot } from "../utils/paths.ts";
|
|
9
|
+
import {
|
|
10
|
+
assertWorktreePointerTrusted,
|
|
11
|
+
buildGitSafeEnv,
|
|
12
|
+
hardenGitArgs,
|
|
13
|
+
resolveTrustedGitDir,
|
|
14
|
+
verifyWorktreeGitdirPointer,
|
|
15
|
+
} from "./git-hardening.ts";
|
|
11
16
|
|
|
12
17
|
export interface WorktreeCleanupResult {
|
|
13
18
|
removed: string[];
|
|
@@ -17,39 +22,16 @@ export interface WorktreeCleanupResult {
|
|
|
17
22
|
committedBranches: string[];
|
|
18
23
|
}
|
|
19
24
|
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
"TERM",
|
|
31
|
-
"LANG",
|
|
32
|
-
"LC_ALL",
|
|
33
|
-
"LC_COLLATE",
|
|
34
|
-
"LC_CTYPE",
|
|
35
|
-
"LC_MESSAGES",
|
|
36
|
-
"XDG_CONFIG_HOME",
|
|
37
|
-
"XDG_DATA_HOME",
|
|
38
|
-
"XDG_CACHE_HOME",
|
|
39
|
-
"NVM_BIN",
|
|
40
|
-
"NVM_DIR",
|
|
41
|
-
"NODE_PATH",
|
|
42
|
-
"GIT_CONFIG_GLOBAL",
|
|
43
|
-
"GIT_CONFIG_SYSTEM",
|
|
44
|
-
"GIT_AUTHOR_NAME",
|
|
45
|
-
"GIT_AUTHOR_EMAIL",
|
|
46
|
-
"GIT_COMMITTER_NAME",
|
|
47
|
-
"GIT_COMMITTER_EMAIL",
|
|
48
|
-
],
|
|
49
|
-
}),
|
|
50
|
-
LANG: "C",
|
|
51
|
-
LC_ALL: "C",
|
|
52
|
-
};
|
|
25
|
+
// MED-1 / R-A4: shared sanitized env for every git spawn (see
|
|
26
|
+
// buildGitSafeEnv in git-hardening.ts — allowList-based secret stripping,
|
|
27
|
+
// GIT_CONFIG_NOSYSTEM=1, GIT_CONFIG_SYSTEM dropped from the allowlist,
|
|
28
|
+
// GIT_CONFIG_GLOBAL kept for commit identity with the documented residual
|
|
29
|
+
// that trusted-config filter definitions remain executable).
|
|
30
|
+
// Built per call (not module-load) so late env changes (tests, config reload)
|
|
31
|
+
// are honored.
|
|
32
|
+
function gitSafeEnv(): Record<string, string> {
|
|
33
|
+
return buildGitSafeEnv();
|
|
34
|
+
}
|
|
53
35
|
|
|
54
36
|
function sanitizeBranchPart(value: string): string {
|
|
55
37
|
return (
|
|
@@ -66,12 +48,20 @@ function sanitizeFilename(value: string): string {
|
|
|
66
48
|
}
|
|
67
49
|
|
|
68
50
|
function git(cwd: string, args: string[]): string {
|
|
51
|
+
// R-A3: re-verify the gitdir pointer before EVERY command whose cwd sits
|
|
52
|
+
// inside a linked worktree (TOCTOU: the entry-level guard in
|
|
53
|
+
// cleanupRunWorktrees can be invalidated by a still-live worker flipping
|
|
54
|
+
// the `.git` pointer mid-chain). Throws OUTSIDE the execFile try so the
|
|
55
|
+
// error propagates unwrapped into the existing "corrupted" handling paths.
|
|
56
|
+
assertWorktreePointerTrusted(cwd);
|
|
69
57
|
try {
|
|
70
|
-
|
|
58
|
+
// MED-1: harden EVERY git invocation (config-injection overrides +
|
|
59
|
+
// diff/commit neutralizers). See src/worktree/git-hardening.ts.
|
|
60
|
+
return execFileSync("git", hardenGitArgs(args), {
|
|
71
61
|
cwd,
|
|
62
|
+
env: gitSafeEnv(),
|
|
72
63
|
encoding: "utf-8",
|
|
73
64
|
stdio: ["ignore", "pipe", "pipe"],
|
|
74
|
-
env: GIT_SAFE_ENV,
|
|
75
65
|
windowsHide: true,
|
|
76
66
|
}).trim();
|
|
77
67
|
} catch (error) {
|
|
@@ -116,6 +106,9 @@ export function cleanupRunWorktrees(
|
|
|
116
106
|
committedBranches: [],
|
|
117
107
|
};
|
|
118
108
|
if (!fs.existsSync(worktreeRoot)) return result;
|
|
109
|
+
// MED-1: resolve the TRUSTED git common dir once (from the orchestrator-owned
|
|
110
|
+
// main repo — never from inside a worktree) for the per-entry gitdir guard.
|
|
111
|
+
const trustedGitDir = resolveTrustedGitDir(manifest.cwd);
|
|
119
112
|
|
|
120
113
|
// M3 fix: use withFileTypes to avoid race between readdirSync and statSync.
|
|
121
114
|
// Rely on Dirent.isDirectory() instead of a separate statSync to eliminate TOCTOU window.
|
|
@@ -127,6 +120,46 @@ export function cleanupRunWorktrees(
|
|
|
127
120
|
if (options.signal?.aborted) break;
|
|
128
121
|
const worktreePath = path.join(worktreeRoot, entry.name);
|
|
129
122
|
if (options.signal?.aborted) break;
|
|
123
|
+
// MED-1 gitdir-hijack guard: a worker can rewrite the worktree `.git`
|
|
124
|
+
// pointer file to an attacker-controlled directory whose poisoned `config`
|
|
125
|
+
// would execute hooks/filters/fsmonitor in the orchestrator context. Before
|
|
126
|
+
// ANY git runs with cwd inside the worktree, the pointer must resolve to the
|
|
127
|
+
// git-registered admin dir (see src/worktree/git-hardening.ts). On mismatch
|
|
128
|
+
// NO git runs from inside; removal is only attempted (force) from the
|
|
129
|
+
// trusted main-repo cwd, and the worktree is otherwise preserved.
|
|
130
|
+
const gitdirCheck = trustedGitDir
|
|
131
|
+
? verifyWorktreeGitdirPointer(worktreePath, trustedGitDir)
|
|
132
|
+
: { ok: false, reason: `main repository git dir not found under ${manifest.cwd}` };
|
|
133
|
+
if (!gitdirCheck.ok) {
|
|
134
|
+
logInternalError(
|
|
135
|
+
"cleanup.worktree.gitdirMismatch",
|
|
136
|
+
new Error(`Refusing to run git inside ${worktreePath}: ${gitdirCheck.reason}`),
|
|
137
|
+
`runId=${manifest.runId}`,
|
|
138
|
+
);
|
|
139
|
+
if (options.force) {
|
|
140
|
+
let removed = false;
|
|
141
|
+
try {
|
|
142
|
+
git(manifest.cwd, ["worktree", "remove", "--force", worktreePath]);
|
|
143
|
+
removed = true;
|
|
144
|
+
} catch {
|
|
145
|
+
try {
|
|
146
|
+
fs.rmSync(worktreePath, { recursive: true, force: true });
|
|
147
|
+
removed = true;
|
|
148
|
+
} catch {
|
|
149
|
+
/* keep as preserved */
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
if (removed) {
|
|
153
|
+
result.removed.push(worktreePath);
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
result.preserved.push({
|
|
158
|
+
path: worktreePath,
|
|
159
|
+
reason: `worktree corrupted — gitdir pointer check failed (${gitdirCheck.reason}); no git executed inside; inspect manually or remove with force=true`,
|
|
160
|
+
});
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
130
163
|
const dirty = isDirty(worktreePath);
|
|
131
164
|
const branchName = `pi-crew/${manifest.runId}/${sanitizeBranchPart(entry.name)}`;
|
|
132
165
|
const safeBranchName = sanitizeBranchPart(entry.name);
|
|
@@ -156,13 +189,9 @@ export function cleanupRunWorktrees(
|
|
|
156
189
|
try {
|
|
157
190
|
// Issue 2 fix: verify status before and after add to ensure atomicity
|
|
158
191
|
const statusBefore = git(worktreePath, ["status", "--porcelain"]);
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
163
|
-
env: GIT_SAFE_ENV,
|
|
164
|
-
windowsHide: true,
|
|
165
|
-
});
|
|
192
|
+
// MED-1: routed through the hardened git() helper (fsmonitor/hooksPath
|
|
193
|
+
// disabled) — a worker-planted config must not execute on `git add`.
|
|
194
|
+
git(worktreePath, ["add", "-A"]);
|
|
166
195
|
// Verify no unexpected changes were added (only staged the original dirty files)
|
|
167
196
|
const statusAfterAdd = git(worktreePath, ["status", "--porcelain"]);
|
|
168
197
|
if (statusAfterAdd !== statusBefore) {
|
|
@@ -177,37 +206,21 @@ export function cleanupRunWorktrees(
|
|
|
177
206
|
if (safeDesc.includes("\n")) {
|
|
178
207
|
safeDesc = safeDesc.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]+/g, " ");
|
|
179
208
|
}
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
184
|
-
env: GIT_SAFE_ENV,
|
|
185
|
-
windowsHide: true,
|
|
186
|
-
});
|
|
209
|
+
// MED-1: --no-verify + hooksPath= neutralize every commit hook
|
|
210
|
+
// (pre-commit/commit-msg/prepare-commit-msg/post-commit).
|
|
211
|
+
git(worktreePath, ["commit", "-m", `pi-crew: ${safeDesc}`]);
|
|
187
212
|
// Issue 1 fix: check signal before branch creation
|
|
188
213
|
if (options.signal?.aborted) break;
|
|
189
214
|
// Create branch in the main repo pointing to this worktree's HEAD
|
|
190
215
|
let branchError: Error | null = null;
|
|
191
216
|
try {
|
|
192
|
-
|
|
193
|
-
cwd: worktreePath,
|
|
194
|
-
encoding: "utf-8",
|
|
195
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
196
|
-
env: GIT_SAFE_ENV,
|
|
197
|
-
windowsHide: true,
|
|
198
|
-
});
|
|
217
|
+
git(worktreePath, ["branch", branchName]);
|
|
199
218
|
} catch (err) {
|
|
200
219
|
branchError = err instanceof Error ? err : new Error(String(err));
|
|
201
220
|
// Branch already exists — use timestamp suffix
|
|
202
221
|
const tsBranch = `${branchName}-${Date.now()}`;
|
|
203
222
|
try {
|
|
204
|
-
|
|
205
|
-
cwd: worktreePath,
|
|
206
|
-
encoding: "utf-8",
|
|
207
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
208
|
-
env: GIT_SAFE_ENV,
|
|
209
|
-
windowsHide: true,
|
|
210
|
-
});
|
|
223
|
+
git(worktreePath, ["branch", tsBranch]);
|
|
211
224
|
} catch (err2) {
|
|
212
225
|
// Both branch attempts failed — accumulate error for outer catch
|
|
213
226
|
const err2_msg = err2 instanceof Error ? err2.message : String(err2);
|