pi-crew 0.11.29 → 0.11.30
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 +12 -0
- package/dist/index.mjs +292 -292
- package/package.json +1 -1
- package/src/runtime/child-pi/child-pi-rpc.ts +56 -26
package/package.json
CHANGED
|
@@ -340,6 +340,42 @@ function readPsEnviron(pid: number): string | undefined {
|
|
|
340
340
|
|
|
341
341
|
let unobservableWarned = false;
|
|
342
342
|
|
|
343
|
+
/** S* (v0.11.30, supersedes the wave-8 process.env swap): build the
|
|
344
|
+
* RpcClient `env` option that yields an EXACT env for the child despite the
|
|
345
|
+
* SDK's internal merge `{...process.env, ...options.env}` (rpc-client.js).
|
|
346
|
+
*
|
|
347
|
+
* Mechanism (both halves experiment-verified 2026-10-20):
|
|
348
|
+
* 1. `{...A, ...{K: undefined}}` — a later key with an `undefined` value
|
|
349
|
+
* OVERRIDES the earlier key (spread semantics).
|
|
350
|
+
* 2. Node's spawn DROPS env entries whose value is `undefined` (the child
|
|
351
|
+
* sees the key as ABSENT, not "undefined"/"").
|
|
352
|
+
* Together: every parent key NOT in the intended set is shadowed to
|
|
353
|
+
* `undefined` in options.env → the merge yields `undefined` for it → Node
|
|
354
|
+
* omits it → the child receives EXACTLY the intended set, on every OS,
|
|
355
|
+
* deterministically — regardless of WHEN the spawn actually happens. The
|
|
356
|
+
* wave-2b/8 sync-prefix assumption and the process.env swap both die as
|
|
357
|
+
* requirements: even a drifted (post-await) spawn merges the same shadowed
|
|
358
|
+
* options.env and still lands on the exact set.
|
|
359
|
+
*
|
|
360
|
+
* Forward-compat: if upstream ever removes the process.env spread, the
|
|
361
|
+
* undefined entries are still dropped by Node → same exact env. If a future
|
|
362
|
+
* SDK validates env values strictly, start() throws LOUD (fail-loud, the
|
|
363
|
+
* right direction). The post-spawn canary (verifyRpcSpawnEnvSplice) stays as
|
|
364
|
+
* a PRODUCTION TRIPWIRE proving "child env == intended set".
|
|
365
|
+
*
|
|
366
|
+
* Runtime typing note: RpcClient's .d.ts says Record<string,string>, but the
|
|
367
|
+
* runtime only spreads the object into spawn (single usage, rpc-client.js)
|
|
368
|
+
* — undefined values flow through harmlessly. Exported for unit tests. */
|
|
369
|
+
export function buildExactEnvShadowOptions(
|
|
370
|
+
intendedEnv: NodeJS.ProcessEnv | Record<string, string | undefined> | undefined,
|
|
371
|
+
): Record<string, string | undefined> {
|
|
372
|
+
const shadow: Record<string, string | undefined> = { ...(intendedEnv ?? {}) };
|
|
373
|
+
for (const key of Object.keys(process.env)) {
|
|
374
|
+
if (!(key in shadow)) shadow[key] = undefined;
|
|
375
|
+
}
|
|
376
|
+
return shadow;
|
|
377
|
+
}
|
|
378
|
+
|
|
343
379
|
/** MINOR-4 / R2-L-A7: post-spawn canary verification. The parent env carries
|
|
344
380
|
* {@link RPC_ENV_SPLICE_CANARY} while the window is open (open->close spans
|
|
345
381
|
* the spawn); the canary is NEVER in the allowlisted childEnv. If the child
|
|
@@ -452,11 +488,7 @@ async function runRpcSession(
|
|
|
452
488
|
const { input, effectiveTask, ctx } = plan;
|
|
453
489
|
const poolKey = poolKeyFor(input);
|
|
454
490
|
const spawnOptions = buildFinalChildPiSpawnOptions(input.cwd, ctx.mergedEnv, ctx.builtEnv, input.model);
|
|
455
|
-
|
|
456
|
-
const childEnv: Record<string, string> = {};
|
|
457
|
-
for (const [k, v] of Object.entries(spawnOptions.env ?? {})) {
|
|
458
|
-
if (v !== undefined) childEnv[k] = v;
|
|
459
|
-
}
|
|
491
|
+
const childEnv = buildExactEnvShadowOptions(spawnOptions.env);
|
|
460
492
|
|
|
461
493
|
let client: RpcClientInstance;
|
|
462
494
|
if (pooled) {
|
|
@@ -493,21 +525,24 @@ async function runRpcSession(
|
|
|
493
525
|
/* no-op unless a fresh spawn installs the real closer below */
|
|
494
526
|
};
|
|
495
527
|
if (!pooled) {
|
|
496
|
-
//
|
|
497
|
-
//
|
|
498
|
-
//
|
|
499
|
-
//
|
|
500
|
-
//
|
|
501
|
-
//
|
|
502
|
-
//
|
|
503
|
-
//
|
|
504
|
-
//
|
|
505
|
-
//
|
|
506
|
-
//
|
|
507
|
-
//
|
|
508
|
-
// spawns share one window
|
|
509
|
-
|
|
510
|
-
delete
|
|
528
|
+
// S* (v0.11.30): NO process.env swap anymore. The RpcClient-internal
|
|
529
|
+
// merge `{...process.env, ...options.env}` is neutralized BY the
|
|
530
|
+
// undefined-shadow options.env (buildExactEnvShadowOptions) — the child
|
|
531
|
+
// receives exactly the intended allowlisted set no matter when the spawn
|
|
532
|
+
// fires (sync prefix or drifted past an await). The wave-2b..8 global
|
|
533
|
+
// `process.env = childEnv` splice is deleted — global mutable state gone.
|
|
534
|
+
//
|
|
535
|
+
// The canary WINDOW below is now purely a VERIFICATION aid: the marker
|
|
536
|
+
// must sit in the parent env at snapshot time so verifyRpcSpawnEnvSplice
|
|
537
|
+
// has something to detect with. It must span the await because the
|
|
538
|
+
// child's env snapshot is taken at its (possibly delayed) spawn; the
|
|
539
|
+
// shadow makes the marker harmless to the child either way. Refcounted:
|
|
540
|
+
// concurrent RPC spawns share one window.
|
|
541
|
+
// Belt & braces: SHADOW the canary to undefined (do NOT delete — a
|
|
542
|
+
// delete lets the parent-env marker win the merge; undefined overrides
|
|
543
|
+
// it). Covers the ordering gap: the shadow snapshot was taken before the
|
|
544
|
+
// window opened, so the marker was not in process.env then.
|
|
545
|
+
childEnv[RPC_ENV_SPLICE_CANARY] = undefined;
|
|
511
546
|
openRpcEnvSpliceWindow();
|
|
512
547
|
let startPromise: Promise<void>;
|
|
513
548
|
windowClosed = false;
|
|
@@ -518,12 +553,7 @@ async function runRpcSession(
|
|
|
518
553
|
}
|
|
519
554
|
};
|
|
520
555
|
try {
|
|
521
|
-
|
|
522
|
-
try {
|
|
523
|
-
startPromise = client.start();
|
|
524
|
-
} finally {
|
|
525
|
-
process.env = savedEnv;
|
|
526
|
-
}
|
|
556
|
+
startPromise = client.start();
|
|
527
557
|
} catch (error) {
|
|
528
558
|
closeWindowOnce();
|
|
529
559
|
throw error;
|