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.
- package/CHANGELOG.md +171 -0
- package/README.md +1 -1
- package/docs/pi-crew-v0.5.16-audit-fix-plan.md +35 -0
- package/docs/pi-crew-v0.5.17-audit-fix-plan.md +80 -0
- package/docs/skills/REFERENCE.md +11 -0
- package/package.json +1 -1
- package/skills/artifact-analysis-loop/SKILL.md +1 -0
- package/skills/async-worker-recovery/SKILL.md +1 -0
- package/skills/child-pi-spawning/SKILL.md +1 -0
- package/skills/context-artifact-hygiene/SKILL.md +1 -0
- package/skills/delegation-patterns/SKILL.md +1 -0
- package/skills/detection-pipeline-design/SKILL.md +2 -1
- package/skills/event-log-tracing/SKILL.md +1 -0
- package/skills/git-master/SKILL.md +1 -0
- package/skills/hunting-investigation-loop/SKILL.md +1 -0
- package/skills/incident-playbook-construction/SKILL.md +1 -0
- package/skills/iterative-audit/SKILL.md +331 -0
- package/skills/live-agent-lifecycle/SKILL.md +1 -0
- package/skills/mailbox-interactive/SKILL.md +1 -0
- package/skills/model-routing-context/SKILL.md +2 -1
- package/skills/multi-perspective-review/SKILL.md +1 -0
- package/skills/observability-reliability/SKILL.md +1 -0
- package/skills/orchestration/SKILL.md +2 -1
- package/skills/ownership-session-security/SKILL.md +1 -0
- package/skills/pi-extension-lifecycle/SKILL.md +3 -2
- package/skills/post-mortem/SKILL.md +1 -0
- package/skills/read-only-explorer/SKILL.md +1 -0
- package/skills/requirements-to-task-packet/SKILL.md +1 -0
- package/skills/resource-discovery-config/SKILL.md +2 -1
- package/skills/runtime-state-reader/SKILL.md +1 -0
- package/skills/safe-bash/SKILL.md +1 -0
- package/skills/scrutinize/SKILL.md +1 -0
- package/skills/secure-agent-orchestration-review/SKILL.md +1 -0
- package/skills/security-review/SKILL.md +1 -0
- package/skills/state-mutation-locking/SKILL.md +1 -0
- package/skills/systematic-debugging/SKILL.md +1 -0
- package/skills/threat-hypothesis-framework/SKILL.md +1 -0
- package/skills/ui-render-performance/SKILL.md +2 -1
- package/skills/verification-before-done/SKILL.md +1 -0
- package/skills/widget-rendering/SKILL.md +2 -1
- package/skills/workspace-isolation/SKILL.md +1 -0
- package/skills/worktree-isolation/SKILL.md +1 -0
- package/src/config/types.ts +1 -0
- package/src/extension/management.ts +1 -1
- package/src/extension/plan-orchestrate.ts +0 -1
- package/src/extension/register.ts +16 -7
- package/src/extension/registration/viewers.ts +1 -1
- package/src/extension/run-index.ts +1 -1
- package/src/extension/team-tool/explain.ts +0 -1
- package/src/extension/team-tool/handle-schedule.ts +0 -1
- package/src/extension/team-tool/health-monitor.ts +0 -1
- package/src/extension/team-tool/orchestrate.ts +12 -4
- package/src/extension/team-tool/run.ts +2 -2
- package/src/extension/team-tool/status.ts +1 -1
- package/src/extension/team-tool.ts +2 -30
- package/src/observability/exporters/otlp-exporter.ts +11 -1
- package/src/runtime/adaptive-plan.ts +18 -2
- package/src/runtime/child-pi.ts +18 -6
- package/src/runtime/crash-recovery.ts +1 -1
- package/src/runtime/crew-agent-records.ts +23 -3
- package/src/runtime/crew-hooks.ts +1 -1
- package/src/runtime/dynamic-script-runner.ts +14 -1
- package/src/runtime/handoff-manager.ts +0 -1
- package/src/runtime/heartbeat-watcher.ts +1 -1
- package/src/runtime/live-session-runtime.ts +0 -1
- package/src/runtime/loop-gates.ts +0 -1
- package/src/runtime/mcp-proxy.ts +2 -2
- package/src/runtime/pipeline-runner.ts +1 -2
- package/src/runtime/sandbox.ts +8 -0
- package/src/runtime/task-packet.ts +124 -0
- package/src/runtime/task-runner/live-executor.ts +1 -2
- package/src/runtime/task-runner/prompt-builder.ts +4 -1
- package/src/runtime/task-runner.ts +2 -2
- package/src/schema/config-schema.ts +1 -0
- package/src/state/event-log.ts +7 -0
- package/src/state/jsonl-writer.ts +24 -0
- package/src/state/locks.ts +66 -35
- package/src/state/run-metrics.ts +1 -2
- package/src/state/schedule.ts +13 -5
- package/src/state/state-store.ts +1 -1
- package/src/tools/safe-bash-extension.ts +1 -1
- package/src/tools/safe-bash.ts +10 -1
- package/src/ui/crew-widget.ts +2 -2
- package/src/ui/render-diff.ts +1 -1
- package/src/ui/run-dashboard.ts +1 -2
- package/src/ui/tool-render.ts +20 -3
- package/src/utils/conflict-detect.ts +0 -1
- package/src/utils/gh-protocol.ts +0 -2
- package/src/workflows/workflow-config.ts +3 -0
- 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
|
|
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
|
|
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 {
|
|
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);
|
|
@@ -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
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
|
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 {
|
|
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
|
|
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
|
|
23
|
-
import { withRunLock
|
|
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
|
-
|
|
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)
|
|
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
|
}
|
package/src/runtime/child-pi.ts
CHANGED
|
@@ -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
|
|
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:
|
|
409
|
-
|
|
410
|
-
//
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
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";
|
package/src/runtime/mcp-proxy.ts
CHANGED
|
@@ -16,8 +16,8 @@
|
|
|
16
16
|
* when proxying from the parent.
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
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 {
|
|
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
|
/**
|
package/src/runtime/sandbox.ts
CHANGED
|
@@ -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,
|