pi-crew 0.5.19 → 0.5.21
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 +56 -0
- package/package.json +1 -1
- package/src/extension/register.ts +4 -3
- package/src/observability/event-bus.ts +1 -1
- package/src/observability/metrics-primitives.ts +7 -1
- package/src/runtime/dynamic-script-runner.ts +0 -1
- package/src/runtime/settings-store.ts +6 -1
- package/src/runtime/task-output-context.ts +11 -1
- package/src/state/event-log.ts +7 -1
- package/src/tools/safe-bash.ts +7 -6
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,61 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.5.21] — Ultimate Final Sweep: HIGH Security + Correctness Fixes (2026-06-03)
|
|
4
|
+
|
|
5
|
+
### Highlights
|
|
6
|
+
- **safe-bash line-continuation bypass fixed** — `$\n(evil)` now blocked
|
|
7
|
+
- **scheduledJobs dead code fixed** — settings sanitizer now passes through scheduled jobs
|
|
8
|
+
- **Memory-bounded file reads** — `readIfSmall` uses `fs.readSync` with buffer instead of full file read
|
|
9
|
+
- **Event log corruption detection** — `scanSequence` logs warnings for corrupt JSON lines
|
|
10
|
+
|
|
11
|
+
### Security
|
|
12
|
+
- `safe-bash.ts`: All structural checks now use `normalized` string (stripped line continuations)
|
|
13
|
+
- `\$\s*\(` regex catches `$<newline>(evil)` → `$(evil)` bypass that bash interprets as command substitution
|
|
14
|
+
- Added 2 regression tests for line-continuation bypass
|
|
15
|
+
|
|
16
|
+
### Fixes
|
|
17
|
+
- `settings-store.ts`: `sanitizeSettings()` now copies `scheduledJobs` as opaque array
|
|
18
|
+
- `task-output-context.ts`: `readIfSmall` uses `Buffer.alloc` + `fs.readSync` instead of `readFileSync` + `slice`
|
|
19
|
+
- `event-log.ts`: `scanSequence` counts and logs corrupt JSON lines via `logInternalError`
|
|
20
|
+
|
|
21
|
+
### Stats
|
|
22
|
+
- Test suite: 2703 pass + 1 skip, 0 fail
|
|
23
|
+
- TypeScript: 0 errors
|
|
24
|
+
- Total issues fixed across 37 rounds: ~155+
|
|
25
|
+
|
|
26
|
+
## [0.5.20] — Verification Sweep: 7 Fixes (2026-06-03)
|
|
27
|
+
|
|
28
|
+
### Highlights
|
|
29
|
+
- **Correctness bug fixed**: `enforceLabelCap` could silently evict actively-used metric entries
|
|
30
|
+
- `Date` removed from forbidden globals (was blocking legitimate workflow scripts)
|
|
31
|
+
- `scheduledJobs` properly typed in `CrewSettings` interface
|
|
32
|
+
- 3 new tests for metric MRU eviction behavior
|
|
33
|
+
|
|
34
|
+
### Fixes
|
|
35
|
+
|
|
36
|
+
#### Correctness
|
|
37
|
+
- `Counter.inc()` and `Gauge.set()` now delete-then-set to move keys to MRU position
|
|
38
|
+
- Previously, `enforceLabelCap` could evict an entry that was just updated
|
|
39
|
+
|
|
40
|
+
#### Consistency
|
|
41
|
+
- Removed `Date` from `FORBIDDEN_GLOBALS` in `DynamicScriptRunner`
|
|
42
|
+
- `Date` is not dangerous — was causing false positives for `myDate`, `updateDate`, etc.
|
|
43
|
+
- `DynamicScriptRunner` and `WorkflowSandbox` now consistent
|
|
44
|
+
|
|
45
|
+
#### Type Safety
|
|
46
|
+
- Added `scheduledJobs?: unknown[]` to `CrewSettings` interface
|
|
47
|
+
- Removed `as any` cast in `register.ts` (now uses `as ScheduledJob`)
|
|
48
|
+
|
|
49
|
+
#### Code Quality
|
|
50
|
+
- Removed dead `reason` variable in `settings-store.ts`
|
|
51
|
+
- Added trailing newline to `event-bus.ts` (POSIX compliance)
|
|
52
|
+
- Added 3 tests for Counter/Gauge MRU eviction behavior
|
|
53
|
+
|
|
54
|
+
### Stats
|
|
55
|
+
- Test suite: 2658+ pass + 1 skip, 0 fail
|
|
56
|
+
- TypeScript: 0 errors
|
|
57
|
+
- Security: All 7 SEC-* issues confirmed still fixed
|
|
58
|
+
|
|
3
59
|
## [0.5.19] — Final Sweep: 8 MEDIUM/LOW Fixes + 2 Test Fixes (2026-06-03)
|
|
4
60
|
|
|
5
61
|
### Highlights
|
package/package.json
CHANGED
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
} from "./async-notifier.ts";
|
|
19
19
|
import { registerAutonomousPolicy } from "./autonomous-policy.ts";
|
|
20
20
|
import { registerCleanupHandler } from "./crew-cleanup.ts";
|
|
21
|
+
import type { ScheduledJob } from "../runtime/scheduler.ts";
|
|
21
22
|
import { clearHooks } from "../hooks/registry.ts";
|
|
22
23
|
import { notifyActiveRuns } from "./session-summary.ts";
|
|
23
24
|
|
|
@@ -1339,10 +1340,10 @@ export function registerPiTeams(pi: ExtensionAPI): void {
|
|
|
1339
1340
|
// Uses a global symbol so the module doesn't need a direct circular import.
|
|
1340
1341
|
(globalThis as Record<symbol | string, unknown>)[Symbol.for("pi-crew:scheduler")] = crewScheduler;
|
|
1341
1342
|
// Load scheduled jobs from settings if present
|
|
1342
|
-
if (Array.isArray(
|
|
1343
|
-
for (const job of
|
|
1343
|
+
if (Array.isArray(crewSettings.scheduledJobs)) {
|
|
1344
|
+
for (const job of crewSettings.scheduledJobs) {
|
|
1344
1345
|
try {
|
|
1345
|
-
crewScheduler.add(job);
|
|
1346
|
+
crewScheduler.add(job as ScheduledJob);
|
|
1346
1347
|
} catch {
|
|
1347
1348
|
/* skip invalid */
|
|
1348
1349
|
}
|
|
@@ -79,4 +79,4 @@ class EventBus {
|
|
|
79
79
|
* This bus is retained for future observability/SIEM integration.
|
|
80
80
|
* See also: progress-tracker.ts which emits agent:progress events.
|
|
81
81
|
*/
|
|
82
|
-
export const crewEventBus = EventBus.getInstance();
|
|
82
|
+
export const crewEventBus = EventBus.getInstance();
|
|
@@ -79,6 +79,9 @@ export class Counter extends Metric {
|
|
|
79
79
|
if (!Number.isFinite(delta) || delta < 0) return;
|
|
80
80
|
const key = labelKey(labels);
|
|
81
81
|
const current = this.values.get(key) ?? { labels: normalizeLabels(labels), value: 0 };
|
|
82
|
+
// Delete before set to move key to end of insertion order (MRU).
|
|
83
|
+
// Without this, enforceLabelCap could evict an actively-used entry.
|
|
84
|
+
this.values.delete(key);
|
|
82
85
|
this.values.set(key, { labels: current.labels, value: current.value + delta });
|
|
83
86
|
enforceLabelCap(this.values, this.name);
|
|
84
87
|
}
|
|
@@ -97,7 +100,10 @@ export class Gauge extends Metric {
|
|
|
97
100
|
|
|
98
101
|
set(labels: MetricLabels = {}, value: number): void {
|
|
99
102
|
if (!Number.isFinite(value)) return;
|
|
100
|
-
|
|
103
|
+
const key = labelKey(labels);
|
|
104
|
+
// Delete before set to move key to end of insertion order (MRU).
|
|
105
|
+
this.values.delete(key);
|
|
106
|
+
this.values.set(key, { labels: normalizeLabels(labels), value });
|
|
101
107
|
enforceLabelCap(this.values, this.name);
|
|
102
108
|
}
|
|
103
109
|
|
|
@@ -11,6 +11,8 @@ export interface CrewSettings {
|
|
|
11
11
|
defaultJoinMode?: JoinMode;
|
|
12
12
|
schedulingEnabled?: boolean;
|
|
13
13
|
notifierIntervalMs?: number;
|
|
14
|
+
/** Scheduled jobs loaded from settings — opaque, passed to crewScheduler */
|
|
15
|
+
scheduledJobs?: unknown[];
|
|
14
16
|
}
|
|
15
17
|
|
|
16
18
|
const MAX_CONCURRENT_CEILING = 1024;
|
|
@@ -55,6 +57,10 @@ function sanitizeSettings(raw: unknown): CrewSettings {
|
|
|
55
57
|
if (typeof r.notifierIntervalMs === "number" && r.notifierIntervalMs >= 1000) {
|
|
56
58
|
out.notifierIntervalMs = r.notifierIntervalMs;
|
|
57
59
|
}
|
|
60
|
+
// Pass through scheduledJobs as opaque array (validated by crewScheduler.add)
|
|
61
|
+
if (Array.isArray(r.scheduledJobs)) {
|
|
62
|
+
out.scheduledJobs = r.scheduledJobs;
|
|
63
|
+
}
|
|
58
64
|
return out;
|
|
59
65
|
}
|
|
60
66
|
|
|
@@ -71,7 +77,6 @@ function readSettingsFile(filePath: string): CrewSettings {
|
|
|
71
77
|
try {
|
|
72
78
|
return sanitizeSettings(JSON.parse(fs.readFileSync(filePath, "utf-8")));
|
|
73
79
|
} catch (err) {
|
|
74
|
-
const reason = err instanceof Error ? err.message : String(err);
|
|
75
80
|
logInternalError("settings-store.read", err, `Ignoring malformed settings at ${filePath}`);
|
|
76
81
|
return {};
|
|
77
82
|
}
|
|
@@ -34,7 +34,17 @@ function readIfSmall(filePath: string, maxBytes = 24_000, baseDir?: string): str
|
|
|
34
34
|
try {
|
|
35
35
|
const safePath = baseDir ? resolveRealContainedPath(baseDir, filePath) : filePath;
|
|
36
36
|
const stat = fs.statSync(safePath);
|
|
37
|
-
if (stat.size > maxBytes)
|
|
37
|
+
if (stat.size > maxBytes) {
|
|
38
|
+
// Use bounded read to avoid loading entire file into memory
|
|
39
|
+
const buf = Buffer.alloc(maxBytes);
|
|
40
|
+
const fd = fs.openSync(safePath, "r");
|
|
41
|
+
try {
|
|
42
|
+
fs.readSync(fd, buf, 0, maxBytes, 0);
|
|
43
|
+
} finally {
|
|
44
|
+
fs.closeSync(fd);
|
|
45
|
+
}
|
|
46
|
+
return `${buf.toString("utf-8")}\n\n...(truncated ${stat.size - maxBytes} bytes)`;
|
|
47
|
+
}
|
|
38
48
|
return fs.readFileSync(safePath, "utf-8");
|
|
39
49
|
} catch {
|
|
40
50
|
return undefined;
|
package/src/state/event-log.ts
CHANGED
|
@@ -149,12 +149,18 @@ function parseSequence(raw: string): number | undefined {
|
|
|
149
149
|
export function scanSequence(eventsPath: string): number {
|
|
150
150
|
if (!fs.existsSync(eventsPath)) return 0;
|
|
151
151
|
let max = 0;
|
|
152
|
+
let skipped = 0;
|
|
152
153
|
for (const line of fs.readFileSync(eventsPath, "utf-8").split("\n")) {
|
|
153
154
|
if (!line.trim()) continue;
|
|
154
155
|
try {
|
|
155
156
|
const event = JSON.parse(line) as TeamEvent;
|
|
156
157
|
max = Math.max(max, event.metadata?.seq ?? 0);
|
|
157
|
-
} catch {
|
|
158
|
+
} catch {
|
|
159
|
+
skipped++;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
if (skipped > 0) {
|
|
163
|
+
logInternalError("event-log.scanSequence.corrupt_lines", undefined, `${eventsPath}: skipped ${skipped} corrupt line(s)`);
|
|
158
164
|
}
|
|
159
165
|
return max;
|
|
160
166
|
}
|
package/src/tools/safe-bash.ts
CHANGED
|
@@ -201,22 +201,23 @@ export function isDangerous(command: string, options: SafeBashOptions = {}): str
|
|
|
201
201
|
}
|
|
202
202
|
|
|
203
203
|
// Additional shell injection checks using regex for non-critical patterns
|
|
204
|
-
// Block command substitution $(...)
|
|
205
|
-
|
|
204
|
+
// Block command substitution $(...) — use normalized to prevent $\n(evil) bypass
|
|
205
|
+
// Also match $<space>(...) which is the normalized form of $\n(evil)
|
|
206
|
+
if (/\$\s*\([^)]*\)/.test(normalized)) {
|
|
206
207
|
return "Command blocked by safe_bash: command substitution $(...) is not allowed";
|
|
207
208
|
}
|
|
208
209
|
// Block backtick substitution
|
|
209
210
|
const backtickRe = /`[^`]*`/;
|
|
210
|
-
if (backtickRe.test(
|
|
211
|
+
if (backtickRe.test(normalized)) {
|
|
211
212
|
return "Command blocked by safe_bash: backtick substitution is not allowed";
|
|
212
213
|
}
|
|
213
214
|
// Block here-docs <<
|
|
214
|
-
if (/<<\s*['"]?[\w-]+['"]?/.test(
|
|
215
|
+
if (/<<\s*['"]?[\w-]+['"]?/.test(normalized) || /\$<<\s*['"]?[\w-]+['"]?/.test(normalized)) {
|
|
215
216
|
return "Command blocked by safe_bash: here-doc is not allowed";
|
|
216
217
|
}
|
|
217
|
-
// Block ${...} variable expansion containing shell metacharacters
|
|
218
|
+
// Block ${...} variable expansion containing shell metacharacters
|
|
218
219
|
const varExpRe = /\$\{([^}]*)\}/;
|
|
219
|
-
const varMatch =
|
|
220
|
+
const varMatch = normalized.match(varExpRe);
|
|
220
221
|
if (varMatch && /[|&;<>]/.test(varMatch[1])) {
|
|
221
222
|
return "Command blocked by safe_bash: variable expansion with shell metacharacters is not allowed";
|
|
222
223
|
}
|