pi-crew 0.5.14 → 0.5.17

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 (90) hide show
  1. package/CHANGELOG.md +171 -0
  2. package/README.md +1 -1
  3. package/docs/pi-crew-v0.5.16-audit-fix-plan.md +35 -0
  4. package/docs/pi-crew-v0.5.17-audit-fix-plan.md +80 -0
  5. package/docs/skills/REFERENCE.md +11 -0
  6. package/package.json +1 -1
  7. package/skills/artifact-analysis-loop/SKILL.md +1 -0
  8. package/skills/async-worker-recovery/SKILL.md +1 -0
  9. package/skills/child-pi-spawning/SKILL.md +1 -0
  10. package/skills/context-artifact-hygiene/SKILL.md +1 -0
  11. package/skills/delegation-patterns/SKILL.md +1 -0
  12. package/skills/detection-pipeline-design/SKILL.md +2 -1
  13. package/skills/event-log-tracing/SKILL.md +1 -0
  14. package/skills/git-master/SKILL.md +1 -0
  15. package/skills/hunting-investigation-loop/SKILL.md +1 -0
  16. package/skills/incident-playbook-construction/SKILL.md +1 -0
  17. package/skills/iterative-audit/SKILL.md +331 -0
  18. package/skills/live-agent-lifecycle/SKILL.md +1 -0
  19. package/skills/mailbox-interactive/SKILL.md +1 -0
  20. package/skills/model-routing-context/SKILL.md +2 -1
  21. package/skills/multi-perspective-review/SKILL.md +1 -0
  22. package/skills/observability-reliability/SKILL.md +1 -0
  23. package/skills/orchestration/SKILL.md +2 -1
  24. package/skills/ownership-session-security/SKILL.md +1 -0
  25. package/skills/pi-extension-lifecycle/SKILL.md +3 -2
  26. package/skills/post-mortem/SKILL.md +1 -0
  27. package/skills/read-only-explorer/SKILL.md +1 -0
  28. package/skills/requirements-to-task-packet/SKILL.md +1 -0
  29. package/skills/resource-discovery-config/SKILL.md +2 -1
  30. package/skills/runtime-state-reader/SKILL.md +1 -0
  31. package/skills/safe-bash/SKILL.md +1 -0
  32. package/skills/scrutinize/SKILL.md +1 -0
  33. package/skills/secure-agent-orchestration-review/SKILL.md +1 -0
  34. package/skills/security-review/SKILL.md +1 -0
  35. package/skills/state-mutation-locking/SKILL.md +1 -0
  36. package/skills/systematic-debugging/SKILL.md +1 -0
  37. package/skills/threat-hypothesis-framework/SKILL.md +1 -0
  38. package/skills/ui-render-performance/SKILL.md +2 -1
  39. package/skills/verification-before-done/SKILL.md +1 -0
  40. package/skills/widget-rendering/SKILL.md +2 -1
  41. package/skills/workspace-isolation/SKILL.md +1 -0
  42. package/skills/worktree-isolation/SKILL.md +1 -0
  43. package/src/config/types.ts +1 -0
  44. package/src/extension/management.ts +1 -1
  45. package/src/extension/plan-orchestrate.ts +0 -1
  46. package/src/extension/register.ts +16 -7
  47. package/src/extension/registration/viewers.ts +1 -1
  48. package/src/extension/run-index.ts +1 -1
  49. package/src/extension/team-tool/explain.ts +0 -1
  50. package/src/extension/team-tool/handle-schedule.ts +0 -1
  51. package/src/extension/team-tool/health-monitor.ts +0 -1
  52. package/src/extension/team-tool/orchestrate.ts +12 -4
  53. package/src/extension/team-tool/run.ts +2 -2
  54. package/src/extension/team-tool/status.ts +1 -1
  55. package/src/extension/team-tool.ts +2 -30
  56. package/src/observability/exporters/otlp-exporter.ts +11 -1
  57. package/src/runtime/adaptive-plan.ts +18 -2
  58. package/src/runtime/child-pi.ts +18 -6
  59. package/src/runtime/crash-recovery.ts +1 -1
  60. package/src/runtime/crew-agent-records.ts +23 -3
  61. package/src/runtime/crew-hooks.ts +1 -1
  62. package/src/runtime/dynamic-script-runner.ts +14 -1
  63. package/src/runtime/handoff-manager.ts +0 -1
  64. package/src/runtime/heartbeat-watcher.ts +1 -1
  65. package/src/runtime/live-session-runtime.ts +0 -1
  66. package/src/runtime/loop-gates.ts +0 -1
  67. package/src/runtime/mcp-proxy.ts +2 -2
  68. package/src/runtime/pipeline-runner.ts +1 -2
  69. package/src/runtime/sandbox.ts +8 -0
  70. package/src/runtime/task-packet.ts +124 -0
  71. package/src/runtime/task-runner/live-executor.ts +1 -2
  72. package/src/runtime/task-runner/prompt-builder.ts +4 -1
  73. package/src/runtime/task-runner.ts +2 -2
  74. package/src/schema/config-schema.ts +1 -0
  75. package/src/state/event-log.ts +7 -0
  76. package/src/state/jsonl-writer.ts +24 -0
  77. package/src/state/locks.ts +66 -35
  78. package/src/state/run-metrics.ts +1 -2
  79. package/src/state/schedule.ts +13 -5
  80. package/src/state/state-store.ts +1 -1
  81. package/src/tools/safe-bash-extension.ts +1 -1
  82. package/src/tools/safe-bash.ts +10 -1
  83. package/src/ui/crew-widget.ts +2 -2
  84. package/src/ui/render-diff.ts +1 -1
  85. package/src/ui/run-dashboard.ts +1 -2
  86. package/src/ui/tool-render.ts +20 -3
  87. package/src/utils/conflict-detect.ts +0 -1
  88. package/src/utils/gh-protocol.ts +0 -2
  89. package/src/workflows/workflow-config.ts +3 -0
  90. package/src/worktree/worktree-manager.ts +75 -1
@@ -6,7 +6,7 @@ import type {
6
6
  ExtensionContext,
7
7
  } from "@earendil-works/pi-coding-agent";
8
8
  import { loadConfig } from "../config/config.ts";
9
- import { applyCrewSettingsToConfig, loadCrewSettings, saveCrewSettings } from "../runtime/settings-store.ts";
9
+ import { applyCrewSettingsToConfig, loadCrewSettings } from "../runtime/settings-store.ts";
10
10
  // 2.7: Lazy-load LiveRunSidebar — only constructed when the user actually opens
11
11
  // a live run sidebar overlay. The class pulls in transcript-viewer and other
12
12
  // heavy UI modules.
@@ -47,12 +47,9 @@ import {
47
47
  createMetricFileSink,
48
48
  type MetricSink,
49
49
  } from "../observability/metric-sink.ts";
50
- import { killProcessPid } from "../runtime/child-pi.ts";
51
50
  import { listLiveAgents } from "../runtime/live-agent-manager.ts";
52
51
  import { createManifestCache } from "../runtime/manifest-cache.ts";
53
- import { checkProcessLiveness } from "../runtime/process-status.ts";
54
52
  import { CrewScheduler } from "../runtime/scheduler.ts";
55
- import { appendEvent } from "../state/event-log.ts";
56
53
  import { loadRunManifestById, updateRunStatus } from "../state/state-store.ts";
57
54
  import type { TeamRunManifest } from "../state/types.ts";
58
55
  import { SubagentManager } from "../subagents/manager.ts";
@@ -128,9 +125,6 @@ import type {
128
125
  // deferred cleanup and cleanupRuntime. Each function is awaited inside an
129
126
  // async context that already runs after registration completes.
130
127
  import {
131
- cancelOrphanedRuns,
132
- detectInterruptedRuns,
133
- purgeStaleActiveRunIndex,
134
128
  reconcileAllStaleRuns,
135
129
  } from "../runtime/crash-recovery.ts";
136
130
  import { appendDeadletter } from "../runtime/deadletter.ts";
@@ -482,6 +476,13 @@ export function registerPiTeams(pi: ExtensionAPI): void {
482
476
  }
483
477
  };
484
478
  const autoRecoveryLast = new Map<string, number>();
479
+ // FIX (Round 22, defensive cap): Bound the cooldown-gate Map. Each run
480
+ // contributes up to 4 keys (one per maybeNotifyHealth kind). Without a cap,
481
+ // a long-running pi session that runs thousands of teams accumulates
482
+ // thousands of entries. Eviction: oldest insertion first — matches the
483
+ // 5-minute cooldown gate semantics, since once the gate has expired the
484
+ // entry is irrelevant.
485
+ const AUTO_RECOVERY_LAST_MAX_ENTRIES = 1000;
485
486
  const configureDeliveryCoordinator = (): void => {
486
487
  deliveryCoordinator?.dispose();
487
488
  deliveryCoordinator = undefined;
@@ -1531,6 +1532,14 @@ export function registerPiTeams(pi: ExtensionAPI): void {
1531
1532
  now - previous < 5 * 60_000
1532
1533
  )
1533
1534
  return;
1535
+ // Defensive cap: evict oldest entries before inserting
1536
+ // when size exceeds the limit. Map's natural insertion
1537
+ // order means the first key is the oldest.
1538
+ while (autoRecoveryLast.size >= AUTO_RECOVERY_LAST_MAX_ENTRIES) {
1539
+ const oldest = autoRecoveryLast.keys().next().value;
1540
+ if (oldest === undefined) break;
1541
+ autoRecoveryLast.delete(oldest);
1542
+ }
1534
1543
  autoRecoveryLast.set(key, now);
1535
1544
  notifyOperator({
1536
1545
  id: key,
@@ -2,7 +2,7 @@ import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
2
2
  import { loadRunManifestById } from "../../state/state-store.ts";
3
3
  import { readCrewAgents } from "../../runtime/crew-agent-records.ts";
4
4
  import { loadConfig } from "../../config/config.ts";
5
- import { listLiveAgents, type LiveAgentHandle } from "../../runtime/live-agent-manager.ts";
5
+ import { listLiveAgents } from "../../runtime/live-agent-manager.ts";
6
6
  import { LiveConversationOverlay } from "../../ui/live-conversation-overlay.ts";
7
7
  import { asCrewTheme } from "../../ui/theme-adapter.ts";
8
8
  // Lazy-loaded: DurableTranscriptViewer is 658ms — only needed for /crew transcript command
@@ -7,7 +7,7 @@ import { findRepoRoot, projectCrewRoot, userCrewRoot } from "../utils/paths.ts";
7
7
  import { activeRunEntries } from "../state/active-run-registry.ts";
8
8
  import { isSafePathId, resolveRealContainedPath } from "../utils/safe-paths.ts";
9
9
  import { sharedScanCache } from "../utils/scan-cache.ts";
10
- import { CancellationToken, createCancellationToken } from "../runtime/cancellation-token.ts";
10
+ import { createCancellationToken } from "../runtime/cancellation-token.ts";
11
11
 
12
12
  function readManifest(filePath: string): TeamRunManifest | undefined {
13
13
  const cached = sharedScanCache.readAndCache("manifests", filePath, filePath);
@@ -1,6 +1,5 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
- import { toolResult } from "../tool-result.ts";
4
3
  import { loadRunManifestById } from "../../state/state-store.ts";
5
4
  import type { TeamRunManifest, TeamTaskState } from "../../state/types.ts";
6
5
 
@@ -3,7 +3,6 @@ import type { PiTeamsToolResult } from "../tool-result.ts";
3
3
  import type { TeamToolParamsValue } from "../../schema/team-tool-schema.ts";
4
4
  import { result, type TeamContext } from "./context.ts";
5
5
  import { humanizeSchedule, nextRunTime, parseSchedule } from "../../runtime/scheduler.ts";
6
- import { loadConfig } from "../../config/config.ts";
7
6
  import { loadCrewSettings, saveCrewSettings } from "../../runtime/settings-store.ts";
8
7
 
9
8
  // Global key for cross-module scheduler access.
@@ -8,7 +8,6 @@ import { listRuns } from "../run-index.ts";
8
8
  import { readCrewAgents } from "../../runtime/crew-agent-records.ts";
9
9
  import {
10
10
  isActiveRunStatus,
11
- isFinishedRunStatus,
12
11
  hasStaleAsyncProcess,
13
12
  isLikelyOrphanedActiveRun,
14
13
  } from "../../runtime/process-status.ts";
@@ -15,6 +15,7 @@ import {
15
15
  parsePlanDocumentSimple,
16
16
  type OrchestratedStep,
17
17
  } from "../plan-orchestrate.ts";
18
+ import { resolveContainedPath } from "../../utils/safe-paths.ts";
18
19
 
19
20
  /**
20
21
  * Handle the orchestrate action.
@@ -38,10 +39,17 @@ export function handleOrchestrate(
38
39
  );
39
40
  }
40
41
 
41
- // Resolve relative paths against ctx.cwd
42
- const resolvedPath = path.isAbsolute(planPath)
43
- ? planPath
44
- : path.resolve(ctx.cwd, planPath);
42
+ // Resolve and validate path stays within ctx.cwd (path-traversal protection)
43
+ let resolvedPath: string;
44
+ try {
45
+ resolvedPath = resolveContainedPath(ctx.cwd, planPath);
46
+ } catch {
47
+ return result(
48
+ `planPath must be within project directory: ${planPath}`,
49
+ { action: "orchestrate", status: "error" },
50
+ true,
51
+ );
52
+ }
45
53
 
46
54
  if (!fs.existsSync(resolvedPath)) {
47
55
  return result(
@@ -8,7 +8,7 @@ import { registerActiveRun, unregisterActiveRun } from "../../state/active-run-r
8
8
  import { createRunManifest, loadRunManifestById, updateRunStatus } from "../../state/state-store.ts";
9
9
  import { atomicWriteJson } from "../../state/atomic-write.ts";
10
10
  import { validateWorkflowForTeam } from "../../workflows/validate-workflow.ts";
11
- import { PipelineRunner, type PipelineWorkflow, type PipelineStage } from "../../runtime/pipeline-runner.ts";
11
+ import { PipelineRunner, type PipelineWorkflow } from "../../runtime/pipeline-runner.ts";
12
12
  // Heavy runtime — lazy-loaded to avoid 1.4s import cost at extension registration.
13
13
  import type { executeTeamRun as ExecuteTeamRunFn } from "../../runtime/team-runner.ts";
14
14
  // eslint-disable-next-line @typescript-eslint/no-unused-vars -- type-only import for TS inference
@@ -24,7 +24,7 @@ async function executeTeamRun(...args: Parameters<typeof ExecuteTeamRunFn>): Pro
24
24
  return _cachedExecuteTeamRun(...args);
25
25
  }
26
26
  import { spawnBackgroundTeamRun } from "../../subagents/async-entry.ts";
27
- import { appendEvent, appendEventAsync, readEvents } from "../../state/event-log.ts";
27
+ import { appendEventAsync, readEvents } from "../../state/event-log.ts";
28
28
  import { resolveCrewRuntime, runtimeResolutionState } from "../../runtime/runtime-resolver.ts";
29
29
  import { normalizeSkillOverride } from "../../runtime/skill-instructions.ts";
30
30
  import { expandParallelResearchWorkflow } from "../../runtime/parallel-research.ts";
@@ -8,7 +8,7 @@ import { applyAttentionState, formatActivityAge, resolveCrewControlConfig } from
8
8
  import { readCrewAgents } from "../../runtime/crew-agent-records.ts";
9
9
  import { checkProcessLiveness, isActiveRunStatus } from "../../runtime/process-status.ts";
10
10
  import { formatTaskGraphLines, waitingReason } from "../../runtime/task-display.ts";
11
- import { verifyTaskCompletion, formatOutputPreview } from "../../runtime/completion-guard.ts";
11
+ import { verifyTaskCompletion } from "../../runtime/completion-guard.ts";
12
12
  import { evaluateRunEffectiveness } from "../../runtime/effectiveness.ts";
13
13
  import type { PiTeamsToolResult } from "../tool-result.ts";
14
14
  import { locateRunCwd } from "../team-tool.ts";
@@ -4,7 +4,6 @@ import type { AgentConfig } from "../agents/agent-config.ts";
4
4
  import {
5
5
  allAgents,
6
6
  discoverAgents,
7
- invalidateAgentDiscoveryCache,
8
7
  listDynamicAgents,
9
8
  registerDynamicAgent,
10
9
  unregisterDynamicAgent,
@@ -19,8 +18,8 @@ import {
19
18
  import type { executeTeamRun as _executeTeamRunFn } from "../runtime/team-runner.ts";
20
19
  import type { TeamToolParamsValue } from "../schema/team-tool-schema.ts";
21
20
  import { writeArtifact } from "../state/artifact-store.ts";
22
- import { appendEvent, readEvents } from "../state/event-log.ts";
23
- import { withRunLock, withRunLockSync } from "../state/locks.ts";
21
+ import { appendEvent } from "../state/event-log.ts";
22
+ import { withRunLock } from "../state/locks.ts";
24
23
  import { replayPendingMailboxMessages } from "../state/mailbox.ts";
25
24
  import {
26
25
  loadRunManifestById,
@@ -33,22 +32,15 @@ import type {
33
32
  TeamRunManifest,
34
33
  TeamTaskState,
35
34
  } from "../state/types.ts";
36
- import { aggregateUsage, formatUsage } from "../state/usage.ts";
37
35
  import { allTeams, discoverTeams } from "../teams/discover-teams.ts";
38
36
  import {
39
37
  allWorkflows,
40
38
  discoverWorkflows,
41
39
  } from "../workflows/discover-workflows.ts";
42
- import { validateWorkflowForTeam } from "../workflows/validate-workflow.ts";
43
- import { cleanupRunWorktrees } from "../worktree/cleanup.ts";
44
40
  import { piTeamsHelp } from "./help.ts";
45
- import { listImportedRuns } from "./import-index.ts";
46
41
  import { handleCreate, handleDelete, handleUpdate } from "./management.ts";
47
42
  import { initializeProject } from "./project-init.ts";
48
- import { exportRunBundle } from "./run-export.ts";
49
- import { importRunBundle } from "./run-import.ts";
50
43
  import { listRuns } from "./run-index.ts";
51
- import { pruneFinishedRuns } from "./run-maintenance.ts";
52
44
  import { formatRecommendation, recommendTeam } from "./team-recommendation.ts";
53
45
  import { handleSettings } from "./team-tool/handle-settings.ts";
54
46
  import type { PiTeamsToolResult } from "./tool-result.ts";
@@ -70,31 +62,12 @@ async function executeTeamRun(
70
62
  return _cachedExecuteTeamRun(...args);
71
63
  }
72
64
 
73
- import {
74
- applyAttentionState,
75
- formatActivityAge,
76
- resolveCrewControlConfig,
77
- } from "../runtime/agent-control.ts";
78
- import {
79
- readCrewAgents,
80
- recordFromTask,
81
- saveCrewAgents,
82
- } from "../runtime/crew-agent-records.ts";
83
65
  import { directTeamAndWorkflowFromRun } from "../runtime/direct-run.ts";
84
- import { writeForegroundInterruptRequest } from "../runtime/foreground-control.ts";
85
66
  import { parsePiJsonOutput } from "../runtime/pi-json-output.ts";
86
- import {
87
- checkProcessLiveness,
88
- isActiveRunStatus,
89
- } from "../runtime/process-status.ts";
90
67
  import {
91
68
  resolveCrewRuntime,
92
69
  runtimeResolutionState,
93
70
  } from "../runtime/runtime-resolver.ts";
94
- import {
95
- formatTaskGraphLines,
96
- waitingReason,
97
- } from "../runtime/task-display.ts";
98
71
  import { handleApi } from "./team-tool/api.ts";
99
72
  import {
100
73
  autonomousPatchFromConfig,
@@ -128,7 +101,6 @@ async function handleRun(
128
101
 
129
102
  import { waitForRun } from "../runtime/run-tracker.ts";
130
103
  import { normalizeSkillOverride } from "../runtime/skill-instructions.ts";
131
- import { logInternalError } from "../utils/internal-error.ts";
132
104
  import { searchAgents, searchTeams } from "../utils/bm25-search.ts";
133
105
  import { projectCrewRoot } from "../utils/paths.ts";
134
106
  import {
@@ -124,8 +124,18 @@ export class OTLPExporter implements MetricExporter {
124
124
  }
125
125
  }
126
126
 
127
- dispose(): void {
127
+ /**
128
+ * FIX (Round 23, resource cleanup): Make dispose() async and await the
129
+ * in-flight push so it completes (or aborts) before we return. The push
130
+ * itself is bounded by the 10s fetch timeout, so this won't hang
131
+ * indefinitely. Without this, dispose() would orphan an in-flight
132
+ * network request whose result is then discarded.
133
+ */
134
+ async dispose(): Promise<void> {
128
135
  if (this.timer) clearInterval(this.timer);
129
136
  this.timer = undefined;
137
+ if (this.inFlight) {
138
+ try { await this.inFlight; } catch { /* swallow — push() already logs errors */ }
139
+ }
130
140
  }
131
141
  }
@@ -44,11 +44,27 @@ export function slug(value: string): string {
44
44
  return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 32) || "task";
45
45
  }
46
46
 
47
+ /** Strip surrounding markdown code fences if present. */
48
+ function stripCodeFence(raw: string): string {
49
+ let s = raw.trim();
50
+ // Remove opening fence: ```json or ```
51
+ if (s.startsWith("```")) {
52
+ const firstNewline = s.indexOf("\n");
53
+ if (firstNewline >= 0) s = s.slice(firstNewline + 1);
54
+ else s = s.slice(3); // edge case: ``` alone on one line
55
+ }
56
+ // Remove closing fence
57
+ if (s.endsWith("```")) {
58
+ s = s.slice(0, -3);
59
+ }
60
+ return s.trim();
61
+ }
62
+
47
63
  export function extractAdaptivePlanJson(text: string): string | undefined {
48
64
  const markerMatch = text.match(/ADAPTIVE_PLAN_JSON_START\s*([\s\S]*?)\s*ADAPTIVE_PLAN_JSON_END/);
49
- if (markerMatch?.[1]) return markerMatch[1];
65
+ if (markerMatch?.[1]) return stripCodeFence(markerMatch[1]);
50
66
  const startIndex = text.indexOf("ADAPTIVE_PLAN_JSON_START");
51
- if (startIndex >= 0) return text.slice(startIndex + "ADAPTIVE_PLAN_JSON_START".length).trim();
67
+ if (startIndex >= 0) return stripCodeFence(text.slice(startIndex + "ADAPTIVE_PLAN_JSON_START".length));
52
68
  const fencedMatch = text.match(/```(?:json)?\s*([\s\S]*?)```/i);
53
69
  return fencedMatch?.[1];
54
70
  }
@@ -8,7 +8,7 @@ import { getPiSpawnCommand } from "./pi-spawn.ts";
8
8
  import { DEFAULT_CHILD_PI } from "../config/defaults.ts";
9
9
  import { logInternalError } from "../utils/internal-error.ts";
10
10
  import { attachPostExitStdioGuard, trySignalChild } from "./post-exit-stdio-guard.ts";
11
- import { redactJsonLine, isSecretKey } from "../utils/redaction.ts";
11
+ import { redactJsonLine } from "../utils/redaction.ts";
12
12
  import { sanitizeEnvSecrets } from "../utils/env-filter.ts";
13
13
  import { registerChildProcess, unregisterChildProcess } from "../extension/crew-cleanup.ts";
14
14
 
@@ -181,6 +181,16 @@ export function buildChildPiSpawnOptions(cwd: string, env: NodeJS.ProcessEnv): S
181
181
  // Bug #12 fix: essential env vars (PATH, HOME, etc.) are always preserved so child can find npm/node.
182
182
  const filteredEnv = sanitizeEnvSecrets(env, {
183
183
  allowList: [
184
+ /*
185
+ * SECURITY WARNING: All model provider API keys below are passed to EVERY child worker.
186
+ * If any child is compromised (e.g. via prompt injection), all listed keys are exposed.
187
+ * This is a deliberate trade-off: multi-provider setups require the child Pi process to
188
+ * authenticate with whichever provider the model routes to. Reducing keys per-child
189
+ * would break multi-provider functionality. Mitigations:
190
+ * - sanitizeEnvSecrets strips all env vars NOT on this list.
191
+ * - Do NOT add wildcards ("*_API_KEY") — only explicit, intended provider keys.
192
+ * - Consider per-task key scoping if the architecture allows it in the future.
193
+ */
184
194
  // Model provider API keys (explicit list — do NOT use wildcards)
185
195
  "MINIMAX_API_KEY",
186
196
  "MINIMAX_GROUP_ID",
@@ -405,14 +415,16 @@ export async function runChildPi(input: ChildPiRunInput): Promise<ChildPiRunResu
405
415
  if (depth.blocked) return { exitCode: 1, stdout: "", stderr: `pi-crew depth guard blocked child worker: depth ${depth.depth} >= max ${depth.maxDepth}` };
406
416
  const mock = process.env.PI_TEAMS_MOCK_CHILD_PI;
407
417
  if (mock) {
408
- // SECURITY: Log mock mode activation prominently for audit trail
409
- console.warn(`[⚠️ PI_CREW_MOCK_MODE] Mock mode active: ${mock} NOT running real agents!`);
410
- // SECURITY FIX: Require PI_CREW_ALLOW_MOCK alongside PI_TEAMS_MOCK_CHILD_PI
418
+ // SECURITY: Require explicit PI_CREW_ALLOW_MOCK=1 to activate mock mode.
419
+ // PI_CREW_ALLOW_MOCK must be set in the parent process env (not by child hooks)
420
+ // since sanitizeEnvSecrets only passes PI_CREW_* vars from the parent.
421
+ // Setup hooks cannot inject PI_CREW_ALLOW_MOCK into the parent's env.
411
422
  const allowMock = process.env.PI_CREW_ALLOW_MOCK === "1" || process.env.PI_CREW_ALLOW_MOCK === "true";
412
423
  if (!allowMock) {
413
- console.error(`[🚨 PI_CREW_MOCK_MODE] SECURITY: PI_TEAMS_MOCK_CHILD_PI is set but PI_CREW_ALLOW_MOCK is not "1". Ignoring mock request for safety.`);
414
- return { exitCode: 1, stdout: "", stderr: "Mock mode requires PI_CREW_ALLOW_MOCK=1 alongside PI_TEAMS_MOCK_CHILD_PI" };
424
+ return { exitCode: 1, stdout: "", stderr: "Mock mode requires PI_CREW_ALLOW_MOCK=1" };
415
425
  }
426
+ // SECURITY: Log mock mode activation prominently for audit trail
427
+ console.warn(`Mock mode active: ${mock} — NOT running real agents!`);
416
428
  if (mock === "success") {
417
429
  const stdout = `[MOCK] Success for ${input.agent.name}\n`;
418
430
  observeStdoutChunk(input, stdout);
@@ -11,7 +11,7 @@ import type { ManifestCache } from "./manifest-cache.ts";
11
11
  import { checkProcessLiveness } from "./process-status.ts";
12
12
  import { reconcileStaleRun, type ReconcileResult } from "./stale-reconciler.ts";
13
13
  import { executeHook, appendHookEvent } from "../hooks/registry.ts";
14
- import { activeRunEntries, unregisterActiveRun, readActiveRunRegistry } from "../state/active-run-registry.ts";
14
+ import { unregisterActiveRun, readActiveRunRegistry } from "../state/active-run-registry.ts";
15
15
  import { resolveRealContainedPath } from "../utils/safe-paths.ts";
16
16
  import { projectCrewRoot, userCrewRoot } from "../utils/paths.ts";
17
17
  import { terminateLiveAgentsForRun } from "./live-agent-manager.ts";
@@ -263,8 +263,28 @@ export function readCrewAgentStatus(manifest: TeamRunManifest, taskOrAgentId: st
263
263
  }
264
264
 
265
265
  const agentEventSeqCache = new Map<string, { size: number; mtimeMs: number; seq: number }>();
266
+ // FIX (Round 22, defensive cap): Bound the per-file-path cache. Without a cap,
267
+ // a long-running pi-crew process that spawns 1000s of agents accumulates 1000s
268
+ // of entries. Mirrors the `asyncAgentReaderCache` pattern (above) and the
269
+ // `NotificationRouter.SEEN_MAP_MAX_SIZE` pattern.
270
+ const AGENT_EVENT_SEQ_CACHE_MAX_ENTRIES = 1000;
266
271
  const AGENT_EVENT_SEQ_SIDECAR = ".seq";
267
272
 
273
+ /**
274
+ * Set an entry in the seq cache, evicting the oldest entries when the cache
275
+ * exceeds the cap. Map's natural insertion order means the first key is the
276
+ * oldest — same as the pattern used in `asyncAgentReaderCache`.
277
+ */
278
+ function setAgentEventSeqCache(filePath: string, entry: { size: number; mtimeMs: number; seq: number }): void {
279
+ if (agentEventSeqCache.has(filePath)) agentEventSeqCache.delete(filePath);
280
+ agentEventSeqCache.set(filePath, entry);
281
+ while (agentEventSeqCache.size > AGENT_EVENT_SEQ_CACHE_MAX_ENTRIES) {
282
+ const oldest = agentEventSeqCache.keys().next().value;
283
+ if (oldest === undefined) break;
284
+ agentEventSeqCache.delete(oldest);
285
+ }
286
+ }
287
+
268
288
  function readSeqFromSidecar(filePath: string): number | undefined {
269
289
  try {
270
290
  const raw = fs.readFileSync(`${filePath}.${AGENT_EVENT_SEQ_SIDECAR}`, "utf-8");
@@ -295,7 +315,7 @@ function nextAgentEventSeq(filePath: string): number {
295
315
  // FIX: Try sidecar file for O(1) lookup before falling back to O(n) scan.
296
316
  const sidecarSeq = readSeqFromSidecar(filePath);
297
317
  if (sidecarSeq !== undefined) {
298
- agentEventSeqCache.set(filePath, { size: stat.size, mtimeMs: stat.mtimeMs, seq: sidecarSeq });
318
+ setAgentEventSeqCache(filePath, { size: stat.size, mtimeMs: stat.mtimeMs, seq: sidecarSeq });
299
319
  return sidecarSeq + 1;
300
320
  }
301
321
  let max = 0;
@@ -309,7 +329,7 @@ function nextAgentEventSeq(filePath: string): number {
309
329
  max += 1;
310
330
  }
311
331
  }
312
- agentEventSeqCache.set(filePath, { size: stat.size, mtimeMs: stat.mtimeMs, seq: max });
332
+ setAgentEventSeqCache(filePath, { size: stat.size, mtimeMs: stat.mtimeMs, seq: max });
313
333
  writeSeqToSidecar(filePath, max);
314
334
  return max + 1;
315
335
  }
@@ -321,7 +341,7 @@ export function appendCrewAgentEvent(manifest: TeamRunManifest, taskId: string,
321
341
  fs.appendFileSync(filePath, `${JSON.stringify(redactSecrets({ seq, time: new Date().toISOString(), event }))}\n`, "utf-8");
322
342
  try {
323
343
  const stat = fs.statSync(filePath);
324
- agentEventSeqCache.set(filePath, { size: stat.size, mtimeMs: stat.mtimeMs, seq });
344
+ setAgentEventSeqCache(filePath, { size: stat.size, mtimeMs: stat.mtimeMs, seq });
325
345
  writeSeqToSidecar(filePath, seq);
326
346
  } catch (error) {
327
347
  logInternalError("crew-agent-records.stat", error, `filePath=${filePath}`);
@@ -146,7 +146,7 @@ export class HookRegistry {
146
146
  emit(event: CrewHookEvent): void {
147
147
  // Validate event type using type guard
148
148
  if (!isValidEventType(event.type)) {
149
- console.warn(`[crew-hooks] Unknown event type: ${event.type}`);
149
+ logInternalError("crew-hooks.unknown-event-type", new Error(`Unknown event type: ${event.type}`));
150
150
  return;
151
151
  }
152
152
 
@@ -444,8 +444,9 @@ export class DynamicScriptRunner {
444
444
  /**
445
445
  * Execute a script without validation (assumes pre-validated).
446
446
  * Use with caution - prefer execute() for untrusted scripts.
447
+ * @internal TEST ONLY — do not use in production code
447
448
  */
448
- executeUnchecked(code: string, timeout?: number): ScriptExecutionResult {
449
+ private executeUnchecked(code: string, timeout?: number): ScriptExecutionResult {
449
450
  const startTime = Date.now();
450
451
 
451
452
  try {
@@ -480,3 +481,15 @@ export class DynamicScriptRunner {
480
481
  export function createScriptRunner(options?: DynamicScriptOptions): DynamicScriptRunner {
481
482
  return new DynamicScriptRunner(options);
482
483
  }
484
+
485
+ /**
486
+ * @internal TEST ONLY — do not use in production code.
487
+ * Exposes DynamicScriptRunner.executeUnchecked for unit testing.
488
+ */
489
+ export function __test_executeUnchecked(
490
+ runner: DynamicScriptRunner,
491
+ code: string,
492
+ timeout?: number,
493
+ ): ScriptExecutionResult {
494
+ return (runner as unknown as { executeUnchecked: (code: string, timeout?: number) => ScriptExecutionResult }).executeUnchecked(code, timeout);
495
+ }
@@ -55,7 +55,6 @@ export function isValidHandoffSummary(value: unknown): value is HandoffSummary {
55
55
  */
56
56
 
57
57
  import type { TeamEvent } from "../state/event-log.ts";
58
- import { appendEventAsync } from "../state/event-log.ts";
59
58
 
60
59
  /**
61
60
  * Represents a key decision made during task execution.
@@ -6,7 +6,7 @@ import { loadRunManifestById } from "../state/state-store.ts";
6
6
  import type { TeamRunManifest } from "../state/types.ts";
7
7
  import { logInternalError } from "../utils/internal-error.ts";
8
8
  import type { ManifestCache } from "./manifest-cache.ts";
9
- import { classifyHeartbeat, DEFAULT_GRADIENT_THRESHOLDS, heartbeatAgeMs, type GradientThresholds, type HeartbeatLevel } from "./heartbeat-gradient.ts";
9
+ import { DEFAULT_GRADIENT_THRESHOLDS, heartbeatAgeMs, type GradientThresholds, type HeartbeatLevel } from "./heartbeat-gradient.ts";
10
10
 
11
11
  export interface HeartbeatWatcherRouter {
12
12
  enqueue(notification: NotificationDescriptor): boolean;
@@ -24,7 +24,6 @@ import { buildExtensionBridge } from "./live-extension-bridge.ts";
24
24
  import { logInternalError } from "../utils/internal-error.ts";
25
25
  // prose-compressor imported for custom tool descriptions below;
26
26
  // tool description compression for SDK-managed tools awaits SDK support.
27
- import { compressToolDescription } from "./prose-compressor.ts";
28
27
  import { buildSensitivePathConstraint } from "./sensitive-paths.ts";
29
28
  import { collectLiveSessionHealth, formatLiveSessionDiagnostics, type LiveSessionHealth } from "./live-session-health.ts";
30
29
  import { listLiveAgents } from "./live-agent-manager.ts";
@@ -8,7 +8,6 @@
8
8
  * Distilled from pi-autoresearch's dual-gate loop pattern.
9
9
  */
10
10
  import * as fs from "node:fs";
11
- import * as path from "node:path";
12
11
  import type { TeamTaskState } from "../state/types.ts";
13
12
 
14
13
  /**
@@ -16,8 +16,8 @@
16
16
  * when proxying from the parent.
17
17
  */
18
18
 
19
- import { defineTool, type ToolDefinition } from "@earendil-works/pi-coding-agent";
20
- import { Type, type Static, type TSchema } from "@sinclair/typebox";
19
+ import { type ToolDefinition } from "@earendil-works/pi-coding-agent";
20
+ import { type Static, type TSchema } from "@sinclair/typebox";
21
21
 
22
22
  export interface McpProxyConfig {
23
23
  /** Whether to enable MCP in the child session. */
@@ -2,8 +2,7 @@ import type { TeamTaskState } from "../state/types.ts";
2
2
  import type { WorkflowConfig, WorkflowStep } from "../workflows/workflow-config.ts";
3
3
  import type { TeamConfig } from "../teams/team-config.ts";
4
4
  import type { AgentConfig } from "../agents/agent-config.ts";
5
- import { writeArtifact } from "../state/artifact-store.ts";
6
- import { appendEvent, appendEventAsync } from "../state/event-log.ts";
5
+ import { appendEventAsync } from "../state/event-log.ts";
7
6
  import { mapConcurrent } from "./parallel-utils.ts";
8
7
 
9
8
  /**
@@ -18,6 +18,9 @@ const FORBIDDEN_PATTERNS = [
18
18
  /__dirname/, // __dirname reference
19
19
  /__filename/, // __filename reference
20
20
  /\bdefine\s*\(/, // AMD define
21
+ // Global escape vectors
22
+ /\bglobalThis\b/, // globalThis reference
23
+ /\bglobal\b/, // global reference (Node.js)
21
24
  ] as const;
22
25
 
23
26
  /**
@@ -119,6 +122,11 @@ export class WorkflowSandbox {
119
122
  safeGlobals[key] = value;
120
123
  }
121
124
 
125
+ // Freeze prototypes before passing to sandbox context to prevent
126
+ // prototype pollution from sandboxed code escaping the sandbox.
127
+ Object.freeze(Object.prototype);
128
+ Object.freeze(Array.prototype);
129
+
122
130
  // Context isolation - explicitly list allowed globals
123
131
  const contextGlobals: Record<string, unknown> = {
124
132
  ...safeGlobals,