pi-crew 0.5.16 → 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 +54 -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 +1 -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/team-tool/orchestrate.ts +12 -4
- package/src/runtime/adaptive-plan.ts +18 -2
- package/src/runtime/child-pi.ts +17 -5
- package/src/runtime/dynamic-script-runner.ts +14 -1
- package/src/runtime/sandbox.ts +8 -0
- package/src/runtime/task-packet.ts +124 -0
- package/src/runtime/task-runner/prompt-builder.ts +4 -1
- package/src/runtime/task-runner.ts +1 -1
- package/src/schema/config-schema.ts +1 -0
- package/src/state/event-log.ts +7 -0
- package/src/tools/safe-bash-extension.ts +1 -1
- package/src/tools/safe-bash.ts +10 -0
- package/src/workflows/workflow-config.ts +3 -0
- package/src/worktree/worktree-manager.ts +75 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,59 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.5.17] — Security Hardening + ECC Patterns + Skill Review (2026-06-03)
|
|
4
|
+
|
|
5
|
+
### Highlights
|
|
6
|
+
- **3 CRITICAL security fixes**: path traversal, sandbox escape, executeUnchecked bypass
|
|
7
|
+
- **3 HIGH security fixes**: allowPatterns bypass, safe-bash fallback message, mock mode
|
|
8
|
+
- **3 MEDIUM security fixes**: home hooks visibility, API keys documentation, sync lock deprecation
|
|
9
|
+
- **2 new features** from ECC/dmux patterns: seedPaths overlay + structured handoff template
|
|
10
|
+
- **2 gap fills**: handoff parser + per-step seedPaths
|
|
11
|
+
- **36 skills reviewed**: origin fields, broken refs fixed, verify-skill.ts updated
|
|
12
|
+
- **1 bug fix**: adaptive-plan parser strips markdown code fences
|
|
13
|
+
- **1 regression fix**: mock mode NODE_ENV gate reverted
|
|
14
|
+
- **41 new tests** across 6 test files
|
|
15
|
+
|
|
16
|
+
### Security Fixes
|
|
17
|
+
|
|
18
|
+
#### CRITICAL
|
|
19
|
+
1. `orchestrate.ts`: Path traversal — planPath validated with `resolveContainedPath()`
|
|
20
|
+
2. `sandbox.ts`: Prototype pollution — `Object.freeze` on prototypes, `globalThis`/`global` in FORBIDDEN_PATTERNS
|
|
21
|
+
3. `dynamic-script-runner.ts`: `executeUnchecked` → private, `__test_executeUnchecked` test-only export
|
|
22
|
+
|
|
23
|
+
#### HIGH
|
|
24
|
+
4. `safe-bash.ts`: allowPatterns validation rejects `/.*/` and permissive catch-all patterns
|
|
25
|
+
5. `safe-bash-extension.ts`: Error message no longer suggests bypassing safe-bash
|
|
26
|
+
6. `child-pi.ts`: Mock mode requires `PI_CREW_ALLOW_MOCK=1` (set in parent process only)
|
|
27
|
+
|
|
28
|
+
#### MEDIUM
|
|
29
|
+
7. `worktree-manager.ts`: `logInternalError` warning when home directory hooks accepted
|
|
30
|
+
8. `child-pi.ts`: SECURITY WARNING JSDoc on API key allow-list trade-off
|
|
31
|
+
9. `event-log.ts`: Expanded deprecation notice on `withEventLogLockSync` blocking behavior
|
|
32
|
+
|
|
33
|
+
### Features (ECC/dmux patterns)
|
|
34
|
+
|
|
35
|
+
- **seedPaths**: Overlay local/uncommitted files into worktrees via config (`worktree.seedPaths`) or per-step (`WorkflowStep.seedPaths`). Path traversal validation, dedup, recursive copy.
|
|
36
|
+
- **Structured Handoff Template**: `HANDOFF_TEMPLATE` constant + `parseHandoffFromOutput()` parser. Agents receive handoff format instructions automatically.
|
|
37
|
+
|
|
38
|
+
### Skill Review
|
|
39
|
+
- All 36 skills: added `origin` YAML frontmatter field
|
|
40
|
+
- Fixed `widget-rendering` wrong file path
|
|
41
|
+
- Fixed `orchestration` + `detection-pipeline-design` broken cross-skill references
|
|
42
|
+
- Fixed 4 skills with wrong `source/pi-mono/` paths
|
|
43
|
+
- `verify-skill.ts` now validates `origin` field
|
|
44
|
+
|
|
45
|
+
### Bug Fixes
|
|
46
|
+
- `adaptive-plan.ts`: `stripCodeFence()` strips markdown code fences inside ADAPTIVE_PLAN markers — fixes planner output parsing for non-frontier models
|
|
47
|
+
- Mock mode regression: reverted NODE_ENV gate, uses PI_CREW_ALLOW_MOCK only (child processes don't inherit NODE_ENV)
|
|
48
|
+
|
|
49
|
+
### Stats
|
|
50
|
+
- Test suite: 2698 pass + 1 skip, 0 fail (was 2657 in v0.5.16; +41 net)
|
|
51
|
+
- TypeScript: 0 errors
|
|
52
|
+
- New test files: 6 (worktree-seed-paths, task-handoff-template, task-handoff-parser, adaptive-plan +3 safe-bash tests)
|
|
53
|
+
- Files touched: 50+
|
|
54
|
+
- Security issues fixed: 9 (3 CRITICAL + 3 HIGH + 3 MEDIUM)
|
|
55
|
+
- False positives verified: 2
|
|
56
|
+
|
|
3
57
|
## [0.5.16] — Rounds 22–31 Audit Fixes (2026-06-02)
|
|
4
58
|
|
|
5
59
|
### Highlights
|
package/package.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: async-worker-recovery
|
|
3
3
|
description: Background worker, heartbeat, stale-run, crash-recovery, and deadletter workflow. Use when debugging stuck/dead workers or changing async run reliability.
|
|
4
|
+
origin: pi-crew
|
|
4
5
|
triggers:
|
|
5
6
|
- "worker crashed"
|
|
6
7
|
- "stale run"
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: detection-pipeline-design
|
|
3
3
|
description: "Design data pipelines for security monitoring and threat intelligence."
|
|
4
|
+
origin: distilled:anthropic-cybersecurity-skills
|
|
4
5
|
triggers:
|
|
5
6
|
- "build pipeline"
|
|
6
7
|
- "design detection"
|
|
@@ -282,4 +283,4 @@ npx tsc --noEmit
|
|
|
282
283
|
node --experimental-strip-types --test test/unit/detection-pipeline.test.ts
|
|
283
284
|
```
|
|
284
285
|
|
|
285
|
-
*See also: `
|
|
286
|
+
*See also: `security-review` skill for detection rule patterns and signature authoring guidance.*
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: iterative-audit
|
|
3
3
|
description: "Iterative multi-round codebase audit with diminishing-returns detection. Run 5-20+ rounds, each focusing on one specific area. Built from 19 rounds of dogfooding pi-crew on itself."
|
|
4
|
+
origin: pi-crew
|
|
4
5
|
triggers:
|
|
5
6
|
- "audit this codebase"
|
|
6
7
|
- "review everything"
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: model-routing-context
|
|
3
3
|
description: Model routing, parent context, thinking level, and prompt construction workflow. Use when changing model fallback, child Pi args, inherited context, task prompts, or compact-read behavior.
|
|
4
|
+
origin: pi-crew
|
|
4
5
|
triggers:
|
|
5
6
|
- "change model"
|
|
6
7
|
- "parent context"
|
|
@@ -15,7 +16,7 @@ Use this skill when working on model/context propagation.
|
|
|
15
16
|
|
|
16
17
|
## Source patterns distilled
|
|
17
18
|
|
|
18
|
-
- Pi session context/model state: `source/pi
|
|
19
|
+
- Pi session context/model state: `source/pi/packages/coding-agent/src/core/session-manager.ts`, `agent-session.ts`, compaction modules
|
|
19
20
|
- pi-crew model and prompt code: `src/runtime/model-fallback.ts`, `src/runtime/pi-args.ts`, `src/runtime/task-runner/prompt-builder.ts`, `src/runtime/task-output-context.ts`, `src/extension/team-tool/context.ts`
|
|
20
21
|
|
|
21
22
|
## Rules
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: orchestration
|
|
3
3
|
description: "Multi-phase orchestration for planners and executors."
|
|
4
|
+
origin: pi-crew
|
|
4
5
|
triggers:
|
|
5
6
|
- "orchestrate this"
|
|
6
7
|
- "coordinate tasks"
|
|
@@ -73,7 +74,7 @@ Maintain the original scope exactly. Không mở rộng scope vì "thấy thêm
|
|
|
73
74
|
### Step 3 — Dispatch phase
|
|
74
75
|
|
|
75
76
|
- Launch all parallel subagents in one `team` call.
|
|
76
|
-
- Each subagent receives a complete task packet (see `task-packet` skill).
|
|
77
|
+
- Each subagent receives a complete task packet (see `requirements-to-task-packet` skill).
|
|
77
78
|
- Set explicit file ownership per worker — no two workers touch the same file.
|
|
78
79
|
- Use `workspaceMode: 'worktree'` when parallel edits risk conflict.
|
|
79
80
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: pi-extension-lifecycle
|
|
3
3
|
description: Pi extension lifecycle and registration patterns.
|
|
4
|
+
origin: pi-crew
|
|
4
5
|
triggers:
|
|
5
6
|
- "add extension"
|
|
6
7
|
- "register tools"
|
|
@@ -15,8 +16,8 @@ Use this skill when working on Pi extension registration or lifecycle behavior.
|
|
|
15
16
|
|
|
16
17
|
## Source patterns distilled
|
|
17
18
|
|
|
18
|
-
- Pi core: `source/pi
|
|
19
|
-
- Pi examples: `source/pi
|
|
19
|
+
- Pi core: `source/pi/packages/coding-agent/src/core/extensions/types.ts`, `loader.ts`, `runner.ts`
|
|
20
|
+
- Pi examples: `source/pi/packages/coding-agent/examples/extensions/`
|
|
20
21
|
- pi-crew extension entry: `src/extension/register.ts`, `src/extension/registration/*.ts`
|
|
21
22
|
|
|
22
23
|
## Rules
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: resource-discovery-config
|
|
3
3
|
description: "pi-crew resource and configuration discovery workflow."
|
|
4
|
+
origin: pi-crew
|
|
4
5
|
triggers:
|
|
5
6
|
- "discover agents"
|
|
6
7
|
- "find teams"
|
|
@@ -15,7 +16,7 @@ Use this skill for pi-crew resource/config work.
|
|
|
15
16
|
|
|
16
17
|
## Source patterns distilled
|
|
17
18
|
|
|
18
|
-
- Pi resource loader: `source/pi
|
|
19
|
+
- Pi resource loader: `source/pi/packages/coding-agent/src/core/resource-loader.ts`, extension `resources_discover` hook
|
|
19
20
|
- pi-crew discovery: `src/agents/discover-agents.ts`, `src/teams/discover-teams.ts`, `src/workflows/discover-workflows.ts`
|
|
20
21
|
- Config: `src/config/config.ts`, `src/schema/config-schema.ts`, `schema.json`, `docs/resource-formats.md`
|
|
21
22
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: secure-agent-orchestration-review
|
|
3
3
|
description: "Use when reviewing delegation, skill loading, tool access, worker prompts, artifacts, runtime config, state, ownership, or subprocess execution."
|
|
4
|
+
origin: pi-crew
|
|
4
5
|
triggers:
|
|
5
6
|
- "review delegation"
|
|
6
7
|
- "check skill security"
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: ui-render-performance
|
|
3
3
|
description: "Non-blocking Pi TUI render workflow."
|
|
4
|
+
origin: pi-crew
|
|
4
5
|
triggers:
|
|
5
6
|
- "widget render"
|
|
6
7
|
- "dashboard pane"
|
|
@@ -14,7 +15,7 @@ Use this skill for Pi/pi-crew TUI work.
|
|
|
14
15
|
|
|
15
16
|
## Source patterns distilled
|
|
16
17
|
|
|
17
|
-
- Pi TUI is synchronous immediate-mode/string rendering: `source/pi
|
|
18
|
+
- Pi TUI is synchronous immediate-mode/string rendering: `source/pi/packages/coding-agent/src/modes/interactive/interactive-mode.ts`
|
|
18
19
|
- Pi extension examples use event-driven state updates, not render-time loading.
|
|
19
20
|
- pi-crew UI: `src/extension/register.ts`, `src/ui/run-dashboard.ts`, `src/ui/run-snapshot-cache.ts`, `src/ui/crew-widget.ts`, `src/ui/powerbar-publisher.ts`, `src/ui/render-scheduler.ts`
|
|
20
21
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: widget-rendering
|
|
3
3
|
description: "Pi TUI crew widget data sources, display priority, and rendering performance."
|
|
4
|
+
origin: pi-crew
|
|
4
5
|
triggers:
|
|
5
6
|
- "empty agent"
|
|
6
7
|
- "ghost run"
|
|
@@ -259,7 +260,7 @@ If ANY answer is NO → Stop. Fix widget rendering issues before proceeding.
|
|
|
259
260
|
- `src/runtime/crew-agent-records.ts` — readCrewAgents, agents.json
|
|
260
261
|
- `src/runtime/process-status.ts` — hasStaleAsyncProcess, isDisplayActiveRun
|
|
261
262
|
- `src/runtime/background-runner.ts` — active run filtering with async PID check
|
|
262
|
-
- `src/
|
|
263
|
+
- `src/state/active-run-registry.ts` — purgeStaleActiveRunIndex
|
|
263
264
|
|
|
264
265
|
---
|
|
265
266
|
|
package/src/config/types.ts
CHANGED
|
@@ -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(
|
|
@@ -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
|
@@ -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);
|
|
@@ -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
|
+
}
|
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,
|
|
@@ -128,6 +128,130 @@ export function validateTaskPacket(packet: TaskPacket): TaskPacketValidationResu
|
|
|
128
128
|
return { valid: errors.length === 0, errors };
|
|
129
129
|
}
|
|
130
130
|
|
|
131
|
+
/**
|
|
132
|
+
* Structured handoff template for task completion reports.
|
|
133
|
+
* Distilled from ECC dmux-workflows pattern — workers use this format
|
|
134
|
+
* so verifiers and downstream consumers can parse output predictably.
|
|
135
|
+
*/
|
|
136
|
+
export const HANDOFF_TEMPLATE = [
|
|
137
|
+
"## Handoff",
|
|
138
|
+
"",
|
|
139
|
+
"### Summary",
|
|
140
|
+
"<!-- 2-3 sentences describing what was done -->",
|
|
141
|
+
"",
|
|
142
|
+
"### Files Changed",
|
|
143
|
+
"<!-- List each file changed with brief description -->",
|
|
144
|
+
"<!-- - path/to/file.ts: description -->",
|
|
145
|
+
"",
|
|
146
|
+
"### Tests / Verification",
|
|
147
|
+
"<!-- What tests pass? What was manually verified? -->",
|
|
148
|
+
"",
|
|
149
|
+
"### Follow-ups",
|
|
150
|
+
"<!-- Any remaining issues or next steps -->",
|
|
151
|
+
].join("\n");
|
|
152
|
+
|
|
153
|
+
export interface ParsedHandoff {
|
|
154
|
+
summary: string[];
|
|
155
|
+
filesChanged: string[];
|
|
156
|
+
tests: string[];
|
|
157
|
+
followups: string[];
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Extract text between a ### heading and the next ### heading or end of text.
|
|
162
|
+
*/
|
|
163
|
+
function extractSection(content: string, heading: string): string {
|
|
164
|
+
const lines = content.split("\n");
|
|
165
|
+
const headingMarker = `### ${heading}`;
|
|
166
|
+
const startIndex = lines.findIndex((line) => line.trim() === headingMarker);
|
|
167
|
+
if (startIndex === -1) return "";
|
|
168
|
+
|
|
169
|
+
const collected: string[] = [];
|
|
170
|
+
for (let i = startIndex + 1; i < lines.length; i++) {
|
|
171
|
+
const trimmed = lines[i].trim();
|
|
172
|
+
if (trimmed.startsWith("### ") || trimmed.startsWith("## ")) break;
|
|
173
|
+
// Stop at paragraph text (non-bullet, non-comment, non-empty) that follows
|
|
174
|
+
// a blank line — signals end of subsection content.
|
|
175
|
+
if (
|
|
176
|
+
trimmed.length > 0 &&
|
|
177
|
+
!trimmed.startsWith("- ") &&
|
|
178
|
+
!trimmed.startsWith("<!--") &&
|
|
179
|
+
i > startIndex + 1 &&
|
|
180
|
+
lines[i - 1].trim() === "" &&
|
|
181
|
+
collected.some((l) => l.trim().length > 0)
|
|
182
|
+
) {
|
|
183
|
+
break;
|
|
184
|
+
}
|
|
185
|
+
collected.push(lines[i]);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return collected.join("\n").trim();
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Parse bullet list items from a section, stripping leading "- " and backtick wrapping.
|
|
193
|
+
*/
|
|
194
|
+
function parseBullets(section: string): string[] {
|
|
195
|
+
if (!section) return [];
|
|
196
|
+
return section
|
|
197
|
+
.split("\n")
|
|
198
|
+
.map((line) => line.trim())
|
|
199
|
+
.filter((line) => line.startsWith("- "))
|
|
200
|
+
.map((line) => {
|
|
201
|
+
let item = line.replace(/^- /, "").trim();
|
|
202
|
+
// Strip surrounding backticks
|
|
203
|
+
if (item.startsWith("`") && item.endsWith("`") && item.length >= 2) {
|
|
204
|
+
item = item.slice(1, -1);
|
|
205
|
+
}
|
|
206
|
+
return item;
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Parse a handoff section that may contain bullets AND free-text paragraphs.
|
|
212
|
+
* Returns all non-empty lines as individual items (bullets get their marker stripped).
|
|
213
|
+
*/
|
|
214
|
+
function parseMixedContent(section: string): string[] {
|
|
215
|
+
if (!section) return [];
|
|
216
|
+
return section
|
|
217
|
+
.split("\n")
|
|
218
|
+
.map((line) => line.trim())
|
|
219
|
+
.filter((line) => line.length > 0 && !line.startsWith("<!--")) // skip HTML comments
|
|
220
|
+
.map((line) => {
|
|
221
|
+
if (line.startsWith("- ")) return line.slice(2).trim();
|
|
222
|
+
return line;
|
|
223
|
+
})
|
|
224
|
+
.map((item) => {
|
|
225
|
+
// Strip surrounding backticks
|
|
226
|
+
if (item.startsWith("`") && item.endsWith("`") && item.length >= 2) {
|
|
227
|
+
return item.slice(1, -1);
|
|
228
|
+
}
|
|
229
|
+
return item;
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Parse structured handoff data from agent output text.
|
|
235
|
+
* Looks for the "## Handoff" heading and extracts subsections.
|
|
236
|
+
* Returns empty arrays for sections not found.
|
|
237
|
+
*/
|
|
238
|
+
export function parseHandoffFromOutput(output: string): ParsedHandoff {
|
|
239
|
+
if (!output || typeof output !== "string") {
|
|
240
|
+
return { summary: [], filesChanged: [], tests: [], followups: [] };
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Find the handoff section — look for ## Handoff
|
|
244
|
+
const handoffIndex = output.indexOf("## Handoff");
|
|
245
|
+
const content = handoffIndex >= 0 ? output.slice(handoffIndex) : output;
|
|
246
|
+
|
|
247
|
+
return {
|
|
248
|
+
summary: parseMixedContent(extractSection(content, "Summary")),
|
|
249
|
+
filesChanged: parseMixedContent(extractSection(content, "Files Changed")),
|
|
250
|
+
tests: parseMixedContent(extractSection(content, "Tests / Verification")),
|
|
251
|
+
followups: parseMixedContent(extractSection(content, "Follow-ups")),
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
|
|
131
255
|
export function renderTaskPacket(packet: TaskPacket): string {
|
|
132
256
|
return [
|
|
133
257
|
"# Task Packet",
|
|
@@ -3,7 +3,7 @@ import type { TeamRunManifest, TeamTaskState, TaskOutputSchema } from "../../sta
|
|
|
3
3
|
import type { WorkflowStep } from "../../workflows/workflow-config.ts";
|
|
4
4
|
import { buildMemoryBlock } from "../agent-memory.ts";
|
|
5
5
|
import { permissionForRole } from "../role-permission.ts";
|
|
6
|
-
import { renderTaskPacket } from "../task-packet.ts";
|
|
6
|
+
import { renderTaskPacket, HANDOFF_TEMPLATE } from "../task-packet.ts";
|
|
7
7
|
import { buildWorkspaceTree } from "../workspace-tree.ts";
|
|
8
8
|
|
|
9
9
|
/**
|
|
@@ -132,6 +132,9 @@ export async function renderTaskPrompt(manifest: TeamRunManifest, step: Workflow
|
|
|
132
132
|
task.taskPacket?.outputSchema ? renderOutputSchemaBlock(task.taskPacket.outputSchema) : "",
|
|
133
133
|
"Task:",
|
|
134
134
|
step.task.replaceAll("{goal}", manifest.goal),
|
|
135
|
+
"",
|
|
136
|
+
"When your task is complete, structure your final output using this handoff template:",
|
|
137
|
+
HANDOFF_TEMPLATE,
|
|
135
138
|
].join("\n");
|
|
136
139
|
|
|
137
140
|
const full = [stablePrefix, "", dynamicSuffix].join("\n");
|
|
@@ -156,7 +156,7 @@ export async function runTeamTask(
|
|
|
156
156
|
let streamBridge: ReturnType<typeof registerStreamBridge> | undefined;
|
|
157
157
|
try {
|
|
158
158
|
streamBridge = registerStreamBridge(manifest.runId);
|
|
159
|
-
const workspace = prepareTaskWorkspace(manifest, input.task);
|
|
159
|
+
const workspace = prepareTaskWorkspace(manifest, input.task, input.step.seedPaths);
|
|
160
160
|
const worktree =
|
|
161
161
|
workspace.worktreePath && workspace.branch
|
|
162
162
|
? {
|
|
@@ -56,6 +56,7 @@ export const PiTeamsWorktreeConfigSchema = Type.Object({
|
|
|
56
56
|
setupHook: Type.Optional(Type.String({ minLength: 1 })),
|
|
57
57
|
setupHookTimeoutMs: Type.Optional(Type.Integer({ minimum: 1 })),
|
|
58
58
|
linkNodeModules: Type.Optional(Type.Boolean()),
|
|
59
|
+
seedPaths: Type.Optional(Type.Array(Type.String({ minLength: 1 }))),
|
|
59
60
|
}, { additionalProperties: false });
|
|
60
61
|
|
|
61
62
|
export const AgentOverrideSchema = Type.Object({
|
package/src/state/event-log.ts
CHANGED
|
@@ -66,6 +66,13 @@ let appendCounter = 0;
|
|
|
66
66
|
*
|
|
67
67
|
* @deprecated Prefer `appendEventAsync()` for callers in async contexts. The sync lock
|
|
68
68
|
* uses `sleepSync` which blocks the event loop and prevents AbortSignal handlers from firing.
|
|
69
|
+
*
|
|
70
|
+
* SECURITY WARNING: This function uses `sleepSync` in its lock-acquire retry loop, which
|
|
71
|
+
* blocks the Node.js event loop for up to 120s. During that time, AbortSignal handlers
|
|
72
|
+
* cannot fire, SIGTERM handlers are delayed, and the process appears unresponsive to
|
|
73
|
+
* orchestrator health checks. Known callers include `appendEvent` (sync path),
|
|
74
|
+
* `flushOneEventLogBuffer`, and `state/mailbox.ts`. Prefer the async alternative
|
|
75
|
+
* (`appendEventAsync`) for all new code.
|
|
69
76
|
*/
|
|
70
77
|
export function withEventLogLockSync<T>(eventsPath: string, fn: () => T): T {
|
|
71
78
|
// Ensure parent directory exists before attempting lock
|
|
@@ -84,7 +84,7 @@ export default function safeBashExtension(pi: ExtensionAPI): void {
|
|
|
84
84
|
content: [
|
|
85
85
|
{
|
|
86
86
|
type: "text" as const,
|
|
87
|
-
text: `🚫 ${danger}\n\
|
|
87
|
+
text: `🚫 ${danger}\n\nCommand blocked by safety policy. If this is a false positive, ask the user for confirmation or use force: true with explicit user approval.`,
|
|
88
88
|
},
|
|
89
89
|
],
|
|
90
90
|
};
|
package/src/tools/safe-bash.ts
CHANGED
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
* Uses linear-time scanning to prevent ReDoS attacks
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
import { logInternalError } from "../utils/internal-error.ts";
|
|
8
|
+
|
|
7
9
|
|
|
8
10
|
// Backward-compatible pattern array (kept for getPatterns API)
|
|
9
11
|
// IMPORTANT: Line 8 (rm pattern with nested quantifiers) has been replaced
|
|
@@ -162,6 +164,14 @@ export function isDangerous(command: string, options: SafeBashOptions = {}): str
|
|
|
162
164
|
|
|
163
165
|
if (!enabled) return null;
|
|
164
166
|
|
|
167
|
+
// Reject overly permissive allowPatterns that would bypass all safety
|
|
168
|
+
for (const pattern of allowPatterns) {
|
|
169
|
+
if (pattern.source === ".*" || (pattern.test("") && pattern.test("rm -rf /"))) {
|
|
170
|
+
logInternalError("safe-bash.permissive-allow-pattern", new Error(`allowPattern rejects nothing: ${pattern}`));
|
|
171
|
+
throw new Error(`Overly permissive allowPattern rejected: ${pattern}. Use specific patterns only.`);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
165
175
|
// Normalize: remove line continuations, collapse whitespace
|
|
166
176
|
const normalized = command.replace(/\\\n/g, " ").replace(/\s+/g, " ").trim();
|
|
167
177
|
|
|
@@ -14,6 +14,9 @@ export interface WorkflowStep {
|
|
|
14
14
|
progress?: boolean;
|
|
15
15
|
worktree?: boolean;
|
|
16
16
|
verify?: boolean;
|
|
17
|
+
/** Per-step files to overlay into the worktree (in addition to global worktree.seedPaths).
|
|
18
|
+
* Useful when only certain steps need access to local drafts or scripts. */
|
|
19
|
+
seedPaths?: string[];
|
|
17
20
|
}
|
|
18
21
|
|
|
19
22
|
export interface WorkflowConfig {
|
|
@@ -116,6 +116,11 @@ function runSetupHook(manifest: TeamRunManifest, task: TeamTaskState, repoRoot:
|
|
|
116
116
|
logInternalError("worktree.setupHook.rejected", new Error("hook path not allowed: " + rawHookPath), `cwd=${manifest.cwd}`);
|
|
117
117
|
return [];
|
|
118
118
|
}
|
|
119
|
+
// SECURITY WARNING: Home directory hooks (~/.pi/hooks/) are user-writable and not project-scoped.
|
|
120
|
+
// A rogue npm postinstall script could place malicious hooks there. Log for visibility.
|
|
121
|
+
if (path.isAbsolute(rawHookPath)) {
|
|
122
|
+
logInternalError("worktree.setupHook.homeHook", new Error("Home directory hook used — ensure ~/.pi/hooks/ is trusted"), `hookPath=${rawHookPath}`);
|
|
123
|
+
}
|
|
119
124
|
const hookPath = path.isAbsolute(rawHookPath) ? rawHookPath : path.resolve(repoRoot, rawHookPath);
|
|
120
125
|
// SECURITY: Verify the resolved hook path is contained within the real repoRoot.
|
|
121
126
|
// This prevents symlink-based escape where repoRoot is a symlink.
|
|
@@ -201,7 +206,64 @@ function pruneStaleWorktrees(repoRoot: string): void {
|
|
|
201
206
|
catch { /* best-effort */ }
|
|
202
207
|
}
|
|
203
208
|
|
|
204
|
-
|
|
209
|
+
/**
|
|
210
|
+
* Normalize and validate seed paths — ensure all paths stay within repoRoot.
|
|
211
|
+
* Rejects path traversal (../) and absolute paths.
|
|
212
|
+
*/
|
|
213
|
+
export function normalizeSeedPaths(seedPaths: string[], repoRoot: string): string[] {
|
|
214
|
+
const resolvedRepoRoot = path.resolve(repoRoot);
|
|
215
|
+
const entries = Array.isArray(seedPaths) ? seedPaths : [];
|
|
216
|
+
const seen = new Set<string>();
|
|
217
|
+
const normalized: string[] = [];
|
|
218
|
+
|
|
219
|
+
for (const entry of entries) {
|
|
220
|
+
if (typeof entry !== "string" || entry.trim().length === 0) continue;
|
|
221
|
+
|
|
222
|
+
const absolutePath = path.resolve(resolvedRepoRoot, entry);
|
|
223
|
+
const relativePath = path.relative(resolvedRepoRoot, absolutePath);
|
|
224
|
+
|
|
225
|
+
if (relativePath.startsWith("..") || path.isAbsolute(relativePath)) {
|
|
226
|
+
throw new Error(`seedPaths entries must stay inside repoRoot: ${entry}`);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const normalizedPath = relativePath.split(path.sep).join("/");
|
|
230
|
+
if (seen.has(normalizedPath)) continue;
|
|
231
|
+
seen.add(normalizedPath);
|
|
232
|
+
normalized.push(normalizedPath);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return normalized;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Overlay seed paths from repoRoot into worktreePath.
|
|
240
|
+
* Copies files and directories, creating parent dirs as needed.
|
|
241
|
+
* Skips non-existent sources with logInternalError (non-fatal).
|
|
242
|
+
*/
|
|
243
|
+
export function overlaySeedPaths(repoRoot: string, worktreePath: string, seedPaths: string[]): void {
|
|
244
|
+
const normalized = normalizeSeedPaths(seedPaths, repoRoot);
|
|
245
|
+
|
|
246
|
+
for (const seedPath of normalized) {
|
|
247
|
+
const sourcePath = path.join(repoRoot, seedPath);
|
|
248
|
+
const destinationPath = path.join(worktreePath, seedPath);
|
|
249
|
+
|
|
250
|
+
if (!fs.existsSync(sourcePath)) {
|
|
251
|
+
logInternalError("worktree.seedPaths.missing", new Error(`Seed path does not exist: ${seedPath}`));
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
fs.mkdirSync(path.dirname(destinationPath), { recursive: true });
|
|
256
|
+
fs.rmSync(destinationPath, { force: true, recursive: true });
|
|
257
|
+
fs.cpSync(sourcePath, destinationPath, {
|
|
258
|
+
dereference: false,
|
|
259
|
+
force: true,
|
|
260
|
+
preserveTimestamps: true,
|
|
261
|
+
recursive: true,
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
export function prepareTaskWorkspace(manifest: TeamRunManifest, task: TeamTaskState, stepSeedPaths?: string[]): PreparedTaskWorkspace {
|
|
205
267
|
if (manifest.workspaceMode !== "worktree") return { cwd: task.cwd };
|
|
206
268
|
const repoRoot = findGitRoot(manifest.cwd);
|
|
207
269
|
const loadedConfig = loadConfig(manifest.cwd);
|
|
@@ -220,6 +282,12 @@ export function prepareTaskWorkspace(manifest: TeamRunManifest, task: TeamTaskSt
|
|
|
220
282
|
if (currentBranch !== branch) {
|
|
221
283
|
throw new Error(`Existing worktree branch mismatch at ${worktreePath}: expected '${branch}', got '${currentBranch}'.`);
|
|
222
284
|
}
|
|
285
|
+
// Overlay seed paths from config + step-level seedPaths (reused worktree)
|
|
286
|
+
const globalSeedPaths = loadedConfig.config.worktree?.seedPaths ?? [];
|
|
287
|
+
const mergedReused = normalizeSeedPaths([...globalSeedPaths, ...(stepSeedPaths ?? [])], repoRoot);
|
|
288
|
+
if (mergedReused.length > 0) {
|
|
289
|
+
overlaySeedPaths(repoRoot, worktreePath, mergedReused);
|
|
290
|
+
}
|
|
223
291
|
return { cwd: worktreePath, worktreePath, branch, reused: true };
|
|
224
292
|
}
|
|
225
293
|
pruneStaleWorktrees(repoRoot);
|
|
@@ -242,6 +310,12 @@ export function prepareTaskWorkspace(manifest: TeamRunManifest, task: TeamTaskSt
|
|
|
242
310
|
}
|
|
243
311
|
const syntheticPaths = runSetupHook(manifest, task, repoRoot, worktreePath, branch);
|
|
244
312
|
const nodeModulesLinked = loadedConfig.config.worktree?.linkNodeModules === true ? linkNodeModulesIfPresent(repoRoot, worktreePath) : false;
|
|
313
|
+
// Overlay seed paths from config + step-level seedPaths
|
|
314
|
+
const globalSeedPaths = loadedConfig.config.worktree?.seedPaths ?? [];
|
|
315
|
+
const merged = normalizeSeedPaths([...globalSeedPaths, ...(stepSeedPaths ?? [])], repoRoot);
|
|
316
|
+
if (merged.length > 0) {
|
|
317
|
+
overlaySeedPaths(repoRoot, worktreePath, merged);
|
|
318
|
+
}
|
|
245
319
|
return { cwd: worktreePath, worktreePath, branch, reused: false, nodeModulesLinked, syntheticPaths };
|
|
246
320
|
}
|
|
247
321
|
|