pi-crew 0.5.18 → 0.5.20
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 +63 -0
- package/package.json +1 -1
- package/src/agents/discover-agents.ts +15 -8
- package/src/extension/register.ts +6 -3
- package/src/observability/event-bus.ts +9 -1
- package/src/observability/metrics-primitives.ts +20 -1
- package/src/runtime/dynamic-script-runner.ts +0 -1
- package/src/runtime/settings-store.ts +4 -2
- package/src/runtime/sidechain-output.ts +2 -0
- package/src/runtime/streaming-output.ts +3 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,68 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.5.20] — Verification Sweep: 7 Fixes (2026-06-03)
|
|
4
|
+
|
|
5
|
+
### Highlights
|
|
6
|
+
- **Correctness bug fixed**: `enforceLabelCap` could silently evict actively-used metric entries
|
|
7
|
+
- `Date` removed from forbidden globals (was blocking legitimate workflow scripts)
|
|
8
|
+
- `scheduledJobs` properly typed in `CrewSettings` interface
|
|
9
|
+
- 3 new tests for metric MRU eviction behavior
|
|
10
|
+
|
|
11
|
+
### Fixes
|
|
12
|
+
|
|
13
|
+
#### Correctness
|
|
14
|
+
- `Counter.inc()` and `Gauge.set()` now delete-then-set to move keys to MRU position
|
|
15
|
+
- Previously, `enforceLabelCap` could evict an entry that was just updated
|
|
16
|
+
|
|
17
|
+
#### Consistency
|
|
18
|
+
- Removed `Date` from `FORBIDDEN_GLOBALS` in `DynamicScriptRunner`
|
|
19
|
+
- `Date` is not dangerous — was causing false positives for `myDate`, `updateDate`, etc.
|
|
20
|
+
- `DynamicScriptRunner` and `WorkflowSandbox` now consistent
|
|
21
|
+
|
|
22
|
+
#### Type Safety
|
|
23
|
+
- Added `scheduledJobs?: unknown[]` to `CrewSettings` interface
|
|
24
|
+
- Removed `as any` cast in `register.ts` (now uses `as ScheduledJob`)
|
|
25
|
+
|
|
26
|
+
#### Code Quality
|
|
27
|
+
- Removed dead `reason` variable in `settings-store.ts`
|
|
28
|
+
- Added trailing newline to `event-bus.ts` (POSIX compliance)
|
|
29
|
+
- Added 3 tests for Counter/Gauge MRU eviction behavior
|
|
30
|
+
|
|
31
|
+
### Stats
|
|
32
|
+
- Test suite: 2658+ pass + 1 skip, 0 fail
|
|
33
|
+
- TypeScript: 0 errors
|
|
34
|
+
- Security: All 7 SEC-* issues confirmed still fixed
|
|
35
|
+
|
|
36
|
+
## [0.5.19] — Final Sweep: 8 MEDIUM/LOW Fixes + 2 Test Fixes (2026-06-03)
|
|
37
|
+
|
|
38
|
+
### Highlights
|
|
39
|
+
- **All remaining issues fixed** — 4-agent review sweep found 0 CRITICAL/HIGH
|
|
40
|
+
- 2 pre-existing test failures fixed (env isolation)
|
|
41
|
+
- Memory bounds added to security log and metrics primitives
|
|
42
|
+
- Defensive path validation in streaming/sidechain output
|
|
43
|
+
- Production cleanup now clears hooks
|
|
44
|
+
|
|
45
|
+
### Fixes
|
|
46
|
+
|
|
47
|
+
#### MEDIUM: Memory bounds
|
|
48
|
+
- `securityEventLog` in `discover-agents.ts` capped at 1,000 entries (was unbounded)
|
|
49
|
+
- `Counter`/`Gauge`/`Histogram` Maps in `metrics-primitives.ts` capped at 10,000 label combinations
|
|
50
|
+
|
|
51
|
+
#### LOW: Code quality
|
|
52
|
+
- `console.warn` → `logInternalError` in `settings-store.ts` and `discover-agents.ts`
|
|
53
|
+
- `crewEventBus` dead code documented (retained for future use)
|
|
54
|
+
- `clearHooks()` called in production cleanup path (`register.ts`)
|
|
55
|
+
- `assertSafePathId` added to `streaming-output.ts` and `sidechain-output.ts`
|
|
56
|
+
|
|
57
|
+
#### Test fixes
|
|
58
|
+
- `adaptive-implementation.test.ts`: replaced `restoreEnv` with `delete` to prevent leaked `PI_CREW_ROLE`
|
|
59
|
+
- `subagent-tools-integration.test.ts`: added env isolation to first test case
|
|
60
|
+
|
|
61
|
+
### Stats
|
|
62
|
+
- Test suite: 2688 pass + 1 skip, 0 fail
|
|
63
|
+
- TypeScript: 0 errors
|
|
64
|
+
- Files changed: 9
|
|
65
|
+
|
|
3
66
|
## [0.5.18] — Final Review Fixes (2026-06-03)
|
|
4
67
|
|
|
5
68
|
### Highlights
|
package/package.json
CHANGED
|
@@ -103,7 +103,9 @@ interface SecurityEvent {
|
|
|
103
103
|
|
|
104
104
|
/**
|
|
105
105
|
* Security event log. In production, this should be sent to a security SIEM.
|
|
106
|
+
* Bounded at MAX_SECURITY_LOG_ENTRIES to prevent unbounded memory growth.
|
|
106
107
|
*/
|
|
108
|
+
const MAX_SECURITY_LOG_ENTRIES = 1000;
|
|
107
109
|
const securityEventLog: SecurityEvent[] = [];
|
|
108
110
|
|
|
109
111
|
/**
|
|
@@ -113,11 +115,16 @@ const securityEventLog: SecurityEvent[] = [];
|
|
|
113
115
|
*/
|
|
114
116
|
function logSecurityEvent(event: SecurityEvent): void {
|
|
115
117
|
securityEventLog.push(event);
|
|
118
|
+
// Evict oldest entries when cap exceeded
|
|
119
|
+
while (securityEventLog.length > MAX_SECURITY_LOG_ENTRIES) {
|
|
120
|
+
securityEventLog.shift();
|
|
121
|
+
}
|
|
116
122
|
|
|
117
|
-
//
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
123
|
+
// Log security events via structured logger
|
|
124
|
+
logInternalError(
|
|
125
|
+
`security.${event.type}`,
|
|
126
|
+
undefined,
|
|
127
|
+
`agent="${event.name}" reason="${event.reason}"`,
|
|
121
128
|
);
|
|
122
129
|
}
|
|
123
130
|
|
|
@@ -195,10 +202,10 @@ function checkProjectAgentShadowsBuiltin(name: string): void {
|
|
|
195
202
|
reason: "project_shadows_protected_builtin",
|
|
196
203
|
timestamp: Date.now(),
|
|
197
204
|
});
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
`
|
|
205
|
+
logInternalError(
|
|
206
|
+
`security.agent_shadow_warning`,
|
|
207
|
+
undefined,
|
|
208
|
+
`Project agent "${name}" shadows a protected builtin. Builtin agents take priority.`,
|
|
202
209
|
);
|
|
203
210
|
return;
|
|
204
211
|
}
|
|
@@ -18,6 +18,8 @@ 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";
|
|
22
|
+
import { clearHooks } from "../hooks/registry.ts";
|
|
21
23
|
import { notifyActiveRuns } from "./session-summary.ts";
|
|
22
24
|
|
|
23
25
|
let _cachedLiveRunSidebar: typeof LiveRunSidebarType | undefined;
|
|
@@ -1109,6 +1111,7 @@ export function registerPiTeams(pi: ExtensionAPI): void {
|
|
|
1109
1111
|
otlpExporter = undefined;
|
|
1110
1112
|
metricRegistry = undefined;
|
|
1111
1113
|
deliveryCoordinator?.dispose();
|
|
1114
|
+
clearHooks();
|
|
1112
1115
|
overflowTracker?.dispose();
|
|
1113
1116
|
deliveryCoordinator = undefined;
|
|
1114
1117
|
overflowTracker = undefined;
|
|
@@ -1337,10 +1340,10 @@ export function registerPiTeams(pi: ExtensionAPI): void {
|
|
|
1337
1340
|
// Uses a global symbol so the module doesn't need a direct circular import.
|
|
1338
1341
|
(globalThis as Record<symbol | string, unknown>)[Symbol.for("pi-crew:scheduler")] = crewScheduler;
|
|
1339
1342
|
// Load scheduled jobs from settings if present
|
|
1340
|
-
if (Array.isArray(
|
|
1341
|
-
for (const job of
|
|
1343
|
+
if (Array.isArray(crewSettings.scheduledJobs)) {
|
|
1344
|
+
for (const job of crewSettings.scheduledJobs) {
|
|
1342
1345
|
try {
|
|
1343
|
-
crewScheduler.add(job);
|
|
1346
|
+
crewScheduler.add(job as ScheduledJob);
|
|
1344
1347
|
} catch {
|
|
1345
1348
|
/* skip invalid */
|
|
1346
1349
|
}
|
|
@@ -71,4 +71,12 @@ class EventBus {
|
|
|
71
71
|
}
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
-
|
|
74
|
+
/**
|
|
75
|
+
* Global event bus for crew lifecycle events.
|
|
76
|
+
*
|
|
77
|
+
* NOTE: Currently only emits — no production subscribers yet.
|
|
78
|
+
* The `runEventBus` (from `ui/run-event-bus.ts`) is the active event system.
|
|
79
|
+
* This bus is retained for future observability/SIEM integration.
|
|
80
|
+
* See also: progress-tracker.ts which emits agent:progress events.
|
|
81
|
+
*/
|
|
82
|
+
export const crewEventBus = EventBus.getInstance();
|
|
@@ -36,6 +36,16 @@ interface StoredHistogram {
|
|
|
36
36
|
|
|
37
37
|
export const DEFAULT_HISTOGRAM_BUCKETS = [1, 2, 5, 10, 25, 50, 100, 250, 500, 1000, 2500, 5000, 10000] as const;
|
|
38
38
|
|
|
39
|
+
/** Maximum number of unique label combinations per metric. */
|
|
40
|
+
const MAX_LABEL_COMBINATIONS = 10_000;
|
|
41
|
+
|
|
42
|
+
function enforceLabelCap(map: Map<string, unknown>, metricName: string): void {
|
|
43
|
+
while (map.size > MAX_LABEL_COMBINATIONS) {
|
|
44
|
+
const firstKey = map.keys().next().value;
|
|
45
|
+
if (firstKey !== undefined) map.delete(firstKey);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
39
49
|
function normalizeLabels(labels: MetricLabels = {}): MetricLabels {
|
|
40
50
|
const normalized: MetricLabels = {};
|
|
41
51
|
for (const [key, value] of Object.entries(labels).sort(([left], [right]) => left.localeCompare(right))) normalized[key] = value;
|
|
@@ -69,7 +79,11 @@ export class Counter extends Metric {
|
|
|
69
79
|
if (!Number.isFinite(delta) || delta < 0) return;
|
|
70
80
|
const key = labelKey(labels);
|
|
71
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);
|
|
72
85
|
this.values.set(key, { labels: current.labels, value: current.value + delta });
|
|
86
|
+
enforceLabelCap(this.values, this.name);
|
|
73
87
|
}
|
|
74
88
|
|
|
75
89
|
value(labels: MetricLabels = {}): number {
|
|
@@ -86,7 +100,11 @@ export class Gauge extends Metric {
|
|
|
86
100
|
|
|
87
101
|
set(labels: MetricLabels = {}, value: number): void {
|
|
88
102
|
if (!Number.isFinite(value)) return;
|
|
89
|
-
|
|
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 });
|
|
107
|
+
enforceLabelCap(this.values, this.name);
|
|
90
108
|
}
|
|
91
109
|
|
|
92
110
|
add(labels: MetricLabels = {}, delta: number): void {
|
|
@@ -123,6 +141,7 @@ export class Histogram extends Metric {
|
|
|
123
141
|
current.sum += value;
|
|
124
142
|
current.count += 1;
|
|
125
143
|
if (!existing) this.observations.set(key, current);
|
|
144
|
+
enforceLabelCap(this.observations, this.name);
|
|
126
145
|
}
|
|
127
146
|
|
|
128
147
|
quantile(labels: MetricLabels = {}, q: number): number {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
|
+
import { logInternalError } from "../utils/internal-error.ts";
|
|
4
5
|
import type { JoinMode } from "./group-join.ts";
|
|
5
6
|
|
|
6
7
|
export interface CrewSettings {
|
|
@@ -10,6 +11,8 @@ export interface CrewSettings {
|
|
|
10
11
|
defaultJoinMode?: JoinMode;
|
|
11
12
|
schedulingEnabled?: boolean;
|
|
12
13
|
notifierIntervalMs?: number;
|
|
14
|
+
/** Scheduled jobs loaded from settings — opaque, passed to crewScheduler */
|
|
15
|
+
scheduledJobs?: unknown[];
|
|
13
16
|
}
|
|
14
17
|
|
|
15
18
|
const MAX_CONCURRENT_CEILING = 1024;
|
|
@@ -70,8 +73,7 @@ function readSettingsFile(filePath: string): CrewSettings {
|
|
|
70
73
|
try {
|
|
71
74
|
return sanitizeSettings(JSON.parse(fs.readFileSync(filePath, "utf-8")));
|
|
72
75
|
} catch (err) {
|
|
73
|
-
|
|
74
|
-
console.warn(`[pi-crew] Ignoring malformed settings at ${filePath}: ${reason}`);
|
|
76
|
+
logInternalError("settings-store.read", err, `Ignoring malformed settings at ${filePath}`);
|
|
75
77
|
return {};
|
|
76
78
|
}
|
|
77
79
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
|
+
import { isSafePathId } from "../utils/safe-paths.ts";
|
|
3
4
|
import { redactSecrets } from "../utils/redaction.ts";
|
|
4
5
|
|
|
5
6
|
export interface SidechainEntry {
|
|
@@ -17,6 +18,7 @@ export function writeSidechainEntry(filePath: string, entry: Omit<SidechainEntry
|
|
|
17
18
|
}
|
|
18
19
|
|
|
19
20
|
export function sidechainOutputPath(stateRoot: string, taskId: string): string {
|
|
21
|
+
if (!isSafePathId(taskId)) throw new Error(`Invalid taskId: ${taskId}`);
|
|
20
22
|
return path.join(stateRoot, "agents", taskId, "sidechain.output.jsonl");
|
|
21
23
|
}
|
|
22
24
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
|
+
import { isSafePathId } from "../utils/safe-paths.ts";
|
|
3
4
|
import type { TeamRunManifest } from "../state/types.ts";
|
|
4
5
|
|
|
5
6
|
export interface StreamingOutputHandle {
|
|
@@ -9,6 +10,7 @@ export interface StreamingOutputHandle {
|
|
|
9
10
|
}
|
|
10
11
|
|
|
11
12
|
export function createStreamingOutput(manifest: TeamRunManifest, taskId: string): StreamingOutputHandle {
|
|
13
|
+
if (!isSafePathId(taskId)) throw new Error(`Invalid taskId: ${taskId}`);
|
|
12
14
|
const outputDir = path.join(manifest.artifactsRoot, "streaming");
|
|
13
15
|
fs.mkdirSync(outputDir, { recursive: true });
|
|
14
16
|
const outputPath = path.join(outputDir, `${taskId}.md`);
|
|
@@ -37,6 +39,7 @@ export function createStreamingOutput(manifest: TeamRunManifest, taskId: string)
|
|
|
37
39
|
}
|
|
38
40
|
|
|
39
41
|
export function readStreamingOutput(manifest: TeamRunManifest, taskId: string): string {
|
|
42
|
+
if (!isSafePathId(taskId)) return "";
|
|
40
43
|
const outputPath = path.join(manifest.artifactsRoot, "streaming", `${taskId}.md`);
|
|
41
44
|
if (!fs.existsSync(outputPath)) return "";
|
|
42
45
|
try {
|