pi-crew 0.7.4 → 0.7.6
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 +79 -0
- package/README.md +11 -11
- package/docs/commands-reference.md +14 -10
- package/docs/troubleshooting.md +131 -0
- package/docs/usage.md +9 -4
- package/package.json +1 -1
- package/src/config/config.ts +11 -4
- package/src/config/types.ts +2 -0
- package/src/errors.ts +66 -0
- package/src/extension/action-suggestions.ts +71 -0
- package/src/extension/context-status-injection.ts +174 -0
- package/src/extension/knowledge-injection.ts +29 -1
- package/src/extension/register.ts +81 -65
- package/src/extension/team-tool/api.ts +3 -2
- package/src/extension/team-tool/cancel.ts +5 -4
- package/src/extension/team-tool/explain.ts +2 -1
- package/src/extension/team-tool/failure-patterns.ts +124 -0
- package/src/extension/team-tool/inspect.ts +10 -6
- package/src/extension/team-tool/lifecycle-actions.ts +5 -4
- package/src/extension/team-tool/respond.ts +4 -3
- package/src/extension/team-tool/run-not-found.ts +54 -0
- package/src/extension/team-tool/run.ts +26 -4
- package/src/extension/team-tool/status.ts +58 -4
- package/src/extension/team-tool.ts +5 -3
- package/src/runtime/async-runner.ts +7 -0
- package/src/runtime/background-runner.ts +7 -1
- package/src/runtime/chain-parser.ts +13 -5
- package/src/runtime/checkpoint.ts +13 -1
- package/src/runtime/child-pi.ts +9 -1
- package/src/runtime/live-session-runtime.ts +15 -1
- package/src/runtime/parent-guard.ts +2 -2
- package/src/runtime/pipeline-runner.ts +3 -1
- package/src/runtime/stale-reconciler.ts +28 -4
- package/src/runtime/task-runner.ts +50 -20
- package/src/runtime/team-runner.ts +19 -2
- package/src/runtime/verification-gates.ts +21 -1
- package/src/runtime/workspace-tree.ts +28 -2
- package/src/schema/team-tool-schema.ts +9 -0
- package/src/state/blob-store.ts +12 -10
- package/src/state/event-log-rotation.ts +114 -93
- package/src/state/event-log.ts +83 -23
- package/src/state/health-store.ts +6 -1
- package/src/state/locks.ts +66 -16
- package/src/state/state-store.ts +46 -2
- package/src/ui/card-colors.ts +7 -3
- package/src/ui/dashboard-panes/agents-pane.ts +15 -2
- package/src/ui/live-duration.ts +58 -0
- package/src/ui/tool-render.ts +7 -11
- package/src/ui/tool-renderers/index.ts +6 -3
- package/src/ui/widget/widget-formatters.ts +2 -13
- package/src/utils/fs-watch.ts +11 -60
- package/src/utils/run-watcher-registry.ts +164 -0
- package/src/workflows/discover-workflows.ts +2 -1
- package/src/workflows/workflow-config.ts +5 -0
- package/src/runtime/dynamic-script-runner.ts +0 -497
- package/src/runtime/sandbox.ts +0 -335
package/src/runtime/sandbox.ts
DELETED
|
@@ -1,335 +0,0 @@
|
|
|
1
|
-
import * as vm from "node:vm";
|
|
2
|
-
|
|
3
|
-
import { sanitizeEnvSecrets } from "../utils/env-filter.ts";
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Forbidden patterns for sandbox security (C4).
|
|
7
|
-
* These are checked during script compilation/validation.
|
|
8
|
-
*/
|
|
9
|
-
const FORBIDDEN_PATTERNS = [
|
|
10
|
-
// ESM patterns
|
|
11
|
-
/import\s*\(/, // Dynamic import()
|
|
12
|
-
/import\s+.*from\s+/, // Static import
|
|
13
|
-
/export\s+(default\s+)?/, // Export statements
|
|
14
|
-
/import\.meta/, // import.meta
|
|
15
|
-
// Module patterns
|
|
16
|
-
/require\s*\(/, // CommonJS require
|
|
17
|
-
/module\./, // module.exports, module.id, etc.
|
|
18
|
-
/__dirname/, // __dirname reference
|
|
19
|
-
/__filename/, // __filename reference
|
|
20
|
-
/\bdefine\s*\(/, // AMD define
|
|
21
|
-
// Global escape vectors
|
|
22
|
-
/\bglobalThis\b/, // globalThis reference
|
|
23
|
-
/\bglobal\b/, // global reference (Node.js)
|
|
24
|
-
// Block constructor chain escape vectors:
|
|
25
|
-
// - `.constructor` followed by `(`, `.`, `;`, or end-of-line/string
|
|
26
|
-
// - `[constructor]` or `["constructor"]` bracket access on any value
|
|
27
|
-
// The bare `constructor` keyword in a class body is safe and allowed.
|
|
28
|
-
/\.constructor\s*\(/, // Block obj.constructor() chain calls
|
|
29
|
-
/\.constructor\s*(?:\.|$|;|,|\s)/, // Block obj.constructor at end, semicolon, comma, or whitespace
|
|
30
|
-
/\[\s*['"]?constructor['"]?\s*\]/, // Block ["constructor"] or ['constructor'] or [constructor]
|
|
31
|
-
] as const;
|
|
32
|
-
|
|
33
|
-
Object.freeze(FORBIDDEN_PATTERNS);
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* SECURITY (HIGH #3 fix): Normalize source code before forbidden-pattern checks
|
|
37
|
-
* to prevent unicode-escape bypasses.
|
|
38
|
-
*
|
|
39
|
-
* Attackers can write `import\u0028"fs"\u0029` which compiles as
|
|
40
|
-
* `import("fs")` but does not match the regex `/import\s*\(/`.
|
|
41
|
-
*
|
|
42
|
-
* This function:
|
|
43
|
-
* 1. Strips null bytes (used to split keywords across boundaries)
|
|
44
|
-
* 2. Decodes \uXXXX escape sequences so regexes see the actual characters
|
|
45
|
-
*/
|
|
46
|
-
export function normalizeCodeForValidation(code: string): string {
|
|
47
|
-
// Strip null bytes
|
|
48
|
-
let normalized = code.replace(/\0/g, "");
|
|
49
|
-
// Decode common unicode escapes: \u0028 → (
|
|
50
|
-
normalized = normalized.replace(
|
|
51
|
-
/\\u([0-9a-fA-F]{4})/g,
|
|
52
|
-
(_, hex) => String.fromCharCode(Number.parseInt(hex, 16)),
|
|
53
|
-
);
|
|
54
|
-
return normalized;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
export interface SandboxOptions {
|
|
58
|
-
timeout?: number;
|
|
59
|
-
globals?: Record<string, unknown>;
|
|
60
|
-
onLog?: (message: string) => void;
|
|
61
|
-
onError?: (message: string) => void;
|
|
62
|
-
onWarn?: (message: string) => void;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* WorkflowSandbox provides a safe execution context for dynamic JavaScript
|
|
67
|
-
* in pi-crew workflows. It creates a VM context with restricted globals
|
|
68
|
-
* and provides safe console and process objects.
|
|
69
|
-
*/
|
|
70
|
-
export class WorkflowSandbox {
|
|
71
|
-
private context: vm.Context;
|
|
72
|
-
private timeout: number;
|
|
73
|
-
|
|
74
|
-
constructor(options: SandboxOptions = {}) {
|
|
75
|
-
this.timeout = options.timeout ?? 30000;
|
|
76
|
-
this.context = this.createSafeContext(options.globals ?? {}, options);
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
private createSafeContext(globals: Record<string, unknown>, options: SandboxOptions): vm.Context {
|
|
80
|
-
// C4: Frozen process object - limited access to process internals.
|
|
81
|
-
// FIX (Round 14, C1+C3): Sanitize env to a small allow-list so secrets
|
|
82
|
-
// like ANTHROPIC_API_KEY, AWS_SECRET_ACCESS_KEY, etc. never reach
|
|
83
|
-
// sandboxed code. Then deep-freeze the env so callers cannot inject
|
|
84
|
-
// new keys (Object.freeze on the wrapper alone would not prevent
|
|
85
|
-
// `frozenProcess.env.newKey = "..."`).
|
|
86
|
-
const safeEnv = Object.freeze(sanitizeEnvSecrets(process.env, {
|
|
87
|
-
allowList: [
|
|
88
|
-
"NODE_ENV",
|
|
89
|
-
// Note: PI_CREW_* globs are not used here because isDangerousGlob
|
|
90
|
-
// flags them as potentially matching secret env vars (PI_CREW_token,
|
|
91
|
-
// PI_CREW_api_key, etc.). Instead, list the specific PI_CREW env vars
|
|
92
|
-
// that sandboxed code legitimately needs.
|
|
93
|
-
"PI_CREW_DEPTH",
|
|
94
|
-
"PI_CREW_INHERIT_PROJECT_CONTEXT",
|
|
95
|
-
"PI_CREW_INHERIT_SKILLS",
|
|
96
|
-
"PI_CREW_MOCK_LIVE_SESSION",
|
|
97
|
-
"PI_CREW_SKIP_HOME_CHECK",
|
|
98
|
-
"PI_CREW_WARM_POOL_SIZE",
|
|
99
|
-
"PATH",
|
|
100
|
-
"PATH_SEPARATOR",
|
|
101
|
-
"USERPROFILE",
|
|
102
|
-
"USER",
|
|
103
|
-
"SHELL",
|
|
104
|
-
"LANG",
|
|
105
|
-
"LC_ALL",
|
|
106
|
-
"LC_CTYPE",
|
|
107
|
-
"TERM",
|
|
108
|
-
"TZ",
|
|
109
|
-
"TMPDIR",
|
|
110
|
-
"TMP",
|
|
111
|
-
"TEMP",
|
|
112
|
-
],
|
|
113
|
-
}));
|
|
114
|
-
const frozenProcess = Object.freeze({
|
|
115
|
-
cwd: () => process.cwd(),
|
|
116
|
-
platform: process.platform,
|
|
117
|
-
arch: process.arch,
|
|
118
|
-
version: process.version,
|
|
119
|
-
env: safeEnv,
|
|
120
|
-
// Explicitly excluded: exit, kill, hrtime, memoryUsage, cpuUsage, binding, dlopen, _tickCallback
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
// Safe console implementation
|
|
124
|
-
const safeConsole = {
|
|
125
|
-
log: (...args: unknown[]) => (options.onLog ?? console.log)(args.map(formatArg).join(" ")),
|
|
126
|
-
error: (...args: unknown[]) => (options.onError ?? console.error)(args.map(formatArg).join(" ")),
|
|
127
|
-
warn: (...args: unknown[]) => (options.onWarn ?? console.warn)(args.map(formatArg).join(" ")),
|
|
128
|
-
info: (...args: unknown[]) => (options.onLog ?? console.log)(args.map(formatArg).join(" ")),
|
|
129
|
-
debug: (...args: unknown[]) => (options.onLog ?? console.log)(args.map(formatArg).join(" ")),
|
|
130
|
-
table: (data: unknown) => (options.onLog ?? console.log)(JSON.stringify(data, null, 2)),
|
|
131
|
-
dir: (data: unknown) => (options.onLog ?? console.log)(JSON.stringify(data, null, 2)),
|
|
132
|
-
};
|
|
133
|
-
|
|
134
|
-
// C4: Ensure globals don't include process, global, or globalThis references
|
|
135
|
-
const safeGlobals: Record<string, unknown> = {};
|
|
136
|
-
for (const [key, value] of Object.entries(globals)) {
|
|
137
|
-
// Filter out dangerous global references
|
|
138
|
-
if (key === "process" || key === "global" || key === "globalThis" || key === "GLOBAL") {
|
|
139
|
-
continue; // Skip - these are handled by frozenProcess or intentionally omitted
|
|
140
|
-
}
|
|
141
|
-
safeGlobals[key] = value;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
// Context isolation - explicitly list allowed globals
|
|
145
|
-
const contextGlobals: Record<string, unknown> = {
|
|
146
|
-
...safeGlobals,
|
|
147
|
-
process: frozenProcess,
|
|
148
|
-
console: safeConsole,
|
|
149
|
-
// Safe Math (static methods only)
|
|
150
|
-
Math: Math,
|
|
151
|
-
// Safe JSON
|
|
152
|
-
JSON: JSON,
|
|
153
|
-
// Safe Number
|
|
154
|
-
Number: Number,
|
|
155
|
-
// Safe String
|
|
156
|
-
String: String,
|
|
157
|
-
// Safe Boolean
|
|
158
|
-
Boolean: Boolean,
|
|
159
|
-
// Safe Array
|
|
160
|
-
Array: Array,
|
|
161
|
-
// Safe Object
|
|
162
|
-
Object: Object,
|
|
163
|
-
// Safe RegExp
|
|
164
|
-
RegExp: RegExp,
|
|
165
|
-
// Safe Error
|
|
166
|
-
Error: Error,
|
|
167
|
-
// Safe Map
|
|
168
|
-
Map: Map,
|
|
169
|
-
// Safe Set
|
|
170
|
-
Set: Set,
|
|
171
|
-
// Safe Promise
|
|
172
|
-
Promise: Promise,
|
|
173
|
-
// Safe Symbol
|
|
174
|
-
Symbol: Symbol,
|
|
175
|
-
// Safe parseInt/parseFloat
|
|
176
|
-
parseInt: parseInt,
|
|
177
|
-
parseFloat: parseFloat,
|
|
178
|
-
isNaN: isNaN,
|
|
179
|
-
isFinite: isFinite,
|
|
180
|
-
// Safe encodeURI/decodeURI
|
|
181
|
-
encodeURI: encodeURI,
|
|
182
|
-
decodeURI: decodeURI,
|
|
183
|
-
encodeURIComponent: encodeURIComponent,
|
|
184
|
-
decodeURIComponent: decodeURIComponent,
|
|
185
|
-
// Safe typed arrays (read-only buffer views)
|
|
186
|
-
ArrayBuffer: ArrayBuffer,
|
|
187
|
-
Uint8Array: Uint8Array,
|
|
188
|
-
};
|
|
189
|
-
|
|
190
|
-
// Freeze the context object itself to prevent sandbox code from
|
|
191
|
-
// adding/removing globals.
|
|
192
|
-
Object.freeze(contextGlobals);
|
|
193
|
-
|
|
194
|
-
const ctx = vm.createContext(contextGlobals);
|
|
195
|
-
|
|
196
|
-
// Freeze prototypes INSIDE the VM context to prevent sandboxed code
|
|
197
|
-
// from polluting Object.prototype or Array.prototype.
|
|
198
|
-
//
|
|
199
|
-
// SECURITY TRADE-OFF: vm.createContext shares host prototypes, so
|
|
200
|
-
// freezing inside the context also freezes them for the host process.
|
|
201
|
-
// This is acceptable because:
|
|
202
|
-
// 1. Pi-crew extensions should not modify built-in prototypes
|
|
203
|
-
// 2. The freeze is idempotent (safe to call multiple times)
|
|
204
|
-
// 3. In test environments, we skip this to allow test frameworks
|
|
205
|
-
// that extend prototypes (e.g., Sinon, should.js)
|
|
206
|
-
if (process.env.NODE_ENV !== "test") {
|
|
207
|
-
try {
|
|
208
|
-
vm.runInContext(
|
|
209
|
-
"Object.freeze(Object.prototype); Object.freeze(Array.prototype);",
|
|
210
|
-
ctx,
|
|
211
|
-
{ filename: "sandbox-init.js", timeout: 1000 },
|
|
212
|
-
);
|
|
213
|
-
} catch {
|
|
214
|
-
// Already frozen — idempotent, safe to ignore
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
return ctx;
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
/**
|
|
222
|
-
* C4: Validate code before execution - check for forbidden patterns and
|
|
223
|
-
* ensure compilation is safe.
|
|
224
|
-
*/
|
|
225
|
-
private validateScript(code: string): void {
|
|
226
|
-
// SECURITY (HIGH #3 fix): Normalize unicode escapes before pattern matching
|
|
227
|
-
const normalized = normalizeCodeForValidation(code);
|
|
228
|
-
// Check for ESM/module patterns
|
|
229
|
-
for (const pattern of FORBIDDEN_PATTERNS) {
|
|
230
|
-
if (pattern.test(normalized)) {
|
|
231
|
-
throw new Error(`Forbidden pattern detected: ${pattern.source}`);
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
// Check for import.meta specifically (C4)
|
|
236
|
-
if (/import\.meta/.test(normalized)) {
|
|
237
|
-
throw new Error("import.meta is not allowed in sandboxed code");
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
// Verify compilation succeeds (C4)
|
|
241
|
-
const wrappedCode = `(function(){ ${code} })()`;
|
|
242
|
-
new vm.Script(wrappedCode, {
|
|
243
|
-
filename: "sandbox-validate.js",
|
|
244
|
-
});
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
/**
|
|
248
|
-
* Execute JavaScript code in the sandboxed context.
|
|
249
|
-
* @param code - The JavaScript code to execute
|
|
250
|
-
* @param timeout - Optional timeout override in milliseconds
|
|
251
|
-
* @returns The result of the script execution
|
|
252
|
-
* @throws Error if code contains forbidden patterns or fails compilation
|
|
253
|
-
*/
|
|
254
|
-
execute(code: string, timeout?: number): unknown {
|
|
255
|
-
// C4: Validate script before execution
|
|
256
|
-
this.validateScript(code);
|
|
257
|
-
|
|
258
|
-
const effectiveTimeout = timeout ?? this.timeout;
|
|
259
|
-
// Wrap code in an IIFE to allow return statements
|
|
260
|
-
const wrappedCode = `(function(){ ${code} })()`;
|
|
261
|
-
const script = new vm.Script(wrappedCode, {
|
|
262
|
-
filename: "workflow.js",
|
|
263
|
-
});
|
|
264
|
-
|
|
265
|
-
return script.runInContext(this.context, {
|
|
266
|
-
timeout: effectiveTimeout,
|
|
267
|
-
displayErrors: true,
|
|
268
|
-
});
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
/**
|
|
272
|
-
* Execute an async function in the sandboxed context.
|
|
273
|
-
* @param fn - Async function to execute
|
|
274
|
-
* @param timeout - Optional timeout override in milliseconds
|
|
275
|
-
* @returns Promise resolving to the function result
|
|
276
|
-
*/
|
|
277
|
-
async executeAsync<T>(fn: () => Promise<T>, timeout?: number): Promise<T> {
|
|
278
|
-
const effectiveTimeout = timeout ?? this.timeout;
|
|
279
|
-
// FIX (Round 14, C2): Run the same validation chain as `execute()` so
|
|
280
|
-
// forbidden patterns (require/import/__dirname/etc.) cannot slip through
|
|
281
|
-
// by hiding inside an arrow function. Previously the function body was
|
|
282
|
-
// stringified and executed with no checks.
|
|
283
|
-
const fnSource = fn.toString();
|
|
284
|
-
this.validateScript(fnSource);
|
|
285
|
-
const script = new vm.Script(`(${fnSource})()`, {
|
|
286
|
-
filename: "workflow.js",
|
|
287
|
-
});
|
|
288
|
-
|
|
289
|
-
const result = script.runInContext(this.context, {
|
|
290
|
-
timeout: effectiveTimeout,
|
|
291
|
-
displayErrors: true,
|
|
292
|
-
});
|
|
293
|
-
|
|
294
|
-
return result as Promise<T>;
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
/**
|
|
298
|
-
* Create a new sandbox with additional globals merged in.
|
|
299
|
-
*/
|
|
300
|
-
extend(additionalGlobals: Record<string, unknown>): WorkflowSandbox {
|
|
301
|
-
const newSandbox = new WorkflowSandbox({
|
|
302
|
-
timeout: this.timeout,
|
|
303
|
-
globals: { ...additionalGlobals },
|
|
304
|
-
});
|
|
305
|
-
return newSandbox;
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
/**
|
|
309
|
-
* Get the VM context for advanced use cases.
|
|
310
|
-
*/
|
|
311
|
-
getContext(): vm.Context {
|
|
312
|
-
return this.context;
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
function formatArg(arg: unknown): string {
|
|
317
|
-
if (typeof arg === "string") return arg;
|
|
318
|
-
if (arg === null) return "null";
|
|
319
|
-
if (arg === undefined) return "undefined";
|
|
320
|
-
if (typeof arg === "object") {
|
|
321
|
-
try {
|
|
322
|
-
return JSON.stringify(arg);
|
|
323
|
-
} catch {
|
|
324
|
-
return String(arg);
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
return String(arg);
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
/**
|
|
331
|
-
* Create a pre-configured sandbox for workflow execution.
|
|
332
|
-
*/
|
|
333
|
-
export function createWorkflowSandbox(options?: SandboxOptions): WorkflowSandbox {
|
|
334
|
-
return new WorkflowSandbox(options);
|
|
335
|
-
}
|