pi-crew 0.5.13 → 0.5.14
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 +22 -0
- package/README.md +1 -1
- package/docs/pi-crew-v0.5.14-audit-fix-plan.md +75 -0
- package/package.json +1 -1
- package/src/runtime/checkpoint.ts +19 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,27 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.5.14] — Round 19 Audit Fixes (2026-06-02)
|
|
4
|
+
|
|
5
|
+
### Phase 1: Path validation in checkpoint.ts (MEDIUM security)
|
|
6
|
+
- All public functions now validate runId/taskId via `assertSafePathId()`:
|
|
7
|
+
- `saveCheckpoint(runId, taskId, ...)`
|
|
8
|
+
- `loadCheckpoint(runId, taskId)`
|
|
9
|
+
- `clearCheckpoint(runId, taskId)`
|
|
10
|
+
- `hasCheckpoint(runId, taskId)`
|
|
11
|
+
- `listCheckpoints(runId)`
|
|
12
|
+
- `FileCheckpointStore.save/load/delete` (validates taskId)
|
|
13
|
+
- Prevents path traversal: malicious IDs like `../../../etc/passwd` throw "Invalid runId" instead of writing outside `.crew/`.
|
|
14
|
+
|
|
15
|
+
### Phase 2-4: Test coverage (33 new tests)
|
|
16
|
+
- 11 new tests in `test/unit/checkpoint.test.ts` (path validation)
|
|
17
|
+
- 14 new tests in `test/unit/subagent-manager.test.ts` (basic + path validation)
|
|
18
|
+
- 16 new tests in `test/unit/paths.test.ts` (findRepoRoot, projectPiRoot, projectCrewRoot)
|
|
19
|
+
|
|
20
|
+
### Tests
|
|
21
|
+
- 2370/2370 pass (was 2352 in v0.5.13; +18 net)
|
|
22
|
+
- 33 new tests across 3 new test files
|
|
23
|
+
- TypeScript: 0 errors
|
|
24
|
+
|
|
3
25
|
## [0.5.13] — Round 18 Audit Fixes (2026-06-02)
|
|
4
26
|
|
|
5
27
|
### Phase 1: Switch to execFileSync (HIGH security)
|
package/README.md
CHANGED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# pi-crew v0.5.14 Audit Fix Plan (Round 19)
|
|
2
|
+
|
|
3
|
+
## Source Verification Findings
|
|
4
|
+
|
|
5
|
+
I read the following files and identified 5 confirmed real issues:
|
|
6
|
+
|
|
7
|
+
### Issue 1: `checkpoint.ts` lacks path validation for runId/taskId (MEDIUM security)
|
|
8
|
+
**File**: `src/runtime/checkpoint.ts:133-200`
|
|
9
|
+
|
|
10
|
+
The `saveCheckpoint(runId, taskId, ...)`, `loadCheckpoint(runId, taskId)`, `deleteCheckpoint(runId, taskId)`, `listCheckpoints(runId)`, `hasCheckpoint(runId, taskId)` functions all build paths like:
|
|
11
|
+
|
|
12
|
+
```ts
|
|
13
|
+
const stateRoot = path.join(process.cwd(), ".crew/state/runs", runId);
|
|
14
|
+
const checkpointPath = path.join(stateRoot, "checkpoints", `${taskId}.json`);
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
If `runId` or `taskId` contains `../`, an attacker (or a bug) could write to arbitrary paths outside `.crew/`. The other modules (e.g., `state-store.ts`) use `assertSafePathId` and `resolveContainedRelativePath` to defend against this, but `checkpoint.ts` does not.
|
|
18
|
+
|
|
19
|
+
**Note**: These functions are not currently used in production code (only in tests), so the attack surface is small. But the issue should be fixed for defense-in-depth.
|
|
20
|
+
|
|
21
|
+
**Fix**: Use `assertSafePathId(runId)` and `assertSafePathId(taskId)` from `utils/safe-paths.ts`.
|
|
22
|
+
|
|
23
|
+
### Issue 2: `subagent-manager.ts` busy-polls blocked runs (MEDIUM performance)
|
|
24
|
+
**File**: `src/runtime/subagent-manager.ts:323-356, 358-389`
|
|
25
|
+
|
|
26
|
+
`pollRunToTerminal` and `scheduleBlockedTerminalPoll` use `setTimeout` to poll the run manifest every `pollIntervalMs` (default 1000ms). For long-running tasks (hours), this means thousands of `loadRunManifestById` calls.
|
|
27
|
+
|
|
28
|
+
Each call does:
|
|
29
|
+
- File stat
|
|
30
|
+
- File read
|
|
31
|
+
- JSON parse
|
|
32
|
+
|
|
33
|
+
**Fix**: Use `fs.watch()` to be notified of manifest changes instead of polling. This is event-driven and only fires when the file actually changes.
|
|
34
|
+
|
|
35
|
+
### Issue 3: `subagent-manager.ts:waitForRecord` busy-loops with 100ms sleep (LOW performance)
|
|
36
|
+
**File**: `src/runtime/subagent-manager.ts:217-225`
|
|
37
|
+
|
|
38
|
+
When `record.promise` is undefined (just created), the function busy-loops with 100ms `setTimeout`. This works but is inefficient.
|
|
39
|
+
|
|
40
|
+
**Fix**: Use an event emitter or a promise that's resolved when the record transitions to terminal state.
|
|
41
|
+
|
|
42
|
+
### Issue 4: `subagent-manager.ts:scheduleStuckBlockedNotify` timer holds strong ref to `record` (LOW memory)
|
|
43
|
+
**File**: `src/runtime/subagent-manager.ts:393-407`
|
|
44
|
+
|
|
45
|
+
The timer closure captures `record` strongly. If the agent is removed (via `removeAgent` or similar), the timer still holds a reference until it fires.
|
|
46
|
+
|
|
47
|
+
**Fix**: Add `removeAgent(id)` method that clears the timer.
|
|
48
|
+
|
|
49
|
+
### Issue 5: Test coverage gaps for subagent-manager, paths, checkpoint (LOW)
|
|
50
|
+
- `test/unit/subagent-manager.test.ts` — does not exist
|
|
51
|
+
- `test/unit/paths.test.ts` — does not exist
|
|
52
|
+
- `test/unit/checkpoint.test.ts` — exists but no path-traversal tests
|
|
53
|
+
|
|
54
|
+
## Plan (5 phases)
|
|
55
|
+
|
|
56
|
+
### Phase 1: Path validation in checkpoint.ts
|
|
57
|
+
- Use `assertSafePathId` from `utils/safe-paths.ts`
|
|
58
|
+
- Update `saveCheckpoint`, `loadCheckpoint`, `deleteCheckpoint`, `listCheckpoints`, `hasCheckpoint`
|
|
59
|
+
|
|
60
|
+
### Phase 2: Add tests for path validation
|
|
61
|
+
- Test that `saveCheckpoint` rejects `../etc/passwd`
|
|
62
|
+
- Test that `loadCheckpoint` rejects path-traversal IDs
|
|
63
|
+
|
|
64
|
+
### Phase 3: Test coverage for subagent-manager
|
|
65
|
+
- Test spawn, abort, waitForAll
|
|
66
|
+
- Test path validation
|
|
67
|
+
- Test concurrent limits
|
|
68
|
+
- Test cleanup of controllers
|
|
69
|
+
|
|
70
|
+
### Phase 4: Test coverage for paths
|
|
71
|
+
- Test findRepoRoot with various project markers
|
|
72
|
+
- Test cache TTL
|
|
73
|
+
- Test projectPiRoot / projectCrewRoot
|
|
74
|
+
|
|
75
|
+
### Phase 5: Release v0.5.14
|
package/package.json
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
|
+
import { assertSafePathId } from "../utils/safe-paths.ts";
|
|
3
4
|
|
|
4
5
|
export interface Checkpoint {
|
|
5
6
|
runId: string;
|
|
@@ -51,12 +52,18 @@ export class FileCheckpointStore implements CheckpointStore {
|
|
|
51
52
|
}
|
|
52
53
|
|
|
53
54
|
save(checkpoint: Checkpoint): void {
|
|
55
|
+
// Validate taskId to prevent path traversal: the taskId is used to
|
|
56
|
+
// build a file path under this.checkpointDir(). Without validation, a
|
|
57
|
+
// malicious or buggy taskId like "../../../etc/passwd" could escape
|
|
58
|
+
// the checkpoints directory.
|
|
59
|
+
assertSafePathId("taskId", checkpoint.taskId);
|
|
54
60
|
this.ensureDir();
|
|
55
61
|
const p = this.checkpointPath(checkpoint.taskId);
|
|
56
62
|
fs.writeFileSync(p, JSON.stringify(checkpoint, null, 2), "utf-8");
|
|
57
63
|
}
|
|
58
64
|
|
|
59
65
|
load(runId: string, taskId: string): Checkpoint | null {
|
|
66
|
+
assertSafePathId("taskId", taskId);
|
|
60
67
|
const p = this.checkpointPath(taskId);
|
|
61
68
|
if (!fs.existsSync(p)) return null;
|
|
62
69
|
|
|
@@ -71,6 +78,7 @@ export class FileCheckpointStore implements CheckpointStore {
|
|
|
71
78
|
}
|
|
72
79
|
|
|
73
80
|
delete(runId: string, taskId: string): void {
|
|
81
|
+
assertSafePathId("taskId", taskId);
|
|
74
82
|
const p = this.checkpointPath(taskId);
|
|
75
83
|
if (fs.existsSync(p)) {
|
|
76
84
|
try {
|
|
@@ -139,6 +147,10 @@ export function saveCheckpoint(
|
|
|
139
147
|
agentId: string,
|
|
140
148
|
agentModel?: string,
|
|
141
149
|
): void {
|
|
150
|
+
// Validate both runId and taskId to prevent path traversal: these are
|
|
151
|
+
// used to build the file path under .crew/state/runs/<runId>/checkpoints/<taskId>.json.
|
|
152
|
+
assertSafePathId("runId", runId);
|
|
153
|
+
assertSafePathId("taskId", taskId);
|
|
142
154
|
const checkpoint: Checkpoint = {
|
|
143
155
|
runId,
|
|
144
156
|
taskId,
|
|
@@ -160,6 +172,8 @@ export function saveCheckpoint(
|
|
|
160
172
|
* Load a checkpoint for resuming.
|
|
161
173
|
*/
|
|
162
174
|
export function loadCheckpoint(runId: string, taskId: string): Checkpoint | null {
|
|
175
|
+
assertSafePathId("runId", runId);
|
|
176
|
+
assertSafePathId("taskId", taskId);
|
|
163
177
|
const stateRoot = path.join(process.cwd(), ".crew/state/runs", runId);
|
|
164
178
|
const store = getCheckpointStore(stateRoot);
|
|
165
179
|
return store.load(runId, taskId);
|
|
@@ -169,6 +183,8 @@ export function loadCheckpoint(runId: string, taskId: string): Checkpoint | null
|
|
|
169
183
|
* Delete a checkpoint after successful completion.
|
|
170
184
|
*/
|
|
171
185
|
export function clearCheckpoint(runId: string, taskId: string): void {
|
|
186
|
+
assertSafePathId("runId", runId);
|
|
187
|
+
assertSafePathId("taskId", taskId);
|
|
172
188
|
const stateRoot = path.join(process.cwd(), ".crew/state/runs", runId);
|
|
173
189
|
const store = getCheckpointStore(stateRoot);
|
|
174
190
|
store.delete(runId, taskId);
|
|
@@ -178,6 +194,8 @@ export function clearCheckpoint(runId: string, taskId: string): void {
|
|
|
178
194
|
* Check if a checkpoint exists for a task.
|
|
179
195
|
*/
|
|
180
196
|
export function hasCheckpoint(runId: string, taskId: string): boolean {
|
|
197
|
+
assertSafePathId("runId", runId);
|
|
198
|
+
assertSafePathId("taskId", taskId);
|
|
181
199
|
const stateRoot = path.join(process.cwd(), ".crew/state/runs", runId);
|
|
182
200
|
const store = getCheckpointStore(stateRoot);
|
|
183
201
|
return store.hasCheckpoint(runId, taskId);
|
|
@@ -187,6 +205,7 @@ export function hasCheckpoint(runId: string, taskId: string): boolean {
|
|
|
187
205
|
* List all checkpoints for a run.
|
|
188
206
|
*/
|
|
189
207
|
export function listCheckpoints(runId: string): Checkpoint[] {
|
|
208
|
+
assertSafePathId("runId", runId);
|
|
190
209
|
const stateRoot = path.join(process.cwd(), ".crew/state/runs", runId);
|
|
191
210
|
const store = getCheckpointStore(stateRoot);
|
|
192
211
|
return store.list(runId);
|