pi-crew 0.9.35 → 0.9.36

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-crew",
3
- "version": "0.9.35",
3
+ "version": "0.9.36",
4
4
  "description": "Pi extension for coordinated AI teams, workflows, worktrees, and async task orchestration",
5
5
  "author": "baphuongna",
6
6
  "license": "MIT",
@@ -210,7 +210,7 @@ export const BACKGROUND_RUNNER_ENV_ALLOWLIST: string[] = [
210
210
  // Phase 1.5: worker-thread atomic writer opt-in (RFC 15).
211
211
  "PI_CREW_WORKER_ATOMIC_WRITER",
212
212
  "PI_TEAMS_WORKER_ATOMIC_WRITER",
213
- // Phase 1.5 #1: verification env sanitization opt-in (RFC 13 §6).
213
+ // Phase 1.5 #1: verification env sanitization opt-out (RFC 13 §6).
214
214
  "PI_CREW_VERIFICATION_SANITIZE_ENV",
215
215
  "PI_TEAMS_VERIFICATION_SANITIZE_ENV",
216
216
  "PI_CREW_VERIFICATION_PRESERVE_ENV",
@@ -25,8 +25,9 @@ import { redactSecretString } from "../utils/redaction.ts";
25
25
  * against adversarial workers; this kills the leak at the source by never
26
26
  * giving the verification process the secret in the first place.
27
27
  *
28
- * Opt-in via `PI_CREW_VERIFICATION_SANITIZE_ENV=1` to avoid breaking existing
29
- * flows whose tests legitimately need API access. Escape hatch:
28
+ * Opt-out via `PI_CREW_VERIFICATION_SANITIZE_ENV=0` or
29
+ * `PI_TEAMS_VERIFICATION_SANITIZE_ENV=0` to disable sanitization (e.g. for
30
+ * tests that legitimately need API access). Escape hatch:
30
31
  * `PI_CREW_VERIFICATION_PRESERVE_ENV=KEY1,KEY2,...` lets users explicitly opt
31
32
  * specific secrets back in (audited via the allowlist validator).
32
33
  */
@@ -62,9 +63,13 @@ const VERIFICATION_ENV_ALLOWLIST: readonly string[] = [
62
63
  "NPM_CONFIG_GLOBALCONFIG",
63
64
  ];
64
65
 
65
- /** Whether env sanitization for verification is enabled (env var opt-in). */
66
+ /** Whether env sanitization for verification is enabled (opt-out: enabled by default). */
66
67
  export function isVerificationEnvSanitizeEnabled(): boolean {
67
- return process.env.PI_CREW_VERIFICATION_SANITIZE_ENV === "1" || process.env.PI_TEAMS_VERIFICATION_SANITIZE_ENV === "1";
68
+ // Opt-out: explicitly disabled via env var set to '0'.
69
+ if (process.env.PI_CREW_VERIFICATION_SANITIZE_ENV === "0" || process.env.PI_TEAMS_VERIFICATION_SANITIZE_ENV === "0") {
70
+ return false;
71
+ }
72
+ return true;
68
73
  }
69
74
 
70
75
  /**
@@ -1,6 +1,7 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import * as fs from "node:fs";
3
3
  import * as path from "node:path";
4
+ import { assertSafePathId } from "../utils/safe-paths.ts";
4
5
 
5
6
  /**
6
7
  * Represents a learned instinct that guides agent behavior.
@@ -120,6 +121,7 @@ export class InstinctStore {
120
121
  if (!savedInstinct.projectId) {
121
122
  throw new Error("Project-scoped instinct requires a projectId");
122
123
  }
124
+ assertSafePathId("projectId", savedInstinct.projectId);
123
125
  this.appendInstinctToFile(this.getProjectInstinctPath(savedInstinct.projectId), savedInstinct);
124
126
  }
125
127
 
@@ -163,6 +165,7 @@ export class InstinctStore {
163
165
  * @returns Array of instincts for the project
164
166
  */
165
167
  getProjectInstincts(projectId: string): Instinct[] {
168
+ assertSafePathId("projectId", projectId);
166
169
  const projectInstincts = this.readInstinctsFromFile(this.getProjectInstinctPath(projectId));
167
170
  const globalInstincts = this.readInstinctsFromFile(this.getGlobalInstinctPath());
168
171
  return [...projectInstincts, ...globalInstincts];
@@ -183,6 +186,7 @@ export class InstinctStore {
183
186
  .readdirSync(projectsDir, { withFileTypes: true })
184
187
  .filter((e) => e.isDirectory())
185
188
  .map((e) => e.name)) {
189
+ assertSafePathId("projectId", projectId);
186
190
  const filePath = path.join(projectsDir, projectId, INSTINCT_FILE);
187
191
  const instincts = this.readInstinctsFromFile(filePath);
188
192
  const index = instincts.findIndex((i) => i.id === instinctId);
@@ -239,6 +243,7 @@ export class InstinctStore {
239
243
  .readdirSync(projectsDir, { withFileTypes: true })
240
244
  .filter((e) => e.isDirectory())
241
245
  .map((e) => e.name)) {
246
+ assertSafePathId("projectId", projectId);
242
247
  const filePath = path.join(projectsDir, projectId, INSTINCT_FILE);
243
248
  instincts = this.readInstinctsFromFile(filePath);
244
249
  index = instincts.findIndex((i) => i.id === instinctId);
@@ -56,6 +56,11 @@ export function ensureIntermediateDir(config: Partial<IntermediateStoreConfig> =
56
56
  * @returns Path to the written file
57
57
  */
58
58
  export function writeIntermediate(config: Partial<IntermediateStoreConfig>, phase: string, stepId: string, data: unknown): string {
59
+ // Security: validate phase/stepId before building the filename to prevent
60
+ // path traversal via poisoned phase or stepId.
61
+ if (!isSafePathId(phase) || !isSafePathId(stepId)) {
62
+ throw new Error("Invalid phase or stepId for intermediate store");
63
+ }
59
64
  const dir = ensureIntermediateDir(config);
60
65
  const filename = `${phase}-${stepId}.json`;
61
66
  const filePath = path.join(dir, filename);