stitchkit 0.108.0 → 0.108.2
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 +25 -0
- package/dist/agent-runtime/sandbox-process-owner.d.ts +3 -3
- package/dist/agent-runtime/sandbox-process-owner.d.ts.map +1 -1
- package/dist/agent-runtime/sandbox-process.d.ts +2 -2
- package/dist/agent-runtime/sandbox-process.d.ts.map +1 -1
- package/dist/agent-runtime/store-conformance-support.d.ts +5 -5
- package/dist/entrypoints/agent-runtime/coding-tools.js +2 -2
- package/dist/entrypoints/agent-runtime/sandbox.js +2 -2
- package/dist/entrypoints/process.js +1 -1
- package/dist/{index-4q9jvqg7.js → index-k0kzhdw9.js} +550 -22
- package/dist/{index-dwf0gymy.js → index-vyaehh67.js} +1 -1
- package/dist/process/bun-child.d.ts +48 -0
- package/dist/process/bun-child.d.ts.map +1 -0
- package/dist/process/command-owner.d.ts +2 -2
- package/dist/process/command-owner.d.ts.map +1 -1
- package/dist/process/contract.d.ts +5 -0
- package/dist/process/contract.d.ts.map +1 -1
- package/dist/process/launch.d.ts +3 -3
- package/dist/process/launch.d.ts.map +1 -1
- package/dist/process/owned-child.d.ts +5 -1
- package/dist/process/owned-child.d.ts.map +1 -1
- package/dist/process/owner-loss-control.d.ts +15 -0
- package/dist/process/owner-loss-control.d.ts.map +1 -0
- package/dist/process/owner-loss-guard.d.ts +3 -0
- package/dist/process/owner-loss-guard.d.ts.map +1 -0
- package/dist/process/owner-loss-protocol.d.ts +10 -0
- package/dist/process/owner-loss-protocol.d.ts.map +1 -0
- package/dist/process/owner-loss-socket.d.ts +4 -0
- package/dist/process/owner-loss-socket.d.ts.map +1 -0
- package/llms/process.txt +25 -10
- package/llms/stitchkit.txt +1 -1
- package/llms-full.txt +26 -11
- package/llms.txt +1 -1
- package/package.json +1 -1
|
@@ -70,6 +70,7 @@ var NativeCommandOptionsSchema = z.strictObject({
|
|
|
70
70
|
onLeaderStarted: z.custom((v) => typeof v === "function").optional(),
|
|
71
71
|
drainTimeoutMs: z.number().int().positive().max(MAX_TIMER_MS).optional(),
|
|
72
72
|
group: z.enum(["own", "caller"]).default("own"),
|
|
73
|
+
ownerLoss: z.enum(["terminate"]).optional(),
|
|
73
74
|
stdio: z.enum(["pipe", "inherit"]).default("pipe"),
|
|
74
75
|
stop: NativeCommandStopPolicySchema.optional(),
|
|
75
76
|
descendants: z.enum(["terminate-after-leader", "leave"]).optional(),
|
|
@@ -108,6 +109,12 @@ var NativeCommandOptionsSchema = z.strictObject({
|
|
|
108
109
|
input: ctx.value,
|
|
109
110
|
message: "group: 'caller' has no group of its own, so descendants does not apply"
|
|
110
111
|
});
|
|
112
|
+
if (input.group === "caller" && input.ownerLoss !== undefined)
|
|
113
|
+
ctx.issues.push({
|
|
114
|
+
code: "custom",
|
|
115
|
+
input: ctx.value,
|
|
116
|
+
message: "ownerLoss: 'terminate' requires group: 'own'"
|
|
117
|
+
});
|
|
111
118
|
if (input.stdio === "inherit") {
|
|
112
119
|
const piped = [
|
|
113
120
|
input.capture && "capture",
|
|
@@ -395,20 +402,371 @@ function createCommandLifetime(input) {
|
|
|
395
402
|
}
|
|
396
403
|
|
|
397
404
|
// src/process/launch.ts
|
|
398
|
-
import { Readable as
|
|
405
|
+
import { Readable as Readable3 } from "node:stream";
|
|
399
406
|
|
|
400
407
|
// src/process/owned-child.ts
|
|
401
408
|
import { spawn } from "node:child_process";
|
|
409
|
+
import { Duplex } from "node:stream";
|
|
410
|
+
|
|
411
|
+
// src/process/bun-child.ts
|
|
412
|
+
import { EventEmitter } from "node:events";
|
|
413
|
+
import { Readable, Writable } from "node:stream";
|
|
414
|
+
|
|
415
|
+
// src/process/owner-loss-control.ts
|
|
416
|
+
import { z as z2 } from "zod";
|
|
417
|
+
var MAX_GUARD_MESSAGE_BYTES = 4096;
|
|
418
|
+
var OwnerLossGuardMessageSchema = z2.discriminatedUnion("kind", [
|
|
419
|
+
z2.strictObject({ kind: z2.literal("started"), pid: z2.number().int().positive() }),
|
|
420
|
+
z2.strictObject({
|
|
421
|
+
kind: z2.literal("unavailable"),
|
|
422
|
+
message: z2.string().min(1).max(1024),
|
|
423
|
+
code: z2.string().min(1).max(64).optional()
|
|
424
|
+
})
|
|
425
|
+
]);
|
|
426
|
+
var guards = new WeakMap;
|
|
427
|
+
function abortOwnerLossLaunch(pid) {
|
|
428
|
+
if (pid === undefined)
|
|
429
|
+
return;
|
|
430
|
+
try {
|
|
431
|
+
process.kill(-pid, "SIGKILL");
|
|
432
|
+
} catch (error) {
|
|
433
|
+
if (!(error instanceof Error && ("code" in error) && error.code === "ESRCH"))
|
|
434
|
+
throw error;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
function unavailable(message, cause) {
|
|
438
|
+
return new NativeCommandError2("COMMAND_UNAVAILABLE", "Command could not start", {
|
|
439
|
+
cause: cause ?? new Error(message)
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
function attachOwnerLossControl(child, control) {
|
|
443
|
+
const settled = Promise.withResolvers();
|
|
444
|
+
settled.promise.catch(() => {
|
|
445
|
+
return;
|
|
446
|
+
});
|
|
447
|
+
let complete = false;
|
|
448
|
+
let bytes = 0;
|
|
449
|
+
let text = "";
|
|
450
|
+
const decoder = new TextDecoder;
|
|
451
|
+
const ignoreSettledError = () => {
|
|
452
|
+
return;
|
|
453
|
+
};
|
|
454
|
+
const finish = (outcome) => {
|
|
455
|
+
if (complete)
|
|
456
|
+
return;
|
|
457
|
+
complete = true;
|
|
458
|
+
control.off("data", onData);
|
|
459
|
+
control.off("end", onMissing);
|
|
460
|
+
control.off("close", onMissing);
|
|
461
|
+
control.off("error", onError);
|
|
462
|
+
control.on("error", ignoreSettledError);
|
|
463
|
+
control.once("close", () => control.off("error", ignoreSettledError));
|
|
464
|
+
if ("pid" in outcome)
|
|
465
|
+
settled.resolve(outcome.pid);
|
|
466
|
+
else
|
|
467
|
+
settled.reject(outcome.error);
|
|
468
|
+
};
|
|
469
|
+
const onMissing = () => finish({ error: unavailable("Owner-loss guard exited before the command started") });
|
|
470
|
+
const onError = (cause) => finish({ error: unavailable("Owner-loss guard control failed", cause) });
|
|
471
|
+
const onData = (chunk) => {
|
|
472
|
+
if (complete)
|
|
473
|
+
return;
|
|
474
|
+
bytes += chunk.byteLength;
|
|
475
|
+
if (bytes > MAX_GUARD_MESSAGE_BYTES) {
|
|
476
|
+
finish({ error: unavailable("Owner-loss guard response exceeded its finite bound") });
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
text += decoder.decode(chunk, { stream: true });
|
|
480
|
+
const newline = text.indexOf(`
|
|
481
|
+
`);
|
|
482
|
+
if (newline < 0)
|
|
483
|
+
return;
|
|
484
|
+
try {
|
|
485
|
+
const message = OwnerLossGuardMessageSchema.parse(JSON.parse(text.slice(0, newline)));
|
|
486
|
+
if (message.kind === "started")
|
|
487
|
+
finish({ pid: message.pid });
|
|
488
|
+
else {
|
|
489
|
+
const cause = new Error(message.message);
|
|
490
|
+
if (message.code !== undefined)
|
|
491
|
+
Object.defineProperty(cause, "code", { value: message.code });
|
|
492
|
+
finish({ error: unavailable(message.message, cause) });
|
|
493
|
+
}
|
|
494
|
+
} catch (cause) {
|
|
495
|
+
finish({ error: unavailable("Owner-loss guard returned an invalid response", cause) });
|
|
496
|
+
}
|
|
497
|
+
};
|
|
498
|
+
control.on("data", onData);
|
|
499
|
+
control.once("end", onMissing);
|
|
500
|
+
control.once("close", onMissing);
|
|
501
|
+
control.once("error", onError);
|
|
502
|
+
child.on("exit", onMissing);
|
|
503
|
+
guards.set(child, { control, started: settled.promise });
|
|
504
|
+
}
|
|
505
|
+
function transferOwnerLossControl(source, target) {
|
|
506
|
+
const registration = guards.get(source);
|
|
507
|
+
if (registration !== undefined)
|
|
508
|
+
guards.set(target, registration);
|
|
509
|
+
}
|
|
510
|
+
async function awaitCommandStart(child) {
|
|
511
|
+
const registration = guards.get(child);
|
|
512
|
+
if (registration !== undefined)
|
|
513
|
+
await registration.started;
|
|
514
|
+
return child.pid;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// src/process/owner-loss-protocol.ts
|
|
518
|
+
var OWNER_LOSS_GUARD_FLAG = "--stitchkit-owner-loss-guard-v1";
|
|
519
|
+
function ownerLossAvailable(platform) {
|
|
520
|
+
return platform === "linux" || platform === "darwin";
|
|
521
|
+
}
|
|
522
|
+
function ownerLossGuardCommand(guard, executable, args) {
|
|
523
|
+
return [
|
|
524
|
+
process.execPath,
|
|
525
|
+
guard.entry,
|
|
526
|
+
OWNER_LOSS_GUARD_FLAG,
|
|
527
|
+
guard.signalTarget,
|
|
528
|
+
executable,
|
|
529
|
+
...args
|
|
530
|
+
];
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
// src/process/owner-loss-socket.ts
|
|
534
|
+
import { connect } from "node:net";
|
|
535
|
+
function connectOwnerLossSocket(fd) {
|
|
536
|
+
return Reflect.apply(connect, undefined, [{ fd }]);
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
// src/process/bun-child.ts
|
|
540
|
+
function hostIsBun() {
|
|
541
|
+
return typeof Bun !== "undefined";
|
|
542
|
+
}
|
|
543
|
+
function readableOf(source) {
|
|
544
|
+
const reader = source.getReader();
|
|
545
|
+
let reading = false;
|
|
546
|
+
return new Readable({
|
|
547
|
+
read() {
|
|
548
|
+
if (reading)
|
|
549
|
+
return;
|
|
550
|
+
reading = true;
|
|
551
|
+
reader.read().then(({ done, value }) => {
|
|
552
|
+
reading = false;
|
|
553
|
+
if (done)
|
|
554
|
+
this.push(null);
|
|
555
|
+
else
|
|
556
|
+
this.push(value);
|
|
557
|
+
}, (error) => {
|
|
558
|
+
reading = false;
|
|
559
|
+
this.destroy(error instanceof Error ? error : new Error(String(error)));
|
|
560
|
+
});
|
|
561
|
+
},
|
|
562
|
+
destroy(error, done) {
|
|
563
|
+
reader.cancel().then(() => done(error), () => done(error));
|
|
564
|
+
}
|
|
565
|
+
});
|
|
566
|
+
}
|
|
567
|
+
var REPORTED_SPAWN_CODES = new Set(["ENOENT", "EACCES", "EAGAIN", "EMFILE", "ENFILE"]);
|
|
568
|
+
function reportedSpawnFailure(cause) {
|
|
569
|
+
return typeof cause === "object" && cause !== null && "code" in cause && typeof cause.code === "string" && REPORTED_SPAWN_CODES.has(cause.code);
|
|
570
|
+
}
|
|
571
|
+
var waitClosed = (stream) => stream.closed ? Promise.resolve() : new Promise((resolve) => stream.once("close", () => resolve()));
|
|
572
|
+
function defineBunChildren() {
|
|
573
|
+
|
|
574
|
+
class BunProcess extends EventEmitter {
|
|
575
|
+
pipes;
|
|
576
|
+
pid;
|
|
577
|
+
exitCode = null;
|
|
578
|
+
signalCode = null;
|
|
579
|
+
#subprocess;
|
|
580
|
+
constructor(pipes) {
|
|
581
|
+
super();
|
|
582
|
+
this.pipes = pipes;
|
|
583
|
+
}
|
|
584
|
+
watch(subprocess) {
|
|
585
|
+
this.pid = subprocess.pid;
|
|
586
|
+
this.#subprocess = subprocess;
|
|
587
|
+
subprocess.exited.then(() => {
|
|
588
|
+
this.exitCode = subprocess.exitCode;
|
|
589
|
+
this.signalCode = subprocess.signalCode;
|
|
590
|
+
this.emit("exit", this.exitCode, this.signalCode);
|
|
591
|
+
for (const pipe of this.pipes)
|
|
592
|
+
if (pipe.readableFlowing === null)
|
|
593
|
+
pipe.resume();
|
|
594
|
+
this.closeAfterPipes();
|
|
595
|
+
});
|
|
596
|
+
}
|
|
597
|
+
fail(cause) {
|
|
598
|
+
for (const pipe of this.pipes)
|
|
599
|
+
pipe.resume();
|
|
600
|
+
setImmediate(() => {
|
|
601
|
+
this.emit("error", cause);
|
|
602
|
+
this.closeAfterPipes();
|
|
603
|
+
});
|
|
604
|
+
}
|
|
605
|
+
kill(signal) {
|
|
606
|
+
const subprocess = this.#subprocess;
|
|
607
|
+
if (!subprocess || this.exitCode !== null || this.signalCode !== null)
|
|
608
|
+
return false;
|
|
609
|
+
try {
|
|
610
|
+
subprocess.kill(signal);
|
|
611
|
+
return true;
|
|
612
|
+
} catch {
|
|
613
|
+
return false;
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
closeAfterPipes() {
|
|
617
|
+
Promise.all(this.pipes.map(waitClosed)).then(() => this.emit("close", this.exitCode, this.signalCode));
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
class BunChild extends BunProcess {
|
|
622
|
+
stdin;
|
|
623
|
+
stdout;
|
|
624
|
+
stderr;
|
|
625
|
+
constructor(stdin, stdout, stderr) {
|
|
626
|
+
super([stdout, stderr]);
|
|
627
|
+
this.stdin = stdin;
|
|
628
|
+
this.stdout = stdout;
|
|
629
|
+
this.stderr = stderr;
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
return { BunProcess, BunChild };
|
|
633
|
+
}
|
|
634
|
+
var bunChildren;
|
|
635
|
+
function children() {
|
|
636
|
+
bunChildren ??= defineBunChildren();
|
|
637
|
+
return bunChildren;
|
|
638
|
+
}
|
|
639
|
+
function sinkWritable(sink) {
|
|
640
|
+
let ended = false;
|
|
641
|
+
const end = () => {
|
|
642
|
+
if (ended)
|
|
643
|
+
return Promise.resolve(0);
|
|
644
|
+
ended = true;
|
|
645
|
+
return Promise.resolve().then(() => sink.end());
|
|
646
|
+
};
|
|
647
|
+
return new Writable({
|
|
648
|
+
write(chunk, _encoding, done) {
|
|
649
|
+
Promise.resolve().then(() => sink.write(chunk)).then(() => done(), done);
|
|
650
|
+
},
|
|
651
|
+
final(done) {
|
|
652
|
+
end().then(() => done(), done);
|
|
653
|
+
},
|
|
654
|
+
destroy(error, done) {
|
|
655
|
+
end().then(() => done(error), () => done(error));
|
|
656
|
+
}
|
|
657
|
+
});
|
|
658
|
+
}
|
|
659
|
+
function spawnBunChild(input) {
|
|
660
|
+
const { BunChild } = children();
|
|
661
|
+
try {
|
|
662
|
+
const ownerLoss = input.ownerLoss;
|
|
663
|
+
const guarded = ownerLoss !== undefined;
|
|
664
|
+
const command = guarded ? ownerLossGuardCommand(ownerLoss, input.executable, input.args) : [input.executable, ...input.args];
|
|
665
|
+
const subprocess = guarded ? Bun.spawn(command, {
|
|
666
|
+
cwd: input.cwd,
|
|
667
|
+
env: input.env,
|
|
668
|
+
detached: input.group,
|
|
669
|
+
stdio: ["pipe", "pipe", "pipe", "socket-fd"]
|
|
670
|
+
}) : Bun.spawn(command, {
|
|
671
|
+
cwd: input.cwd,
|
|
672
|
+
env: input.env,
|
|
673
|
+
detached: input.group,
|
|
674
|
+
stdin: "pipe",
|
|
675
|
+
stdout: "pipe",
|
|
676
|
+
stderr: "pipe"
|
|
677
|
+
});
|
|
678
|
+
const child = new BunChild(sinkWritable(subprocess.stdin), readableOf(subprocess.stdout), readableOf(subprocess.stderr));
|
|
679
|
+
child.watch(subprocess);
|
|
680
|
+
if (guarded) {
|
|
681
|
+
const fd = subprocess.stdio[3];
|
|
682
|
+
if (typeof fd !== "number") {
|
|
683
|
+
abortOwnerLossLaunch(subprocess.pid);
|
|
684
|
+
throw new Error("Owner-loss guard supplied no control socket");
|
|
685
|
+
}
|
|
686
|
+
attachOwnerLossControl(child, connectOwnerLossSocket(fd));
|
|
687
|
+
}
|
|
688
|
+
return child;
|
|
689
|
+
} catch (cause) {
|
|
690
|
+
if (!reportedSpawnFailure(cause))
|
|
691
|
+
throw cause;
|
|
692
|
+
const child = new BunChild(new Writable({ write: (_chunk, _encoding, done) => done() }), Readable.from([]), Readable.from([]));
|
|
693
|
+
child.fail(cause);
|
|
694
|
+
return child;
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
function spawnBunInheritedChild(input) {
|
|
698
|
+
const { BunProcess } = children();
|
|
699
|
+
const child = new BunProcess([]);
|
|
700
|
+
try {
|
|
701
|
+
const ownerLoss = input.ownerLoss;
|
|
702
|
+
const guarded = ownerLoss !== undefined;
|
|
703
|
+
const command = guarded ? ownerLossGuardCommand(ownerLoss, input.executable, input.args) : [input.executable, ...input.args];
|
|
704
|
+
const subprocess = guarded ? Bun.spawn(command, {
|
|
705
|
+
cwd: input.cwd,
|
|
706
|
+
env: input.env,
|
|
707
|
+
detached: input.group,
|
|
708
|
+
stdio: ["inherit", "inherit", "inherit", "socket-fd"]
|
|
709
|
+
}) : Bun.spawn(command, {
|
|
710
|
+
cwd: input.cwd,
|
|
711
|
+
env: input.env,
|
|
712
|
+
detached: input.group,
|
|
713
|
+
stdin: "inherit",
|
|
714
|
+
stdout: "inherit",
|
|
715
|
+
stderr: "inherit"
|
|
716
|
+
});
|
|
717
|
+
child.watch(subprocess);
|
|
718
|
+
if (guarded) {
|
|
719
|
+
const fd = subprocess.stdio[3];
|
|
720
|
+
if (typeof fd !== "number") {
|
|
721
|
+
abortOwnerLossLaunch(subprocess.pid);
|
|
722
|
+
throw new Error("Owner-loss guard supplied no control socket");
|
|
723
|
+
}
|
|
724
|
+
attachOwnerLossControl(child, connectOwnerLossSocket(fd));
|
|
725
|
+
}
|
|
726
|
+
} catch (cause) {
|
|
727
|
+
if (!reportedSpawnFailure(cause))
|
|
728
|
+
throw cause;
|
|
729
|
+
child.fail(cause);
|
|
730
|
+
}
|
|
731
|
+
return child;
|
|
732
|
+
}
|
|
733
|
+
function spawnBunGuardTarget(executable, args) {
|
|
734
|
+
return Bun.spawn([executable, ...args], {
|
|
735
|
+
stdin: "inherit",
|
|
736
|
+
stdout: "inherit",
|
|
737
|
+
stderr: "inherit"
|
|
738
|
+
});
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
// src/process/owned-child.ts
|
|
402
742
|
var groups = new WeakSet;
|
|
403
743
|
function spawnOwnedCommand(input) {
|
|
404
|
-
const child =
|
|
744
|
+
const child = hostIsBun() ? spawnBunChild(input) : nodeChild(input);
|
|
745
|
+
if (input.group)
|
|
746
|
+
groups.add(child);
|
|
747
|
+
return child;
|
|
748
|
+
}
|
|
749
|
+
function nodeChild(input) {
|
|
750
|
+
if (input.ownerLoss === undefined)
|
|
751
|
+
return spawn(input.executable, [...input.args], {
|
|
752
|
+
cwd: input.cwd,
|
|
753
|
+
env: input.env,
|
|
754
|
+
detached: input.group,
|
|
755
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
756
|
+
});
|
|
757
|
+
const [executable, ...args] = ownerLossGuardCommand(input.ownerLoss, input.executable, input.args);
|
|
758
|
+
const child = spawn(executable, args, {
|
|
405
759
|
cwd: input.cwd,
|
|
406
760
|
env: input.env,
|
|
407
761
|
detached: input.group,
|
|
408
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
762
|
+
stdio: ["pipe", "pipe", "pipe", "pipe"]
|
|
409
763
|
});
|
|
410
|
-
|
|
411
|
-
|
|
764
|
+
const control = child.stdio[3];
|
|
765
|
+
if (!(control instanceof Duplex)) {
|
|
766
|
+
abortOwnerLossLaunch(child.pid);
|
|
767
|
+
throw new Error("Owner-loss guard supplied no control pipe");
|
|
768
|
+
}
|
|
769
|
+
attachOwnerLossControl(child, control);
|
|
412
770
|
return child;
|
|
413
771
|
}
|
|
414
772
|
var INHERITED_OUTPUT = {
|
|
@@ -445,12 +803,31 @@ class InheritedChild {
|
|
|
445
803
|
}
|
|
446
804
|
}
|
|
447
805
|
function spawnInheritedCommand(input) {
|
|
448
|
-
const
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
806
|
+
const observable = hostIsBun() ? spawnBunInheritedChild(input) : (() => {
|
|
807
|
+
if (input.ownerLoss === undefined)
|
|
808
|
+
return spawn(input.executable, [...input.args], {
|
|
809
|
+
cwd: input.cwd,
|
|
810
|
+
env: input.env,
|
|
811
|
+
detached: input.group,
|
|
812
|
+
stdio: "inherit"
|
|
813
|
+
});
|
|
814
|
+
const [executable, ...args] = ownerLossGuardCommand(input.ownerLoss, input.executable, input.args);
|
|
815
|
+
const spawned = spawn(executable, args, {
|
|
816
|
+
cwd: input.cwd,
|
|
817
|
+
env: input.env,
|
|
818
|
+
detached: input.group,
|
|
819
|
+
stdio: ["inherit", "inherit", "inherit", "pipe"]
|
|
820
|
+
});
|
|
821
|
+
const control = spawned.stdio[3];
|
|
822
|
+
if (!(control instanceof Duplex)) {
|
|
823
|
+
abortOwnerLossLaunch(spawned.pid);
|
|
824
|
+
throw new Error("Owner-loss guard supplied no control pipe");
|
|
825
|
+
}
|
|
826
|
+
attachOwnerLossControl(spawned, control);
|
|
827
|
+
return spawned;
|
|
828
|
+
})();
|
|
829
|
+
const child = new InheritedChild(observable);
|
|
830
|
+
transferOwnerLossControl(observable, child);
|
|
454
831
|
if (input.group)
|
|
455
832
|
groups.add(child);
|
|
456
833
|
return child;
|
|
@@ -459,8 +836,153 @@ function ownsCommandGroup(child) {
|
|
|
459
836
|
return groups.has(child);
|
|
460
837
|
}
|
|
461
838
|
|
|
839
|
+
// src/process/owner-loss-guard.ts
|
|
840
|
+
import { spawn as spawn2 } from "node:child_process";
|
|
841
|
+
import { Socket } from "node:net";
|
|
842
|
+
import { fileURLToPath } from "node:url";
|
|
843
|
+
var FORWARDED_SIGNALS = [
|
|
844
|
+
"SIGTERM",
|
|
845
|
+
"SIGINT",
|
|
846
|
+
"SIGHUP",
|
|
847
|
+
"SIGQUIT",
|
|
848
|
+
"SIGUSR1",
|
|
849
|
+
"SIGUSR2"
|
|
850
|
+
];
|
|
851
|
+
function causeMessage(cause) {
|
|
852
|
+
const message = cause instanceof Error ? cause.message : String(cause);
|
|
853
|
+
if (typeof cause === "object" && cause !== null && "code" in cause && typeof cause.code === "string")
|
|
854
|
+
return { message, code: cause.code };
|
|
855
|
+
return { message };
|
|
856
|
+
}
|
|
857
|
+
function guardMessage(control, message, written) {
|
|
858
|
+
control.write(`${JSON.stringify(message)}
|
|
859
|
+
`, written);
|
|
860
|
+
}
|
|
861
|
+
function mirrorTargetExit(control, code, signal) {
|
|
862
|
+
control.destroy();
|
|
863
|
+
if (signal !== null)
|
|
864
|
+
process.kill(process.pid, signal);
|
|
865
|
+
else
|
|
866
|
+
process.exitCode = code ?? 1;
|
|
867
|
+
}
|
|
868
|
+
function refuseTarget(control, cause) {
|
|
869
|
+
const detail = causeMessage(cause);
|
|
870
|
+
control.end(`${JSON.stringify({ kind: "unavailable", ...detail })}
|
|
871
|
+
`, () => {
|
|
872
|
+
process.exitCode = 127;
|
|
873
|
+
});
|
|
874
|
+
}
|
|
875
|
+
function spawnNodeTarget(executable, args, onStarted, onExit, onUnavailable) {
|
|
876
|
+
let child;
|
|
877
|
+
try {
|
|
878
|
+
child = spawn2(executable, [...args], { stdio: "inherit" });
|
|
879
|
+
} catch (cause) {
|
|
880
|
+
onUnavailable(cause);
|
|
881
|
+
return;
|
|
882
|
+
}
|
|
883
|
+
child.once("spawn", () => {
|
|
884
|
+
const pid = child.pid;
|
|
885
|
+
if (pid === undefined)
|
|
886
|
+
onUnavailable(new Error("Command started without a pid"));
|
|
887
|
+
else
|
|
888
|
+
onStarted(pid);
|
|
889
|
+
});
|
|
890
|
+
child.once("error", onUnavailable);
|
|
891
|
+
child.once("exit", onExit);
|
|
892
|
+
}
|
|
893
|
+
function spawnBunTarget(executable, args, onStarted, onExit, onUnavailable) {
|
|
894
|
+
let child;
|
|
895
|
+
try {
|
|
896
|
+
child = spawnBunGuardTarget(executable, args);
|
|
897
|
+
} catch (cause) {
|
|
898
|
+
onUnavailable(cause);
|
|
899
|
+
return;
|
|
900
|
+
}
|
|
901
|
+
onStarted(child.pid);
|
|
902
|
+
child.exited.then(() => onExit(child.exitCode, child.signalCode));
|
|
903
|
+
}
|
|
904
|
+
function runOwnerLossGuard(signalTarget, executable, args) {
|
|
905
|
+
const bun = hostIsBun();
|
|
906
|
+
const control = bun ? connectOwnerLossSocket(3) : new Socket({ fd: 3, readable: true, writable: true });
|
|
907
|
+
let armed = true;
|
|
908
|
+
const ownerLost = () => {
|
|
909
|
+
if (!armed)
|
|
910
|
+
return;
|
|
911
|
+
process.kill(-process.pid, "SIGKILL");
|
|
912
|
+
};
|
|
913
|
+
control.once("end", ownerLost);
|
|
914
|
+
control.once("close", ownerLost);
|
|
915
|
+
control.once("error", ownerLost);
|
|
916
|
+
if (signalTarget !== "group" && signalTarget !== "leader" || executable === undefined) {
|
|
917
|
+
armed = false;
|
|
918
|
+
refuseTarget(control, new Error("Owner-loss guard received an invalid invocation"));
|
|
919
|
+
return;
|
|
920
|
+
}
|
|
921
|
+
let targetPid;
|
|
922
|
+
let handshakeWritten = false;
|
|
923
|
+
let pendingTargetExit;
|
|
924
|
+
const forwarders = new Map;
|
|
925
|
+
if (signalTarget === "leader")
|
|
926
|
+
for (const signal of FORWARDED_SIGNALS) {
|
|
927
|
+
const forward = () => {
|
|
928
|
+
if (targetPid === undefined)
|
|
929
|
+
return;
|
|
930
|
+
try {
|
|
931
|
+
process.kill(targetPid, signal);
|
|
932
|
+
} catch (error) {
|
|
933
|
+
if (!(error instanceof Error && ("code" in error) && error.code === "ESRCH"))
|
|
934
|
+
throw error;
|
|
935
|
+
}
|
|
936
|
+
};
|
|
937
|
+
forwarders.set(signal, forward);
|
|
938
|
+
process.on(signal, forward);
|
|
939
|
+
}
|
|
940
|
+
const removeForwarders = () => {
|
|
941
|
+
for (const [signal, forward] of forwarders)
|
|
942
|
+
process.removeListener(signal, forward);
|
|
943
|
+
forwarders.clear();
|
|
944
|
+
};
|
|
945
|
+
const targetStarted = (pid) => {
|
|
946
|
+
targetPid = pid;
|
|
947
|
+
guardMessage(control, { kind: "started", pid }, () => {
|
|
948
|
+
handshakeWritten = true;
|
|
949
|
+
const pending = pendingTargetExit;
|
|
950
|
+
if (pending !== undefined) {
|
|
951
|
+
pendingTargetExit = undefined;
|
|
952
|
+
targetExit(pending.code, pending.signal);
|
|
953
|
+
}
|
|
954
|
+
});
|
|
955
|
+
};
|
|
956
|
+
const targetExit = (code, signal) => {
|
|
957
|
+
if (!handshakeWritten) {
|
|
958
|
+
pendingTargetExit = { code, signal };
|
|
959
|
+
return;
|
|
960
|
+
}
|
|
961
|
+
armed = false;
|
|
962
|
+
removeForwarders();
|
|
963
|
+
mirrorTargetExit(control, code, signal);
|
|
964
|
+
};
|
|
965
|
+
const targetUnavailable = (cause) => {
|
|
966
|
+
armed = false;
|
|
967
|
+
removeForwarders();
|
|
968
|
+
refuseTarget(control, cause);
|
|
969
|
+
};
|
|
970
|
+
if (!bun)
|
|
971
|
+
spawnNodeTarget(executable, args, targetStarted, targetExit, targetUnavailable);
|
|
972
|
+
else
|
|
973
|
+
spawnBunTarget(executable, args, targetStarted, targetExit, targetUnavailable);
|
|
974
|
+
}
|
|
975
|
+
function ownerLossGuardEntry() {
|
|
976
|
+
return fileURLToPath(import.meta.url);
|
|
977
|
+
}
|
|
978
|
+
if (process.argv[2] === OWNER_LOSS_GUARD_FLAG) {
|
|
979
|
+
const entry = process.argv[1];
|
|
980
|
+
if (entry !== undefined && fileURLToPath(import.meta.url) === entry)
|
|
981
|
+
runOwnerLossGuard(process.argv[3], process.argv[4], process.argv.slice(5));
|
|
982
|
+
}
|
|
983
|
+
|
|
462
984
|
// src/process/transport.ts
|
|
463
|
-
import { Readable, Writable } from "node:stream";
|
|
985
|
+
import { Readable as Readable2, Writable as Writable2 } from "node:stream";
|
|
464
986
|
function createCommandTransport(child) {
|
|
465
987
|
const leader = Promise.withResolvers();
|
|
466
988
|
const closed = Promise.withResolvers();
|
|
@@ -517,17 +1039,17 @@ function createCommandTransport(child) {
|
|
|
517
1039
|
stderr = child.stderr;
|
|
518
1040
|
destructors.set("stderr", () => stderr?.destroy());
|
|
519
1041
|
const input = "stdin" in child ? child.stdin : undefined;
|
|
520
|
-
if (input instanceof
|
|
1042
|
+
if (input instanceof Writable2) {
|
|
521
1043
|
stdin = input;
|
|
522
1044
|
pendingHandles.add("stdin");
|
|
523
1045
|
destructors.set("stdin", () => input.destroy());
|
|
524
1046
|
pipe("stdin", input);
|
|
525
1047
|
}
|
|
526
|
-
if (stdout instanceof
|
|
1048
|
+
if (stdout instanceof Readable2)
|
|
527
1049
|
pipe("stdout", stdout);
|
|
528
|
-
if (stderr instanceof
|
|
1050
|
+
if (stderr instanceof Readable2)
|
|
529
1051
|
pipe("stderr", stderr);
|
|
530
|
-
if (stdout instanceof
|
|
1052
|
+
if (stdout instanceof Readable2 && stderr instanceof Readable2)
|
|
531
1053
|
release("launcher");
|
|
532
1054
|
return { stdout, stderr };
|
|
533
1055
|
},
|
|
@@ -545,7 +1067,7 @@ function createCommandTransport(child) {
|
|
|
545
1067
|
child.stdout.destroy();
|
|
546
1068
|
else if (name === "stderr")
|
|
547
1069
|
child.stderr.destroy();
|
|
548
|
-
else if ("stdin" in child && child.stdin instanceof
|
|
1070
|
+
else if ("stdin" in child && child.stdin instanceof Writable2)
|
|
549
1071
|
child.stdin.destroy();
|
|
550
1072
|
} catch (cause) {
|
|
551
1073
|
failures.push(cause);
|
|
@@ -578,7 +1100,7 @@ function nativeCommandOwner(child) {
|
|
|
578
1100
|
return owners.get(child);
|
|
579
1101
|
}
|
|
580
1102
|
function outputStream(source, closed, maxBufferedBytes) {
|
|
581
|
-
if (source instanceof
|
|
1103
|
+
if (source instanceof Readable3)
|
|
582
1104
|
return {
|
|
583
1105
|
bytes: source,
|
|
584
1106
|
destroy() {
|
|
@@ -661,7 +1183,8 @@ function launchNativeCommand({
|
|
|
661
1183
|
args: options.args,
|
|
662
1184
|
cwd: options.cwd,
|
|
663
1185
|
env: options.envPolicy === "ambient" ? { ...process.env, ...options.env } : options.env ?? {},
|
|
664
|
-
group: options.group === "own" && (driver?.group ?? true)
|
|
1186
|
+
group: options.group === "own" && (driver?.group ?? true),
|
|
1187
|
+
ownerLoss: options.ownerLoss === "terminate" ? { entry: ownerLossGuardEntry(), signalTarget: options.stop.target } : undefined
|
|
665
1188
|
};
|
|
666
1189
|
let inherited;
|
|
667
1190
|
if (!launch) {
|
|
@@ -690,10 +1213,12 @@ function launchNativeCommand({
|
|
|
690
1213
|
...transport,
|
|
691
1214
|
stdout: stdout.bytes,
|
|
692
1215
|
stderr: stderr.bytes,
|
|
693
|
-
start() {
|
|
1216
|
+
async start() {
|
|
694
1217
|
if (native)
|
|
695
1218
|
onSpawn?.(native);
|
|
1219
|
+
const pid = await awaitCommandStart(child);
|
|
696
1220
|
stdin?.end(options.stdin);
|
|
1221
|
+
return pid;
|
|
697
1222
|
}
|
|
698
1223
|
};
|
|
699
1224
|
}
|
|
@@ -852,6 +1377,10 @@ function startNativeCommand(input, onSpawn, driver) {
|
|
|
852
1377
|
killOn?.throwIfAborted();
|
|
853
1378
|
if (process.platform === "win32" && driver?.group !== false)
|
|
854
1379
|
throw new NativeCommandError2("COMMAND_UNAVAILABLE", "POSIX process groups are required");
|
|
1380
|
+
if (options.ownerLoss === "terminate" && !ownerLossAvailable(process.platform))
|
|
1381
|
+
throw new NativeCommandError2("COMMAND_UNAVAILABLE", "ownerLoss: 'terminate' is available on Linux and Darwin");
|
|
1382
|
+
if (options.ownerLoss === "terminate" && (driver?.launch !== undefined || driver?.group === false))
|
|
1383
|
+
throw new NativeCommandError2("COMMAND_UNAVAILABLE", "ownerLoss: 'terminate' requires the package-owned process-group launcher");
|
|
855
1384
|
const { cleanupTimeoutMs, timeoutMs } = nativeCommandBudgets(options, driver);
|
|
856
1385
|
const controller = new AbortController;
|
|
857
1386
|
const abort = () => controller.abort(options.signal?.reason);
|
|
@@ -957,8 +1486,7 @@ function startNativeCommand(input, onSpawn, driver) {
|
|
|
957
1486
|
};
|
|
958
1487
|
const run = (async () => {
|
|
959
1488
|
try {
|
|
960
|
-
launched.start();
|
|
961
|
-
const pid = acquired?.child.pid;
|
|
1489
|
+
const pid = await raceAbort(launched.start(), controller.signal);
|
|
962
1490
|
if (pid !== undefined)
|
|
963
1491
|
options.onLeaderStarted?.({ pid });
|
|
964
1492
|
const drains = Promise.all([
|