pi-crew 0.5.5 → 0.5.7

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.
Files changed (74) hide show
  1. package/CHANGELOG.md +153 -0
  2. package/README.md +17 -1
  3. package/docs/architecture.md +2 -0
  4. package/docs/migration-v0.4-v0.5.md +19 -2
  5. package/docs/pi-crew-v0.5.5-audit-fix-plan.md +133 -0
  6. package/package.json +7 -5
  7. package/src/benchmark/benchmark-runner.ts +45 -0
  8. package/src/benchmark/feedback-loop.ts +5 -0
  9. package/src/config/config.ts +38 -4
  10. package/src/config/defaults.ts +5 -0
  11. package/src/config/suggestions.ts +8 -0
  12. package/src/extension/async-notifier.ts +10 -1
  13. package/src/extension/cross-extension-rpc.ts +1 -1
  14. package/src/extension/notification-router.ts +18 -0
  15. package/src/extension/register.ts +13 -17
  16. package/src/extension/registration/subagent-tools.ts +1 -1
  17. package/src/extension/team-tool/anchor.ts +201 -0
  18. package/src/extension/team-tool/api.ts +2 -1
  19. package/src/extension/team-tool/auto-summarize.ts +154 -0
  20. package/src/extension/team-tool/run.ts +37 -2
  21. package/src/extension/team-tool.ts +44 -2
  22. package/src/hooks/registry.ts +1 -3
  23. package/src/observability/event-bus.ts +13 -4
  24. package/src/observability/event-to-metric.ts +0 -2
  25. package/src/runtime/anchor-manager.ts +473 -0
  26. package/src/runtime/async-runner.ts +8 -4
  27. package/src/runtime/auto-summarize.ts +350 -0
  28. package/src/runtime/background-runner.ts +2 -1
  29. package/src/runtime/budget-tracker.ts +354 -0
  30. package/src/runtime/chain-runner.ts +507 -0
  31. package/src/runtime/child-pi.ts +24 -6
  32. package/src/runtime/crash-recovery.ts +5 -4
  33. package/src/runtime/crew-agent-records.ts +32 -1
  34. package/src/runtime/custom-tools/irc-tool.ts +13 -0
  35. package/src/runtime/custom-tools/submit-result-tool.ts +3 -2
  36. package/src/runtime/delivery-coordinator.ts +10 -3
  37. package/src/runtime/dynamic-script-runner.ts +482 -0
  38. package/src/runtime/handoff-manager.ts +589 -0
  39. package/src/runtime/hidden-handoff.ts +424 -0
  40. package/src/runtime/live-agent-manager.ts +20 -4
  41. package/src/runtime/live-session-runtime.ts +39 -4
  42. package/src/runtime/manifest-cache.ts +2 -1
  43. package/src/runtime/model-resolver.ts +16 -4
  44. package/src/runtime/phase-tracker.ts +373 -0
  45. package/src/runtime/pipeline-runner.ts +514 -0
  46. package/src/runtime/retry-runner.ts +354 -0
  47. package/src/runtime/sandbox.ts +252 -0
  48. package/src/runtime/scheduler.ts +7 -2
  49. package/src/runtime/subagent-manager.ts +1 -1
  50. package/src/runtime/task-graph.ts +11 -1
  51. package/src/runtime/task-runner.ts +15 -1
  52. package/src/runtime/team-runner.ts +4 -3
  53. package/src/schema/team-tool-schema.ts +31 -0
  54. package/src/skills/discover-skills.ts +5 -0
  55. package/src/state/active-run-registry.ts +19 -3
  56. package/src/state/contracts.ts +9 -0
  57. package/src/state/crew-init.ts +3 -3
  58. package/src/state/decision-ledger.ts +26 -32
  59. package/src/state/event-log-rotation.ts +2 -2
  60. package/src/state/event-log.ts +17 -4
  61. package/src/state/mailbox.ts +35 -1
  62. package/src/state/run-cache.ts +18 -8
  63. package/src/tools/safe-bash-extension.ts +1 -0
  64. package/src/tools/safe-bash.ts +153 -20
  65. package/src/ui/overlays/mailbox-detail-overlay.ts +13 -2
  66. package/src/ui/powerbar-publisher.ts +1 -0
  67. package/src/ui/transcript-cache.ts +13 -0
  68. package/src/utils/bm25-search.ts +16 -8
  69. package/src/utils/env-filter.ts +8 -5
  70. package/src/utils/redaction.ts +169 -15
  71. package/src/utils/sse-parser.ts +10 -1
  72. package/src/worktree/cleanup.ts +6 -1
  73. package/workflows/chain.workflow.md +252 -0
  74. package/workflows/pipeline.workflow.md +27 -0
@@ -28,11 +28,10 @@ export class DeliveryCoordinator {
28
28
  private flushing = false;
29
29
  private readonly deps: DeliveryCoordinatorDeps;
30
30
  private ttlTimer: ReturnType<typeof setInterval> | undefined;
31
+ private timerStarted = false;
31
32
 
32
33
  constructor(deps: DeliveryCoordinatorDeps) {
33
34
  this.deps = deps;
34
- this.ttlTimer = setInterval(() => this.evictExpired(), 60_000);
35
- this.ttlTimer.unref();
36
35
  }
37
36
 
38
37
  activate(sessionId: string): void {
@@ -102,9 +101,11 @@ export class DeliveryCoordinator {
102
101
 
103
102
  flushQueuedResults(): void {
104
103
  if (!this.active || this.pending.length === 0) return;
105
- // H7: Set flushing BEFORE splice to prevent re-entrancy
104
+ // HIGH-16/ MEDIUM-16: Set flushing BEFORE splice to prevent re-entrancy
106
105
  if (this.flushing) return;
107
106
  this.flushing = true;
107
+ // Note: this.flushing is now set, so concurrent calls will exit early due to the check above
108
+ // This serves as a simple lock to prevent race conditions
108
109
  const batch = this.pending.splice(0);
109
110
  try {
110
111
  const retryLater: PendingDelivery[] = [];
@@ -162,6 +163,12 @@ export class DeliveryCoordinator {
162
163
  }
163
164
 
164
165
  private enqueue(delivery: PendingDelivery): void {
166
+ // Lazily start the TTL timer on first enqueue (only if never started)
167
+ if (!this.timerStarted) {
168
+ this.timerStarted = true;
169
+ this.ttlTimer = setInterval(() => this.evictExpired(), 60_000);
170
+ this.ttlTimer.unref();
171
+ }
165
172
  this.pending.push({ ...delivery, generation: this.generation });
166
173
  }
167
174
 
@@ -0,0 +1,482 @@
1
+ import * as vm from "node:vm";
2
+ import * as acorn from "acorn";
3
+ import { WorkflowSandbox, type SandboxOptions } from "./sandbox.ts";
4
+
5
+ /**
6
+ * Forbidden globals that could compromise sandbox security or cause side effects.
7
+ * These are checked during AST validation before execution.
8
+ */
9
+ export const FORBIDDEN_GLOBALS = [
10
+ "Date",
11
+ "Math.random",
12
+ "require",
13
+ "import",
14
+ "module",
15
+ "exports",
16
+ "__dirname",
17
+ "__filename",
18
+ "process.exit",
19
+ "process.kill",
20
+ "process.hrtime",
21
+ "process.memoryUsage",
22
+ "process.cpuUsage",
23
+ "process.binding",
24
+ "process.dlopen",
25
+ "process._tickCallback",
26
+ "eval",
27
+ "Function",
28
+ "AsyncFunction",
29
+ "GeneratorFunction",
30
+ "Proxy",
31
+ "Reflect",
32
+ "WebAssembly",
33
+ "global",
34
+ "globalThis",
35
+ "window",
36
+ "document",
37
+ "XMLHttpRequest",
38
+ "fetch",
39
+ "WebSocket",
40
+ "Worker",
41
+ "SharedArrayBuffer",
42
+ "Atomics",
43
+ ] as const;
44
+
45
+ // Freeze the array at runtime to ensure it's truly immutable
46
+ Object.freeze(FORBIDDEN_GLOBALS);
47
+
48
+ export type ForbiddenGlobal = (typeof FORBIDDEN_GLOBALS)[number];
49
+
50
+ export interface ScriptValidationResult {
51
+ valid: boolean;
52
+ errors: ScriptValidationError[];
53
+ warnings: ScriptValidationWarning[];
54
+ }
55
+
56
+ export interface ScriptValidationError {
57
+ type: "forbidden_global" | "forbidden_syntax" | "parse_error";
58
+ message: string;
59
+ location?: { line: number; column: number };
60
+ }
61
+
62
+ export interface ScriptValidationWarning {
63
+ type: "deprecated_api" | "potentially_unsafe";
64
+ message: string;
65
+ location?: { line: number; column: number };
66
+ }
67
+
68
+ export interface DynamicScriptOptions {
69
+ timeout?: number;
70
+ maxTokens?: number;
71
+ allowAwait?: boolean;
72
+ allowAsync?: boolean;
73
+ strictMode?: boolean;
74
+ /** Enable strict AST whitelist mode (C2) - reject dynamic property access, call expressions */
75
+ strictAstWhitelist?: boolean;
76
+ }
77
+
78
+ export interface ScriptExecutionResult {
79
+ success: boolean;
80
+ value?: unknown;
81
+ error?: string;
82
+ executionTime: number;
83
+ validation: ScriptValidationResult;
84
+ }
85
+
86
+ /**
87
+ * DynamicScriptRunner executes JavaScript in a VM sandbox with AST validation
88
+ * and forbidden pattern detection.
89
+ *
90
+ * Note: AST parsing is simplified without acorn. For full AST validation,
91
+ * add acorn as a dependency.
92
+ */
93
+ export class DynamicScriptRunner {
94
+ private sandbox: WorkflowSandbox;
95
+ private defaultTimeout: number;
96
+ private options: DynamicScriptOptions;
97
+
98
+ constructor(options: DynamicScriptOptions = {}) {
99
+ this.defaultTimeout = options.timeout ?? 30000;
100
+ this.options = options;
101
+ this.sandbox = new WorkflowSandbox({
102
+ timeout: this.defaultTimeout,
103
+ });
104
+ }
105
+
106
+ /**
107
+ * Validate script before execution using full AST parsing (C1).
108
+ * Uses acorn for comprehensive syntax tree analysis.
109
+ */
110
+ validate(code: string): ScriptValidationResult {
111
+ const errors: ScriptValidationError[] = [];
112
+ const warnings: ScriptValidationWarning[] = [];
113
+
114
+ // C1: Full AST parsing with acorn for complete validation
115
+ let ast: acorn.Node | null = null;
116
+ try {
117
+ ast = acorn.parse(code, {
118
+ ecmaVersion: "latest",
119
+ sourceType: "script",
120
+ allowReturnOutsideFunction: true,
121
+ allowAwaitOutsideFunction: this.options.allowAwait ?? false,
122
+ });
123
+ } catch (parseError) {
124
+ errors.push({
125
+ type: "parse_error",
126
+ message: `Parse error: ${parseError instanceof Error ? parseError.message : String(parseError)}`,
127
+ });
128
+ return { valid: false, errors, warnings };
129
+ }
130
+
131
+ // C2: Strict AST whitelist mode - reject dynamic property access and call expressions
132
+ if (this.options.strictAstWhitelist) {
133
+ this.validateAstWhitelist(ast, errors);
134
+ }
135
+
136
+ // C4: Bytecode compilation verification - verify script compiles correctly
137
+ this.verifyCompilation(code, errors);
138
+
139
+ // Check for forbidden globals using regex patterns (fallback)
140
+ this.checkForForbiddenGlobals(code, errors);
141
+
142
+ // Check for forbidden syntax patterns
143
+ this.checkForForbiddenSyntax(code, errors, warnings);
144
+
145
+ // Check for potentially unsafe patterns
146
+ this.checkForPotentiallyUnsafePatterns(code, warnings);
147
+
148
+ return {
149
+ valid: errors.length === 0,
150
+ errors,
151
+ warnings,
152
+ };
153
+ }
154
+
155
+ /**
156
+ * C2: Validate AST against strict whitelist - reject dynamic property access,
157
+ * call expressions, and other potentially dangerous patterns.
158
+ */
159
+ private validateAstWhitelist(
160
+ ast: acorn.Node,
161
+ errors: ScriptValidationError[],
162
+ ): void {
163
+ const walkNode = (node: acorn.Node): void => {
164
+ const n = node as acorn.Node & { type: string };
165
+
166
+ // Reject MemberExpression (e.g., obj.prop, obj[key])
167
+ if (n.type === "MemberExpression") {
168
+ errors.push({
169
+ type: "forbidden_syntax",
170
+ message: "Dynamic property access is not allowed in strict whitelist mode",
171
+ });
172
+ }
173
+
174
+ // Reject CallExpression (e.g., fn(), obj.method())
175
+ if (n.type === "CallExpression") {
176
+ errors.push({
177
+ type: "forbidden_syntax",
178
+ message: "Call expressions are not allowed in strict whitelist mode",
179
+ });
180
+ }
181
+
182
+ // Reject NewExpression (e.g., new Foo())
183
+ if (n.type === "NewExpression") {
184
+ errors.push({
185
+ type: "forbidden_syntax",
186
+ message: "Constructor calls are not allowed in strict whitelist mode",
187
+ });
188
+ }
189
+
190
+ // Reject UpdateExpression (++a, a--, etc.)
191
+ if (n.type === "UpdateExpression") {
192
+ errors.push({
193
+ type: "forbidden_syntax",
194
+ message: "Update expressions are not allowed in strict whitelist mode",
195
+ });
196
+ }
197
+
198
+ // Reject AssignmentExpression (a = b, a += b, etc.)
199
+ if (n.type === "AssignmentExpression") {
200
+ errors.push({
201
+ type: "forbidden_syntax",
202
+ message: "Assignment expressions are not allowed in strict whitelist mode",
203
+ });
204
+ }
205
+
206
+ // Reject ForInStatement and ForOfStatement
207
+ if (n.type === "ForInStatement" || n.type === "ForOfStatement") {
208
+ errors.push({
209
+ type: "forbidden_syntax",
210
+ message: "For-in/for-of loops are not allowed in strict whitelist mode",
211
+ });
212
+ }
213
+
214
+ // Recurse into children
215
+ for (const key of Object.keys(n as object)) {
216
+ const value = (n as unknown as Record<string, unknown>)[key];
217
+ if (value && typeof value === "object") {
218
+ if (Array.isArray(value)) {
219
+ for (const child of value) {
220
+ if (child && typeof child === "object" && "type" in child) {
221
+ walkNode(child as acorn.Node);
222
+ }
223
+ }
224
+ } else if ("type" in value) {
225
+ walkNode(value as acorn.Node);
226
+ }
227
+ }
228
+ }
229
+ };
230
+
231
+ // Cast to access body property (Program node)
232
+ const programNode = ast as unknown as { body: acorn.Node[] };
233
+ for (const node of programNode.body) {
234
+ walkNode(node);
235
+ }
236
+ }
237
+
238
+ /**
239
+ * C4: Verify script compiles to bytecode correctly. If compilation fails,
240
+ * the script cannot be executed safely.
241
+ */
242
+ private verifyCompilation(code: string, errors: ScriptValidationError[]): void {
243
+ try {
244
+ const wrappedCode = `(function(){ ${code} })()`;
245
+ new vm.Script(wrappedCode, {
246
+ filename: "compile-check.js",
247
+ });
248
+ } catch (compileError) {
249
+ errors.push({
250
+ type: "parse_error",
251
+ message: `Compilation verification failed: ${compileError instanceof Error ? compileError.message : String(compileError)}`,
252
+ });
253
+ }
254
+ }
255
+
256
+ private checkForForbiddenGlobals(code: string, errors: ScriptValidationError[]): void {
257
+ // Check each forbidden global pattern
258
+ for (const forbidden of FORBIDDEN_GLOBALS) {
259
+ if (forbidden.includes(".")) {
260
+ // Check for member expressions like Math.random, process.exit
261
+ const [obj, prop] = forbidden.split(".");
262
+ const pattern = new RegExp(`\\b${obj}\\s*\\.\\s*${prop}\\b`);
263
+ if (pattern.test(code)) {
264
+ errors.push({
265
+ type: "forbidden_global",
266
+ message: `Forbidden global access: '${forbidden}'`,
267
+ });
268
+ }
269
+ } else {
270
+ // Check for simple identifiers
271
+ // But avoid false positives like "myDate" matching "Date"
272
+ const pattern = new RegExp(`\\b${forbidden}\\b`);
273
+ if (pattern.test(code)) {
274
+ errors.push({
275
+ type: "forbidden_global",
276
+ message: `Forbidden global: '${forbidden}'`,
277
+ });
278
+ }
279
+ }
280
+ }
281
+ }
282
+
283
+ private checkForForbiddenSyntax(
284
+ code: string,
285
+ errors: ScriptValidationError[],
286
+ warnings: ScriptValidationWarning[],
287
+ ): void {
288
+ // Check for eval()
289
+ if (/\beval\s*\(/.test(code)) {
290
+ errors.push({
291
+ type: "forbidden_syntax",
292
+ message: "eval() is not allowed",
293
+ });
294
+ }
295
+
296
+ // Check for Function constructor
297
+ if (/\bnew\s+Function\s*\(/.test(code) || /\bFunction\s*\(\s*['"`]/.test(code)) {
298
+ errors.push({
299
+ type: "forbidden_syntax",
300
+ message: "Function constructor is not allowed",
301
+ });
302
+ }
303
+
304
+ // Check for AsyncFunction constructor
305
+ if (/\bnew\s+AsyncFunction\s*\(/.test(code) || /\bAsyncFunction\s*\(\s*['"`]/.test(code)) {
306
+ errors.push({
307
+ type: "forbidden_syntax",
308
+ message: "AsyncFunction constructor is not allowed",
309
+ });
310
+ }
311
+
312
+ // Check for GeneratorFunction constructor
313
+ if (/\bnew\s+GeneratorFunction\s*\(/.test(code)) {
314
+ errors.push({
315
+ type: "forbidden_syntax",
316
+ message: "GeneratorFunction constructor is not allowed",
317
+ });
318
+ }
319
+
320
+ // Check for Promise constructor - warn but don't block
321
+ if (/\bnew\s+Promise\s*\(/.test(code)) {
322
+ warnings.push({
323
+ type: "potentially_unsafe",
324
+ message: "Direct Promise constructor usage - consider using async/await instead",
325
+ });
326
+ }
327
+ }
328
+
329
+ private checkForPotentiallyUnsafePatterns(code: string, warnings: ScriptValidationWarning[]): void {
330
+ // Check for try-catch with broad catch - warn
331
+ if (/\bcatch\s*\(\s*\)\s*\{/.test(code)) {
332
+ warnings.push({
333
+ type: "potentially_unsafe",
334
+ message: "Broad catch clause - consider catching specific error types",
335
+ });
336
+ }
337
+
338
+ // Check for nested function declarations - warn about potential complexity
339
+ if (/function\s+\w+\s*\([^)]*\)\s*\{[^}]*function\s+/.test(code)) {
340
+ warnings.push({
341
+ type: "potentially_unsafe",
342
+ message: "Nested function declaration - consider extracting to module level",
343
+ });
344
+ }
345
+
346
+ // Check for with statement - deprecated and potentially unsafe
347
+ if (/\bwith\s*\(/.test(code)) {
348
+ warnings.push({
349
+ type: "potentially_unsafe",
350
+ message: "with statement is deprecated and potentially unsafe",
351
+ });
352
+ }
353
+ }
354
+
355
+ /**
356
+ * Execute a script with validation.
357
+ * @param code - The JavaScript code to execute
358
+ * @param options - Execution options
359
+ * @returns The execution result
360
+ */
361
+ execute(code: string, options?: DynamicScriptOptions): ScriptExecutionResult {
362
+ const startTime = Date.now();
363
+ const timeout = options?.timeout ?? this.defaultTimeout;
364
+
365
+ // Validate first
366
+ const validation = this.validate(code);
367
+ if (!validation.valid) {
368
+ return {
369
+ success: false,
370
+ error: validation.errors.map((e) => e.message).join("; "),
371
+ executionTime: Date.now() - startTime,
372
+ validation,
373
+ };
374
+ }
375
+
376
+ try {
377
+ const value = this.sandbox.execute(code, timeout);
378
+ return {
379
+ success: true,
380
+ value,
381
+ executionTime: Date.now() - startTime,
382
+ validation,
383
+ };
384
+ } catch (error) {
385
+ return {
386
+ success: false,
387
+ error: error instanceof Error ? error.message : String(error),
388
+ executionTime: Date.now() - startTime,
389
+ validation,
390
+ };
391
+ }
392
+ }
393
+
394
+ /**
395
+ * Execute an async script with validation.
396
+ * @param code - The JavaScript code to execute (must be async or return Promise)
397
+ * @param options - Execution options
398
+ * @returns Promise resolving to the execution result
399
+ */
400
+ async executeAsync(code: string, options?: DynamicScriptOptions): Promise<ScriptExecutionResult> {
401
+ const startTime = Date.now();
402
+ const timeout = options?.timeout ?? this.defaultTimeout;
403
+
404
+ // Wrap in async IIFE for async/await support
405
+ const asyncCode = `(async () => { ${code} })()`;
406
+
407
+ // Validate the wrapped code (not the original code)
408
+ const validation = this.validate(asyncCode);
409
+ if (!validation.valid) {
410
+ return {
411
+ success: false,
412
+ error: validation.errors.map((e) => e.message).join("; "),
413
+ executionTime: Date.now() - startTime,
414
+ validation,
415
+ };
416
+ }
417
+
418
+ try {
419
+ // Execute using vm directly for async support
420
+ const script = new vm.Script(asyncCode, {
421
+ filename: "workflow.js",
422
+ });
423
+
424
+ const result = await script.runInContext(this.sandbox.getContext(), {
425
+ timeout,
426
+ displayErrors: true,
427
+ });
428
+ return {
429
+ success: true,
430
+ value: result,
431
+ executionTime: Date.now() - startTime,
432
+ validation,
433
+ };
434
+ } catch (error) {
435
+ return {
436
+ success: false,
437
+ error: error instanceof Error ? error.message : String(error),
438
+ executionTime: Date.now() - startTime,
439
+ validation,
440
+ };
441
+ }
442
+ }
443
+
444
+ /**
445
+ * Execute a script without validation (assumes pre-validated).
446
+ * Use with caution - prefer execute() for untrusted scripts.
447
+ */
448
+ executeUnchecked(code: string, timeout?: number): ScriptExecutionResult {
449
+ const startTime = Date.now();
450
+
451
+ try {
452
+ const value = this.sandbox.execute(code, timeout ?? this.defaultTimeout);
453
+ return {
454
+ success: true,
455
+ value,
456
+ executionTime: Date.now() - startTime,
457
+ validation: { valid: true, errors: [], warnings: [] },
458
+ };
459
+ } catch (error) {
460
+ return {
461
+ success: false,
462
+ error: error instanceof Error ? error.message : String(error),
463
+ executionTime: Date.now() - startTime,
464
+ validation: { valid: true, errors: [], warnings: [] },
465
+ };
466
+ }
467
+ }
468
+
469
+ /**
470
+ * Get the list of forbidden globals for documentation.
471
+ */
472
+ getForbiddenGlobals(): readonly string[] {
473
+ return FORBIDDEN_GLOBALS;
474
+ }
475
+ }
476
+
477
+ /**
478
+ * Create a pre-configured script runner for workflow execution.
479
+ */
480
+ export function createScriptRunner(options?: DynamicScriptOptions): DynamicScriptRunner {
481
+ return new DynamicScriptRunner(options);
482
+ }