pi-crew 0.5.20 → 0.5.22
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 +43 -0
- package/package.json +1 -1
- package/src/config/config.ts +7 -1
- package/src/config/defaults.ts +11 -1
- package/src/extension/register.ts +2 -0
- package/src/extension/team-tool.ts +5 -0
- package/src/hooks/registry.ts +3 -0
- package/src/runtime/settings-store.ts +4 -0
- package/src/runtime/task-output-context.ts +11 -1
- package/src/state/event-log.ts +7 -1
- package/src/state/mailbox.ts +12 -0
- package/src/tools/safe-bash.ts +7 -6
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,48 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.5.22] — Remaining Issues from Ultimate Sweep (2026-06-03)
|
|
4
|
+
|
|
5
|
+
### Highlights
|
|
6
|
+
- `DEFAULT_CHILD_PI` frozen with `Readonly<>` type (prevents mutation)
|
|
7
|
+
- `parseWithSchema` logs validation failures with context
|
|
8
|
+
- Global registry cleanup (`uninstallCrewGlobalRegistry`)
|
|
9
|
+
- Mailbox sender auth and cross-workspace hooks documented
|
|
10
|
+
|
|
11
|
+
### Fixes
|
|
12
|
+
- `defaults.ts`: `DEFAULT_CHILD_PI` wrapped in `Readonly<{...}>` to prevent mutation via module injection
|
|
13
|
+
- `config.ts`: `parseWithSchema` logs validation failures when context provided
|
|
14
|
+
- `team-tool.ts`: Added `uninstallCrewGlobalRegistry()` paired with install
|
|
15
|
+
- `register.ts`: Calls `uninstallCrewGlobalRegistry()` in `cleanupRuntime()`
|
|
16
|
+
- `mailbox.ts`: Security documentation for sender authentication
|
|
17
|
+
- `hooks/registry.ts`: Security documentation for cross-workspace hook behavior
|
|
18
|
+
|
|
19
|
+
### Stats
|
|
20
|
+
- Test suite: 2703 pass + 1 skip, 0 fail
|
|
21
|
+
- TypeScript: 0 errors
|
|
22
|
+
|
|
23
|
+
## [0.5.21] — Ultimate Final Sweep: HIGH Security + Correctness Fixes (2026-06-03)
|
|
24
|
+
|
|
25
|
+
### Highlights
|
|
26
|
+
- **safe-bash line-continuation bypass fixed** — `$\n(evil)` now blocked
|
|
27
|
+
- **scheduledJobs dead code fixed** — settings sanitizer now passes through scheduled jobs
|
|
28
|
+
- **Memory-bounded file reads** — `readIfSmall` uses `fs.readSync` with buffer instead of full file read
|
|
29
|
+
- **Event log corruption detection** — `scanSequence` logs warnings for corrupt JSON lines
|
|
30
|
+
|
|
31
|
+
### Security
|
|
32
|
+
- `safe-bash.ts`: All structural checks now use `normalized` string (stripped line continuations)
|
|
33
|
+
- `\$\s*\(` regex catches `$<newline>(evil)` → `$(evil)` bypass that bash interprets as command substitution
|
|
34
|
+
- Added 2 regression tests for line-continuation bypass
|
|
35
|
+
|
|
36
|
+
### Fixes
|
|
37
|
+
- `settings-store.ts`: `sanitizeSettings()` now copies `scheduledJobs` as opaque array
|
|
38
|
+
- `task-output-context.ts`: `readIfSmall` uses `Buffer.alloc` + `fs.readSync` instead of `readFileSync` + `slice`
|
|
39
|
+
- `event-log.ts`: `scanSequence` counts and logs corrupt JSON lines via `logInternalError`
|
|
40
|
+
|
|
41
|
+
### Stats
|
|
42
|
+
- Test suite: 2703 pass + 1 skip, 0 fail
|
|
43
|
+
- TypeScript: 0 errors
|
|
44
|
+
- Total issues fixed across 37 rounds: ~155+
|
|
45
|
+
|
|
3
46
|
## [0.5.20] — Verification Sweep: 7 Fixes (2026-06-03)
|
|
4
47
|
|
|
5
48
|
### Highlights
|
package/package.json
CHANGED
package/src/config/config.ts
CHANGED
|
@@ -520,8 +520,14 @@ function asRecord(value: unknown): Record<string, unknown> | undefined {
|
|
|
520
520
|
function parseWithSchema<T extends TSchema>(
|
|
521
521
|
schema: T,
|
|
522
522
|
value: unknown,
|
|
523
|
+
context?: string,
|
|
523
524
|
): Static<T> | undefined {
|
|
524
|
-
if (!Value.Check(schema, value))
|
|
525
|
+
if (!Value.Check(schema, value)) {
|
|
526
|
+
if (context) {
|
|
527
|
+
logInternalError("config.parseWithSchema", undefined, `${context}: schema validation failed`);
|
|
528
|
+
}
|
|
529
|
+
return undefined;
|
|
530
|
+
}
|
|
525
531
|
return Value.Decode(schema, value);
|
|
526
532
|
}
|
|
527
533
|
|
package/src/config/defaults.ts
CHANGED
|
@@ -1,4 +1,14 @@
|
|
|
1
|
-
export const DEFAULT_CHILD_PI
|
|
1
|
+
export const DEFAULT_CHILD_PI: Readonly<{
|
|
2
|
+
postExitStdioGuardMs: number;
|
|
3
|
+
finalDrainMs: number;
|
|
4
|
+
hardKillMs: number;
|
|
5
|
+
responseTimeoutMs: number;
|
|
6
|
+
maxCaptureBytes: number;
|
|
7
|
+
maxAssistantTextChars: number;
|
|
8
|
+
maxToolResultChars: number;
|
|
9
|
+
maxToolInputChars: number;
|
|
10
|
+
maxCompactContentChars: number;
|
|
11
|
+
}> = {
|
|
2
12
|
postExitStdioGuardMs: 3000,
|
|
3
13
|
finalDrainMs: 5000,
|
|
4
14
|
hardKillMs: 3000,
|
|
@@ -20,6 +20,7 @@ import { registerAutonomousPolicy } from "./autonomous-policy.ts";
|
|
|
20
20
|
import { registerCleanupHandler } from "./crew-cleanup.ts";
|
|
21
21
|
import type { ScheduledJob } from "../runtime/scheduler.ts";
|
|
22
22
|
import { clearHooks } from "../hooks/registry.ts";
|
|
23
|
+
import { uninstallCrewGlobalRegistry } from "./team-tool.ts";
|
|
23
24
|
import { notifyActiveRuns } from "./session-summary.ts";
|
|
24
25
|
|
|
25
26
|
let _cachedLiveRunSidebar: typeof LiveRunSidebarType | undefined;
|
|
@@ -1112,6 +1113,7 @@ export function registerPiTeams(pi: ExtensionAPI): void {
|
|
|
1112
1113
|
metricRegistry = undefined;
|
|
1113
1114
|
deliveryCoordinator?.dispose();
|
|
1114
1115
|
clearHooks();
|
|
1116
|
+
uninstallCrewGlobalRegistry();
|
|
1115
1117
|
overflowTracker?.dispose();
|
|
1116
1118
|
deliveryCoordinator = undefined;
|
|
1117
1119
|
overflowTracker = undefined;
|
|
@@ -1278,3 +1278,8 @@ export function installCrewGlobalRegistry(): void {
|
|
|
1278
1278
|
listDynamicAgents,
|
|
1279
1279
|
});
|
|
1280
1280
|
}
|
|
1281
|
+
|
|
1282
|
+
/** Remove the global CrewRegistry singleton. Call during session cleanup. */
|
|
1283
|
+
export function uninstallCrewGlobalRegistry(): void {
|
|
1284
|
+
delete (globalThis as Record<symbol | string, unknown>)[CREW_REGISTRY_KEY];
|
|
1285
|
+
}
|
package/src/hooks/registry.ts
CHANGED
|
@@ -30,6 +30,9 @@ export async function executeHook(name: HookName, ctx: HookContext): Promise<Hoo
|
|
|
30
30
|
// SECURITY: If ctx contains a workspaceId, filter hooks to only those scoped to
|
|
31
31
|
// this workspace. This prevents globally-registered hooks from operating on runs
|
|
32
32
|
// they weren't designed for.
|
|
33
|
+
// SECURITY: Hooks without workspaceId match ALL workspaces. This is intentional
|
|
34
|
+
// for globally-applicable hooks (e.g., logging, metrics). For multi-tenant
|
|
35
|
+
// environments, all hooks should set workspaceId to prevent cross-workspace access.
|
|
33
36
|
const scopedHooks = hooks.filter((h) => !h.workspaceId || h.workspaceId === ctx.workspaceId);
|
|
34
37
|
if (scopedHooks.length === 0) return { hookName: name, outcome: "allow", durationMs: 0 };
|
|
35
38
|
const start = Date.now();
|
|
@@ -57,6 +57,10 @@ function sanitizeSettings(raw: unknown): CrewSettings {
|
|
|
57
57
|
if (typeof r.notifierIntervalMs === "number" && r.notifierIntervalMs >= 1000) {
|
|
58
58
|
out.notifierIntervalMs = r.notifierIntervalMs;
|
|
59
59
|
}
|
|
60
|
+
// Pass through scheduledJobs as opaque array (validated by crewScheduler.add)
|
|
61
|
+
if (Array.isArray(r.scheduledJobs)) {
|
|
62
|
+
out.scheduledJobs = r.scheduledJobs;
|
|
63
|
+
}
|
|
60
64
|
return out;
|
|
61
65
|
}
|
|
62
66
|
|
|
@@ -34,7 +34,17 @@ function readIfSmall(filePath: string, maxBytes = 24_000, baseDir?: string): str
|
|
|
34
34
|
try {
|
|
35
35
|
const safePath = baseDir ? resolveRealContainedPath(baseDir, filePath) : filePath;
|
|
36
36
|
const stat = fs.statSync(safePath);
|
|
37
|
-
if (stat.size > maxBytes)
|
|
37
|
+
if (stat.size > maxBytes) {
|
|
38
|
+
// Use bounded read to avoid loading entire file into memory
|
|
39
|
+
const buf = Buffer.alloc(maxBytes);
|
|
40
|
+
const fd = fs.openSync(safePath, "r");
|
|
41
|
+
try {
|
|
42
|
+
fs.readSync(fd, buf, 0, maxBytes, 0);
|
|
43
|
+
} finally {
|
|
44
|
+
fs.closeSync(fd);
|
|
45
|
+
}
|
|
46
|
+
return `${buf.toString("utf-8")}\n\n...(truncated ${stat.size - maxBytes} bytes)`;
|
|
47
|
+
}
|
|
38
48
|
return fs.readFileSync(safePath, "utf-8");
|
|
39
49
|
} catch {
|
|
40
50
|
return undefined;
|
package/src/state/event-log.ts
CHANGED
|
@@ -149,12 +149,18 @@ function parseSequence(raw: string): number | undefined {
|
|
|
149
149
|
export function scanSequence(eventsPath: string): number {
|
|
150
150
|
if (!fs.existsSync(eventsPath)) return 0;
|
|
151
151
|
let max = 0;
|
|
152
|
+
let skipped = 0;
|
|
152
153
|
for (const line of fs.readFileSync(eventsPath, "utf-8").split("\n")) {
|
|
153
154
|
if (!line.trim()) continue;
|
|
154
155
|
try {
|
|
155
156
|
const event = JSON.parse(line) as TeamEvent;
|
|
156
157
|
max = Math.max(max, event.metadata?.seq ?? 0);
|
|
157
|
-
} catch {
|
|
158
|
+
} catch {
|
|
159
|
+
skipped++;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
if (skipped > 0) {
|
|
163
|
+
logInternalError("event-log.scanSequence.corrupt_lines", undefined, `${eventsPath}: skipped ${skipped} corrupt line(s)`);
|
|
158
164
|
}
|
|
159
165
|
return max;
|
|
160
166
|
}
|
package/src/state/mailbox.ts
CHANGED
|
@@ -327,6 +327,18 @@ function writeDeliveryState(manifest: TeamRunManifest, state: MailboxDeliverySta
|
|
|
327
327
|
atomicWriteFile(deliveryFile(manifest, true), `${JSON.stringify(redactSecrets(state), null, 2)}\n`);
|
|
328
328
|
}
|
|
329
329
|
|
|
330
|
+
/**
|
|
331
|
+
* Append a message to a run's or task's mailbox.
|
|
332
|
+
*
|
|
333
|
+
* SECURITY NOTE: The `from` field is caller-declared — there is no cryptographic
|
|
334
|
+
* sender authentication. This is acceptable because `appendMailboxMessage` is an
|
|
335
|
+
* internal API only callable from within the pi-crew process (no external input).
|
|
336
|
+
* All callers (handleSteer, handleRespond, handleFollowUp) derive `from` from
|
|
337
|
+
* authenticated context (session role, task assignment).
|
|
338
|
+
*
|
|
339
|
+
* If pi-crew ever exposes mailbox writes to external/untrusted input, sender
|
|
340
|
+
* authentication (HMAC or session key) must be added.
|
|
341
|
+
*/
|
|
330
342
|
export function appendMailboxMessage(manifest: TeamRunManifest, message: Omit<MailboxMessage, "id" | "runId" | "createdAt" | "status"> & { id?: string; status?: MailboxMessageStatus }): MailboxMessage {
|
|
331
343
|
if (message.taskId) ensureTaskMailbox(manifest, message.taskId);
|
|
332
344
|
else ensureRunMailbox(manifest);
|
package/src/tools/safe-bash.ts
CHANGED
|
@@ -201,22 +201,23 @@ export function isDangerous(command: string, options: SafeBashOptions = {}): str
|
|
|
201
201
|
}
|
|
202
202
|
|
|
203
203
|
// Additional shell injection checks using regex for non-critical patterns
|
|
204
|
-
// Block command substitution $(...)
|
|
205
|
-
|
|
204
|
+
// Block command substitution $(...) — use normalized to prevent $\n(evil) bypass
|
|
205
|
+
// Also match $<space>(...) which is the normalized form of $\n(evil)
|
|
206
|
+
if (/\$\s*\([^)]*\)/.test(normalized)) {
|
|
206
207
|
return "Command blocked by safe_bash: command substitution $(...) is not allowed";
|
|
207
208
|
}
|
|
208
209
|
// Block backtick substitution
|
|
209
210
|
const backtickRe = /`[^`]*`/;
|
|
210
|
-
if (backtickRe.test(
|
|
211
|
+
if (backtickRe.test(normalized)) {
|
|
211
212
|
return "Command blocked by safe_bash: backtick substitution is not allowed";
|
|
212
213
|
}
|
|
213
214
|
// Block here-docs <<
|
|
214
|
-
if (/<<\s*['"]?[\w-]+['"]?/.test(
|
|
215
|
+
if (/<<\s*['"]?[\w-]+['"]?/.test(normalized) || /\$<<\s*['"]?[\w-]+['"]?/.test(normalized)) {
|
|
215
216
|
return "Command blocked by safe_bash: here-doc is not allowed";
|
|
216
217
|
}
|
|
217
|
-
// Block ${...} variable expansion containing shell metacharacters
|
|
218
|
+
// Block ${...} variable expansion containing shell metacharacters
|
|
218
219
|
const varExpRe = /\$\{([^}]*)\}/;
|
|
219
|
-
const varMatch =
|
|
220
|
+
const varMatch = normalized.match(varExpRe);
|
|
220
221
|
if (varMatch && /[|&;<>]/.test(varMatch[1])) {
|
|
221
222
|
return "Command blocked by safe_bash: variable expansion with shell metacharacters is not allowed";
|
|
222
223
|
}
|