pi-crew 0.11.30 → 0.11.35

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.11.30",
3
+ "version": "0.11.35",
4
4
  "description": "Pi extension for coordinated AI teams, workflows, worktrees, and async task orchestration",
5
5
  "author": "baphuongna",
6
6
  "license": "MIT",
@@ -198,6 +198,24 @@ const CREW_ENV_FORWARDING_DECL: Record<string, CrewEnvForwardDecision> = {
198
198
  reason: "per-task compound broker token via built.env/pipe (F4) — secret, never allowlisted",
199
199
  },
200
200
  PI_CREW_BROKER_TASK_ID: { kind: "child-write", reason: "child-pi-spawn broker handshake" },
201
+ PI_CREW_INFERENCE_BROKER: {
202
+ kind: "gap",
203
+ reason: "v0.11.31 inference broker toggle — resolved in the PARENT only (ensureInferenceBroker); deliberately never forwarded to children (a child re-starting its own broker would be nonsensical)",
204
+ },
205
+ PI_CREW_INFERENCE_TOKEN: {
206
+ kind: "child-write",
207
+ reason: "v0.11.31 per-worker inference broker token via built.env — secret, never allowlisted",
208
+ },
209
+ PI_CREW_INFERENCE_BUDGET_TOKENS: {
210
+ kind: "gap",
211
+ reason: "v0.11.31 per-worker budget read by the parent broker at token issuance (registerInferenceWorker)",
212
+ },
213
+ PI_CREW_INFERENCE_UPSTREAM: {
214
+ kind: "test-only",
215
+ reason: "v0.11.31 broker upstream override — TESTS ONLY (local mock), production never sets it",
216
+ },
217
+ PI_CREW_INFERENCE_UPSTREAM_ANTHROPIC: { kind: "test-only", reason: "v0.11.34 anthropic-only broker upstream override — TESTS ONLY" },
218
+ PI_CREW_INFERENCE_UPSTREAM_OPENAI: { kind: "test-only", reason: "v0.11.34 openai-only broker upstream override — TESTS ONLY" },
201
219
  PI_CREW_STEERING_FILE: { kind: "child-write", reason: "child-pi-spawn per-run steering JSONL" },
202
220
  PI_CREW_MAX_OUTPUT: { kind: "child-write", reason: "pi-args emits agent.maxTokens into built.env" },
203
221
  PI_CREW_RPC_SECRET: { kind: "child-write", reason: "shared HMAC secret handed to the child at spawn — secret name, never allowlisted" },
@@ -494,6 +494,32 @@ export const CREW_ENV_VARS: Record<string, CrewEnvVarSpec> = {
494
494
  name: "PI_CREW_WORKER_TRANSPORT",
495
495
  doc: "W7 EXPERIMENTAL worker transport override: 'rpc' selects the prototype RPC frame-client (src/runtime/rpc/) at the run-worker seam (wired e2728f68; D1/DR2 also plumbs config runtime.workerTransport to the same seam — env WINS over config when set, invalid env fails safe to stdio); anything else/unset → config value, default stdio child-pi path (rpc-worker.ts resolveWorkerTransport)",
496
496
  },
497
+ PI_CREW_INFERENCE_BROKER: {
498
+ name: "PI_CREW_INFERENCE_BROKER",
499
+ parser: "boolean",
500
+ doc: "v0.11.31 (S-broker PoC) opt-in loopback inference broker: '1' starts a 127.0.0.1 HTTP broker in the parent; child workers get ONLY a per-worker token (PI_CREW_INFERENCE_TOKEN) + a scoped agent dir (PI_CODING_AGENT_DIR containing models.json that reroutes the built-in anthropic provider to the broker) instead of any real provider key. Real key (ANTHROPIC_API_KEY / ANTHROPIC_AUTH_TOKEN from the PARENT env only) is substituted after requests leave the worker. FAIL-CLOSED: flag set without an upstream credential throws at first spawn (inference-broker.ts). PoC: anthropic upstream only, env-credential users only (auth.json users unsupported yet)",
501
+ },
502
+ PI_CREW_INFERENCE_TOKEN: {
503
+ name: "PI_CREW_INFERENCE_TOKEN",
504
+ doc: "v0.11.31 child-side per-worker inference broker token (256-bit random, heap-only on the parent, injected via built.env by prepareSpawnContext when the broker is active; interpolated into models.json apiKey as $PI_CREW_INFERENCE_TOKEN by the child at request time). NEVER user-set; workerKey-scoped — re-registering a task id replaces (kills) the old token",
505
+ },
506
+ PI_CREW_INFERENCE_BUDGET_TOKENS: {
507
+ name: "PI_CREW_INFERENCE_BUDGET_TOKENS",
508
+ parser: "int",
509
+ doc: "v0.11.31 per-worker inference token budget counted from streamed Anthropic usage events; 0/unset = unlimited. Requests past the budget get 429 (inference-broker.ts registerInferenceWorker)",
510
+ },
511
+ PI_CREW_INFERENCE_UPSTREAM: {
512
+ name: "PI_CREW_INFERENCE_UPSTREAM",
513
+ doc: "v0.11.31 inference broker upstream override applied to EVERY provider (defaults: anthropic https://api.anthropic.com, openai https://api.openai.com/v1). TESTS ONLY — points the broker at a local mock upstream; production callers must never set it",
514
+ },
515
+ PI_CREW_INFERENCE_UPSTREAM_ANTHROPIC: {
516
+ name: "PI_CREW_INFERENCE_UPSTREAM_ANTHROPIC",
517
+ doc: "v0.11.34 anthropic-only broker upstream override (beats PI_CREW_INFERENCE_UPSTREAM). TESTS ONLY — local mock; production must never set it",
518
+ },
519
+ PI_CREW_INFERENCE_UPSTREAM_OPENAI: {
520
+ name: "PI_CREW_INFERENCE_UPSTREAM_OPENAI",
521
+ doc: "v0.11.34 openai-only broker upstream override (beats PI_CREW_INFERENCE_UPSTREAM). TESTS ONLY — local mock; production must never set it",
522
+ },
497
523
  PI_CREW_CLASSIFIER_ENABLED: {
498
524
  name: "PI_CREW_CLASSIFIER_ENABLED",
499
525
  parser: "boolean",
@@ -1,4 +1,5 @@
1
1
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+ import { stopInferenceBroker } from "../runtime/child-pi/inference-broker.ts";
2
3
  import { cleanupAllTrackedTempDirs } from "../runtime/model/pi-args.ts";
3
4
  import { isSessionSwitchInFlight } from "../ui/inline-panel/view-session-store.ts";
4
5
  import { logInternalError } from "../utils/internal-error.ts";
@@ -89,6 +90,16 @@ export function registerCleanupHandler(pi: ExtensionAPI, opts?: { disposeTermina
89
90
  // Kill all child-pi processes
90
91
  await cleanupChildProcesses();
91
92
 
93
+ // v0.11.31: stop the loopback inference broker — revokes every
94
+ // worker token (kill-switch) and removes the scoped agent dir BEFORE
95
+ // generic temp cleanup (which would also catch it — this is the
96
+ // explicit, token-revoking path).
97
+ try {
98
+ stopInferenceBroker();
99
+ } catch (error) {
100
+ logInternalError("crew-cleanup.inference-broker", error);
101
+ }
102
+
92
103
  // Cleanup temp directories
93
104
  await cleanupTempDirectories();
94
105
 
@@ -28,6 +28,13 @@ import { buildPiWorkerArgs, createSafeTempDir, getPiTempBase } from "../model/pi
28
28
  import { getPiSpawnCommand } from "../pi-spawn.ts";
29
29
  import { findLatestScratchpadSnapshot } from "../scratchpad/snapshot-lookup.ts";
30
30
  import type { ChildPiRunInput, ChildPiRunResult } from "./child-pi.ts";
31
+ import {
32
+ getInferenceBrokerSnapshot,
33
+ INFERENCE_TOKEN_ENV,
34
+ PI_AGENT_DIR_ENV,
35
+ registerInferenceWorker,
36
+ requireInferenceBrokerIfEnabled,
37
+ } from "./inference-broker.ts";
31
38
 
32
39
  // ── Env allowlist (base set always passed to children) ──────────────────
33
40
  // Provider API keys are injected dynamically via buildScopedAllowList() only
@@ -102,7 +109,13 @@ export function buildChildPiSpawnOptions(
102
109
  // provider does not spawn keyless. When no model is given, only
103
110
  // BASE_ALLOWLIST system vars pass through — no provider keys leak.
104
111
  const scopeModels: (string | undefined)[] = modelChain && modelChain.length > 0 ? modelChain : model !== undefined ? [model] : [];
105
- const allowList = scopeModels.length > 0 ? buildScopedAllowList(BASE_ALLOWLIST, scopeModels) : BASE_ALLOWLIST;
112
+ // v0.11.31 inference broker: when active, the child's ONLY credential is
113
+ // its per-worker broker token — provider env keys are NEVER injected (they
114
+ // would defeat the broker by handing the worker the real reusable key
115
+ // again). The broker snapshot is read-only module state (no side effects —
116
+ // the broker is started by prepareSpawnContext, never here).
117
+ const inferenceBrokerActive = getInferenceBrokerSnapshot().active;
118
+ const allowList = !inferenceBrokerActive && scopeModels.length > 0 ? buildScopedAllowList(BASE_ALLOWLIST, scopeModels) : BASE_ALLOWLIST;
106
119
  // R6-8: warn when the scoped models resolve to ZERO provider env keys AND at
107
120
  // least one is a truly-bare id (no "provider/" prefix) — env-key auth + bare
108
121
  // id means the child worker will very likely fail authentication (observed as
@@ -244,6 +257,15 @@ export function buildFinalChildPiSpawnOptions(
244
257
  // process would never see steering file, kind, role, or broker creds.
245
258
  // Safe because step (a) just proved builtEnv holds no secret keys.
246
259
  spawnOptions.env = { ...spawnOptions.env, ...builtEnv };
260
+ // (c′) v0.11.31 inference broker: pi's NATIVE agent-dir override is NOT a
261
+ // control-namespace key, so it never travels through built.env (step (a)
262
+ // would reject it) — it is mapped here, at the spawn-options layer, from
263
+ // the broker snapshot when a per-worker token is present. The scoped dir
264
+ // contains ONLY models.json (no auth.json, no extensions, no skills).
265
+ const inferenceSnapshot = getInferenceBrokerSnapshot();
266
+ if (inferenceSnapshot.active && builtEnv[INFERENCE_TOKEN_ENV]) {
267
+ spawnOptions.env[PI_AGENT_DIR_ENV] = inferenceSnapshot.scopedAgentDir ?? "";
268
+ }
247
269
  // (d) Return the composed SpawnOptions for spawn(...).
248
270
  return spawnOptions;
249
271
  }
@@ -265,6 +287,8 @@ export interface SpawnContext {
265
287
  tempDir: string | undefined;
266
288
  /** The per-call built.env (control vars only) — for security canary assertions. */
267
289
  builtEnv: Record<string, string | undefined>;
290
+ /** v0.11.31: inference-broker worker key that owns this spawn's token (cleanup/revoke bookkeeping). */
291
+ inferenceWorkerKey?: string;
268
292
  }
269
293
 
270
294
  /**
@@ -308,6 +332,21 @@ export function prepareSpawnContext(
308
332
  // child env gets parentDepth+1 = the true grandchild depth.
309
333
  env: depthEnv,
310
334
  });
335
+ // v0.11.31 inference broker (opt-in PI_CREW_INFERENCE_BROKER=1): the broker
336
+ // must already be running (child-pi.ts run() awaits maybeStartInferenceBroker()
337
+ // first — http listen needs an event-loop turn, so the SYNC spawn path can
338
+ // only require, never start). Issue this worker's token. Control-namespace
339
+ // key → passes assertOnlyControlEnvKeys. FAIL-CLOSED: flag on without a
340
+ // started broker or an upstream credential throws here (never silently
341
+ // spawn keyless / real-key workers). Worker key: the task id when present
342
+ // (retries REPLACE the old token — registerInferenceWorker semantics), else
343
+ // a per-spawn unique.
344
+ const inferenceSnapshot = requireInferenceBrokerIfEnabled();
345
+ let inferenceWorkerKey: string | undefined;
346
+ if (inferenceSnapshot.active) {
347
+ inferenceWorkerKey = input.agentId ? `inference:${input.agentId}` : `inference:spawn:${process.pid}:${Date.now().toString(36)}`;
348
+ built.env[INFERENCE_TOKEN_ENV] = registerInferenceWorker(inferenceWorkerKey);
349
+ }
311
350
  // Pass steering file path to child for real-time steer injection
312
351
  if (input.steeringFile) built.env.PI_CREW_STEERING_FILE = input.steeringFile;
313
352
  // I5: run/task identity is threaded ALWAYS (not broker-gated) so the worker's
@@ -433,6 +472,7 @@ export function prepareSpawnContext(
433
472
  mergedEnv: { ...process.env, ...built.env },
434
473
  tempDir: built.tempDir,
435
474
  builtEnv: built.env,
475
+ inferenceWorkerKey,
436
476
  },
437
477
  };
438
478
  }
@@ -36,6 +36,7 @@ import { ChildPiSteeringController } from "./child-pi-steering.ts";
36
36
  import { ChildPiLineObserver, isFinalAssistantEvent } from "./child-pi-streams.ts";
37
37
  // Phase 2.3: the six timer constructs moved to child-pi-timers.ts (pure motion).
38
38
  import { createChildPiTimers } from "./child-pi-timers.ts";
39
+ import { maybeStartInferenceBroker } from "./inference-broker.ts";
39
40
  import { runMockChildPi } from "./mock-fixtures.ts";
40
41
  // W2 (session-file recovery): deterministic worker session flags + crash-path
41
42
  // tail-replay of the session JSONL (see session-recovery.ts header).
@@ -777,6 +778,9 @@ export async function runChildPi(input: ChildPiRunInput): Promise<ChildPiRunResu
777
778
  // guard below returns before the argv is ever consumed.
778
779
  const versionedInput: ChildPiRunInput =
779
780
  !input.signal?.aborted && !resolveHermeticWorkers(input.hermeticWorkers) ? { ...input, piVersion: await probePiVersion() } : input;
781
+ // v0.11.31: start the inference broker (flag-gated, fail-closed) BEFORE the
782
+ // sync spawn-prep path — http listen needs an event-loop turn.
783
+ await maybeStartInferenceBroker();
780
784
  const spawnPrep = prepareSpawnContext(brokerSpawn ? { ...versionedInput, brokerSpawn } : versionedInput, effectiveTask, depthEnv);
781
785
  if (spawnPrep.kind === "aborted") return spawnPrep.result;
782
786
  const { spawnSpec, mergedEnv, tempDir, builtEnv, builtArgs } = spawnPrep.ctx;