stitchkit 0.108.1 → 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 +13 -0
- 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-epg514ys.js → index-k0kzhdw9.js} +374 -15
- package/dist/{index-e35py42g.js → index-vyaehh67.js} +1 -1
- package/dist/process/bun-child.d.ts +10 -0
- package/dist/process/bun-child.d.ts.map +1 -1
- 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 +1 -1
- package/dist/process/launch.d.ts.map +1 -1
- package/dist/process/owned-child.d.ts +3 -0
- 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
package/CHANGELOG.md
CHANGED
|
@@ -15,6 +15,19 @@ additive**; the first breaking change landed in 0.10.0. Grep the file for
|
|
|
15
15
|
|
|
16
16
|
## [Unreleased]
|
|
17
17
|
|
|
18
|
+
## [0.108.2] - 2026-10-09
|
|
19
|
+
|
|
20
|
+
### Added
|
|
21
|
+
|
|
22
|
+
- `stitchkit/process` — `runNativeCommand` takes `ownerLoss: 'terminate'`: on Linux and Darwin under
|
|
23
|
+
Bun or Node, a package-owned per-command guard binds the command's process group to a private
|
|
24
|
+
inherited kernel channel. If the JavaScript caller is killed or crashes, channel EOF makes the
|
|
25
|
+
guard KILL its own group without polling, `finally` or PID reclamation. The option requires
|
|
26
|
+
`group: 'own'`; unsupported platforms refuse with `COMMAND_UNAVAILABLE`. It is opt-in because each
|
|
27
|
+
guarded call uses one extra short-lived process and descriptor. Output, stdin, target exit status,
|
|
28
|
+
deadlines and cancellation retain the existing contract; `onLeaderStarted` reports the guard that
|
|
29
|
+
leads the owned group. → ADR 0255
|
|
30
|
+
|
|
18
31
|
## [0.108.1] - 2026-10-07
|
|
19
32
|
|
|
20
33
|
### Fixed
|
|
@@ -6,9 +6,9 @@ import {
|
|
|
6
6
|
AgentCodingToolAuthorizationSchema2,
|
|
7
7
|
AgentCodingToolPathAuthorizationSchema2,
|
|
8
8
|
createAgentCodingTools2
|
|
9
|
-
} from "../../index-
|
|
9
|
+
} from "../../index-vyaehh67.js";
|
|
10
10
|
import"../../index-2kbysk9s.js";
|
|
11
|
-
import"../../index-
|
|
11
|
+
import"../../index-k0kzhdw9.js";
|
|
12
12
|
import"../../index-8ks9b85e.js";
|
|
13
13
|
import"../../index-zrtrjkbf.js";
|
|
14
14
|
import"../../index-636mggad.js";
|
|
@@ -8,7 +8,7 @@ import"../../index-q6crhe1n.js";
|
|
|
8
8
|
import"../../index-fynr93w4.js";
|
|
9
9
|
import {
|
|
10
10
|
createAgentCodingTools2
|
|
11
|
-
} from "../../index-
|
|
11
|
+
} from "../../index-vyaehh67.js";
|
|
12
12
|
import"../../index-2kbysk9s.js";
|
|
13
13
|
import {
|
|
14
14
|
COMMAND_CLEANUP_TIMEOUT_MS,
|
|
@@ -17,7 +17,7 @@ import {
|
|
|
17
17
|
nativeCommandOwner,
|
|
18
18
|
terminateOwnedGroup,
|
|
19
19
|
startNativeCommand
|
|
20
|
-
} from "../../index-
|
|
20
|
+
} from "../../index-k0kzhdw9.js";
|
|
21
21
|
import"../../index-zrtrjkbf.js";
|
|
22
22
|
import {
|
|
23
23
|
MAX_TIMER_MS
|
|
@@ -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",
|
|
@@ -399,10 +406,137 @@ 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";
|
|
402
410
|
|
|
403
411
|
// src/process/bun-child.ts
|
|
404
412
|
import { EventEmitter } from "node:events";
|
|
405
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
|
|
406
540
|
function hostIsBun() {
|
|
407
541
|
return typeof Bun !== "undefined";
|
|
408
542
|
}
|
|
@@ -525,7 +659,15 @@ function sinkWritable(sink) {
|
|
|
525
659
|
function spawnBunChild(input) {
|
|
526
660
|
const { BunChild } = children();
|
|
527
661
|
try {
|
|
528
|
-
const
|
|
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, {
|
|
529
671
|
cwd: input.cwd,
|
|
530
672
|
env: input.env,
|
|
531
673
|
detached: input.group,
|
|
@@ -535,6 +677,14 @@ function spawnBunChild(input) {
|
|
|
535
677
|
});
|
|
536
678
|
const child = new BunChild(sinkWritable(subprocess.stdin), readableOf(subprocess.stdout), readableOf(subprocess.stderr));
|
|
537
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
|
+
}
|
|
538
688
|
return child;
|
|
539
689
|
} catch (cause) {
|
|
540
690
|
if (!reportedSpawnFailure(cause))
|
|
@@ -548,14 +698,31 @@ function spawnBunInheritedChild(input) {
|
|
|
548
698
|
const { BunProcess } = children();
|
|
549
699
|
const child = new BunProcess([]);
|
|
550
700
|
try {
|
|
551
|
-
|
|
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, {
|
|
552
710
|
cwd: input.cwd,
|
|
553
711
|
env: input.env,
|
|
554
712
|
detached: input.group,
|
|
555
713
|
stdin: "inherit",
|
|
556
714
|
stdout: "inherit",
|
|
557
715
|
stderr: "inherit"
|
|
558
|
-
})
|
|
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
|
+
}
|
|
559
726
|
} catch (cause) {
|
|
560
727
|
if (!reportedSpawnFailure(cause))
|
|
561
728
|
throw cause;
|
|
@@ -563,6 +730,13 @@ function spawnBunInheritedChild(input) {
|
|
|
563
730
|
}
|
|
564
731
|
return child;
|
|
565
732
|
}
|
|
733
|
+
function spawnBunGuardTarget(executable, args) {
|
|
734
|
+
return Bun.spawn([executable, ...args], {
|
|
735
|
+
stdin: "inherit",
|
|
736
|
+
stdout: "inherit",
|
|
737
|
+
stderr: "inherit"
|
|
738
|
+
});
|
|
739
|
+
}
|
|
566
740
|
|
|
567
741
|
// src/process/owned-child.ts
|
|
568
742
|
var groups = new WeakSet;
|
|
@@ -573,12 +747,27 @@ function spawnOwnedCommand(input) {
|
|
|
573
747
|
return child;
|
|
574
748
|
}
|
|
575
749
|
function nodeChild(input) {
|
|
576
|
-
|
|
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, {
|
|
577
759
|
cwd: input.cwd,
|
|
578
760
|
env: input.env,
|
|
579
761
|
detached: input.group,
|
|
580
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
762
|
+
stdio: ["pipe", "pipe", "pipe", "pipe"]
|
|
581
763
|
});
|
|
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);
|
|
770
|
+
return child;
|
|
582
771
|
}
|
|
583
772
|
var INHERITED_OUTPUT = {
|
|
584
773
|
destroyed: true,
|
|
@@ -614,12 +803,31 @@ class InheritedChild {
|
|
|
614
803
|
}
|
|
615
804
|
}
|
|
616
805
|
function spawnInheritedCommand(input) {
|
|
617
|
-
const
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
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);
|
|
623
831
|
if (input.group)
|
|
624
832
|
groups.add(child);
|
|
625
833
|
return child;
|
|
@@ -628,6 +836,151 @@ function ownsCommandGroup(child) {
|
|
|
628
836
|
return groups.has(child);
|
|
629
837
|
}
|
|
630
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
|
+
|
|
631
984
|
// src/process/transport.ts
|
|
632
985
|
import { Readable as Readable2, Writable as Writable2 } from "node:stream";
|
|
633
986
|
function createCommandTransport(child) {
|
|
@@ -830,7 +1183,8 @@ function launchNativeCommand({
|
|
|
830
1183
|
args: options.args,
|
|
831
1184
|
cwd: options.cwd,
|
|
832
1185
|
env: options.envPolicy === "ambient" ? { ...process.env, ...options.env } : options.env ?? {},
|
|
833
|
-
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
|
|
834
1188
|
};
|
|
835
1189
|
let inherited;
|
|
836
1190
|
if (!launch) {
|
|
@@ -859,10 +1213,12 @@ function launchNativeCommand({
|
|
|
859
1213
|
...transport,
|
|
860
1214
|
stdout: stdout.bytes,
|
|
861
1215
|
stderr: stderr.bytes,
|
|
862
|
-
start() {
|
|
1216
|
+
async start() {
|
|
863
1217
|
if (native)
|
|
864
1218
|
onSpawn?.(native);
|
|
1219
|
+
const pid = await awaitCommandStart(child);
|
|
865
1220
|
stdin?.end(options.stdin);
|
|
1221
|
+
return pid;
|
|
866
1222
|
}
|
|
867
1223
|
};
|
|
868
1224
|
}
|
|
@@ -1021,6 +1377,10 @@ function startNativeCommand(input, onSpawn, driver) {
|
|
|
1021
1377
|
killOn?.throwIfAborted();
|
|
1022
1378
|
if (process.platform === "win32" && driver?.group !== false)
|
|
1023
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");
|
|
1024
1384
|
const { cleanupTimeoutMs, timeoutMs } = nativeCommandBudgets(options, driver);
|
|
1025
1385
|
const controller = new AbortController;
|
|
1026
1386
|
const abort = () => controller.abort(options.signal?.reason);
|
|
@@ -1126,8 +1486,7 @@ function startNativeCommand(input, onSpawn, driver) {
|
|
|
1126
1486
|
};
|
|
1127
1487
|
const run = (async () => {
|
|
1128
1488
|
try {
|
|
1129
|
-
launched.start();
|
|
1130
|
-
const pid = acquired?.child.pid;
|
|
1489
|
+
const pid = await raceAbort(launched.start(), controller.signal);
|
|
1131
1490
|
if (pid !== undefined)
|
|
1132
1491
|
options.onLeaderStarted?.({ pid });
|
|
1133
1492
|
const drains = Promise.all([
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { EventEmitter } from 'node:events';
|
|
2
2
|
import { Readable, Writable } from 'node:stream';
|
|
3
|
+
import { type OwnerLossGuardInvocation } from './owner-loss-protocol.js';
|
|
3
4
|
/** What a spawned command exposes to its owner: the subset of a Node child the package uses. */
|
|
4
5
|
export interface OwnedChild extends EventEmitter {
|
|
5
6
|
readonly pid?: number;
|
|
@@ -23,6 +24,7 @@ interface BunSpawnInput {
|
|
|
23
24
|
cwd?: string;
|
|
24
25
|
env?: NodeJS.ProcessEnv;
|
|
25
26
|
group: boolean;
|
|
27
|
+
ownerLoss?: OwnerLossGuardInvocation;
|
|
26
28
|
}
|
|
27
29
|
/**
|
|
28
30
|
* Whether the host is Bun. Bun's `node:child_process` can lose a child's `exit` event under
|
|
@@ -34,5 +36,13 @@ export declare function hostIsBun(): boolean;
|
|
|
34
36
|
export declare function spawnBunChild(input: BunSpawnInput): OwnedChild;
|
|
35
37
|
/** `spawnInheritedCommand` for Bun: a command with the caller's stdio. */
|
|
36
38
|
export declare function spawnBunInheritedChild(input: BunSpawnInput): ObservableChild;
|
|
39
|
+
export interface BunGuardTarget {
|
|
40
|
+
readonly pid: number;
|
|
41
|
+
readonly exitCode: number | null;
|
|
42
|
+
readonly signalCode: string | number | null;
|
|
43
|
+
readonly exited: Promise<unknown>;
|
|
44
|
+
}
|
|
45
|
+
/** Bun-native target observation for the standalone owner-loss guard. */
|
|
46
|
+
export declare function spawnBunGuardTarget(executable: string, args: readonly string[]): BunGuardTarget;
|
|
37
47
|
export {};
|
|
38
48
|
//# sourceMappingURL=bun-child.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bun-child.d.ts","sourceRoot":"","sources":["../../src/process/bun-child.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3C,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAEjD,gGAAgG;AAChG,MAAM,WAAW,UAAW,SAAQ,YAAY;IAC9C,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC,OAAO,GAAG,IAAI,CAAC;IAC3C,QAAQ,CAAC,KAAK,EAAE,QAAQ,CAAC;IACzB,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC;IAC1B,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC;IAC1B,IAAI,CAAC,MAAM,EAAE,MAAM,CAAC,OAAO,GAAG,OAAO,CAAC;CACvC;AAED,oEAAoE;AACpE,MAAM,WAAW,eAAgB,SAAQ,YAAY;IACnD,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC,OAAO,GAAG,IAAI,CAAC;IAC3C,IAAI,CAAC,MAAM,EAAE,MAAM,CAAC,OAAO,GAAG,OAAO,CAAC;CACvC;AAED,UAAU,aAAa;IACrB,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,SAAS,MAAM,EAAE,CAAC;IACxB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB,KAAK,EAAE,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"bun-child.d.ts","sourceRoot":"","sources":["../../src/process/bun-child.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3C,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAEjD,OAAO,EAAE,KAAK,wBAAwB,EAAyB,MAAM,uBAAuB,CAAC;AAG7F,gGAAgG;AAChG,MAAM,WAAW,UAAW,SAAQ,YAAY;IAC9C,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC,OAAO,GAAG,IAAI,CAAC;IAC3C,QAAQ,CAAC,KAAK,EAAE,QAAQ,CAAC;IACzB,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC;IAC1B,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC;IAC1B,IAAI,CAAC,MAAM,EAAE,MAAM,CAAC,OAAO,GAAG,OAAO,CAAC;CACvC;AAED,oEAAoE;AACpE,MAAM,WAAW,eAAgB,SAAQ,YAAY;IACnD,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC,OAAO,GAAG,IAAI,CAAC;IAC3C,IAAI,CAAC,MAAM,EAAE,MAAM,CAAC,OAAO,GAAG,OAAO,CAAC;CACvC;AAED,UAAU,aAAa;IACrB,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,SAAS,MAAM,EAAE,CAAC;IACxB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB,KAAK,EAAE,OAAO,CAAC;IACf,SAAS,CAAC,EAAE,wBAAwB,CAAC;CACtC;AAED;;;;GAIG;AACH,wBAAgB,SAAS,IAAI,OAAO,CAEnC;AAqKD,+DAA+D;AAC/D,wBAAgB,aAAa,CAAC,KAAK,EAAE,aAAa,GAAG,UAAU,CAgD9D;AAED,0EAA0E;AAC1E,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,aAAa,GAAG,eAAe,CAsC5E;AAED,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC,QAAQ,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC;IAC5C,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC,OAAO,CAAC,CAAC;CACnC;AAED,yEAAyE;AACzE,wBAAgB,mBAAmB,CACjC,UAAU,EAAE,MAAM,EAClB,IAAI,EAAE,SAAS,MAAM,EAAE,GACtB,cAAc,CAMhB"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"command-owner.d.ts","sourceRoot":"","sources":["../../src/process/command-owner.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAG9C,OAAO,EAEL,KAAK,oBAAoB,EAI1B,MAAM,YAAY,CAAC;AAEpB,OAAO,EAEL,KAAK,yBAAyB,EAG/B,MAAM,UAAU,CAAC;
|
|
1
|
+
{"version":3,"file":"command-owner.d.ts","sourceRoot":"","sources":["../../src/process/command-owner.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAG9C,OAAO,EAEL,KAAK,oBAAoB,EAI1B,MAAM,YAAY,CAAC;AAEpB,OAAO,EAEL,KAAK,yBAAyB,EAG/B,MAAM,UAAU,CAAC;AAQlB,wFAAwF;AACxF,wBAAgB,kBAAkB,CAChC,KAAK,EAAE,oBAAoB,EAC3B,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,UAAU,KAAK,IAAI,EACrC,MAAM,CAAC,EAAE,yBAAyB;;;;;;;;;;EA8KnC"}
|
|
@@ -77,6 +77,9 @@ export declare const NativeCommandOptionsSchema: z.ZodPipe<z.ZodObject<{
|
|
|
77
77
|
caller: "caller";
|
|
78
78
|
own: "own";
|
|
79
79
|
}>>;
|
|
80
|
+
ownerLoss: z.ZodOptional<z.ZodEnum<{
|
|
81
|
+
terminate: "terminate";
|
|
82
|
+
}>>;
|
|
80
83
|
stdio: z.ZodDefault<z.ZodEnum<{
|
|
81
84
|
inherit: "inherit";
|
|
82
85
|
pipe: "pipe";
|
|
@@ -121,6 +124,7 @@ export declare const NativeCommandOptionsSchema: z.ZodPipe<z.ZodObject<{
|
|
|
121
124
|
}) => void) | undefined;
|
|
122
125
|
drainTimeoutMs?: number | undefined;
|
|
123
126
|
group: "caller" | "own";
|
|
127
|
+
ownerLoss?: "terminate" | undefined;
|
|
124
128
|
stdio: "inherit" | "pipe";
|
|
125
129
|
cleanupTimeoutMs: number;
|
|
126
130
|
stop: {
|
|
@@ -149,6 +153,7 @@ export declare const NativeCommandOptionsSchema: z.ZodPipe<z.ZodObject<{
|
|
|
149
153
|
}) => void) | undefined;
|
|
150
154
|
drainTimeoutMs?: number | undefined;
|
|
151
155
|
group: "caller" | "own";
|
|
156
|
+
ownerLoss?: "terminate" | undefined;
|
|
152
157
|
stdio: "inherit" | "pipe";
|
|
153
158
|
stop?: {
|
|
154
159
|
target: "group" | "leader";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"contract.d.ts","sourceRoot":"","sources":["../../src/process/contract.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB,QAAA,MAAM,6BAA6B;;;;;;;;;;;;2BAajC,CAAC;AACH;;;;;;GAMG;AACH,MAAM,MAAM,uBAAuB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,6BAA6B,CAAC,CAAC;AAEpF;;GAEG;AACH,eAAO,MAAM,0BAA0B,OAAO,CAAC;AAE/C,gGAAgG;AAChG,eAAO,MAAM,yBAAyB,UAAY,CAAC;AAEnD,QAAA,MAAM,6BAA6B;;;;;;;;;;;;;;;kBAOjC,CAAC;AACH;;;;;GAKG;AACH,MAAM,MAAM,uBAAuB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,6BAA6B,CAAC,CAAC;AACpF,MAAM,MAAM,6BAA6B,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,6BAA6B,CAAC,CAAC;AAiC3F,eAAO,MAAM,0BAA0B;;;;;;;;;;;;;;;gDAqBpB,UAAU,WACR,QAAQ,GAAG,QAAQ,UACpB,WAAW,KAChB,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;uDAKX,uBAAuB,UAAU,WAAW,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;uDAMrE;QAAE,GAAG,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI
|
|
1
|
+
{"version":3,"file":"contract.d.ts","sourceRoot":"","sources":["../../src/process/contract.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB,QAAA,MAAM,6BAA6B;;;;;;;;;;;;2BAajC,CAAC;AACH;;;;;;GAMG;AACH,MAAM,MAAM,uBAAuB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,6BAA6B,CAAC,CAAC;AAEpF;;GAEG;AACH,eAAO,MAAM,0BAA0B,OAAO,CAAC;AAE/C,gGAAgG;AAChG,eAAO,MAAM,yBAAyB,UAAY,CAAC;AAEnD,QAAA,MAAM,6BAA6B;;;;;;;;;;;;;;;kBAOjC,CAAC;AACH;;;;;GAKG;AACH,MAAM,MAAM,uBAAuB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,6BAA6B,CAAC,CAAC;AACpF,MAAM,MAAM,6BAA6B,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,6BAA6B,CAAC,CAAC;AAiC3F,eAAO,MAAM,0BAA0B;;;;;;;;;;;;;;;gDAqBpB,UAAU,WACR,QAAQ,GAAG,QAAQ,UACpB,WAAW,KAChB,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;uDAKX,uBAAuB,UAAU,WAAW,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;uDAMrE;QAAE,GAAG,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4F3C,CAAC;AACN;;;GAGG;AACH,MAAM,MAAM,oBAAoB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,0BAA0B,CAAC,CAAC;AAC9E,MAAM,MAAM,0BAA0B,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,0BAA0B,CAAC,CAAC;AACrF,QAAA,MAAM,yBAAyB;;;;;;iBAU7B,CAAC;AACH;;;GAGG;AACH,MAAM,MAAM,mBAAmB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,yBAAyB,CAAC,CAAC;AAE5E,QAAA,MAAM,8BAA8B;;;EAAwC,CAAC;AAE7E;;;;;;GAMG;AACH,qBAAa,kBAAmB,SAAQ,KAAK;aAezB,IAAI,EAAE,eAAe,GAAG,qBAAqB,GAAG,iBAAiB;IAdnF,2GAA2G;IAC3G,SAAgB,MAAM,EAAE,CAAC,CAAC,KAAK,CAAC,OAAO,8BAA8B,CAAC,GAAG,SAAS,CAAC;IAEnF,YACE,IAAI,EAAE,eAAe,EACrB,OAAO,EAAE,MAAM,EACf,OAAO,CAAC,EAAE,YAAY,GAAG;QAAE,MAAM,CAAC,EAAE,CAAC,CAAC,KAAK,CAAC,OAAO,8BAA8B,CAAC,CAAA;KAAE,EACpF;IACF,YACE,IAAI,EAAE,eAAe,GAAG,qBAAqB,GAAG,iBAAiB,EACjE,OAAO,EAAE,MAAM,EACf,OAAO,CAAC,EAAE,YAAY,GAAG;QAAE,MAAM,CAAC,EAAE,KAAK,CAAA;KAAE,EAC3C;CAgBH"}
|
package/dist/process/launch.d.ts
CHANGED
|
@@ -56,7 +56,7 @@ export declare function launchNativeCommand({ options, driver, onSpawn, onFailur
|
|
|
56
56
|
}): {
|
|
57
57
|
stdout: AsyncIterable<Uint8Array<ArrayBufferLike>>;
|
|
58
58
|
stderr: AsyncIterable<Uint8Array<ArrayBufferLike>>;
|
|
59
|
-
start():
|
|
59
|
+
start(): Promise<number | undefined>;
|
|
60
60
|
child: NativeCommandLaunchedProcess;
|
|
61
61
|
leader: Promise<{
|
|
62
62
|
kind: "exit";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"launch.d.ts","sourceRoot":"","sources":["../../src/process/launch.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAEvC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAC9C,OAAO,EAAsB,KAAK,0BAA0B,EAAE,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"launch.d.ts","sourceRoot":"","sources":["../../src/process/launch.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAEvC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAC9C,OAAO,EAAsB,KAAK,0BAA0B,EAAE,MAAM,YAAY,CAAC;AAIjF,OAAO,EAA0B,KAAK,sBAAsB,EAAE,MAAM,aAAa,CAAC;AAElF,wFAAwF;AACxF,MAAM,WAAW,4BAA4B;IAC3C,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC,QAAQ,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IACnC,QAAQ,CAAC,MAAM,EAAE,2BAA2B,CAAC;IAC7C,QAAQ,CAAC,MAAM,EAAE,2BAA2B,CAAC;IAC7C,IAAI,CAAC,MAAM,EAAE,SAAS,GAAG,OAAO,CAAC;IACjC,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,GAAG,OAAO,CAAC;IAC9D,EAAE,CACA,KAAK,EAAE,MAAM,GAAG,OAAO,EACvB,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,GAC7D,OAAO,CAAC;CACZ;AAED,MAAM,WAAW,2BAA2B;IAC1C,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;IAC5B,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,KAAK,EAAE,UAAU,KAAK,IAAI,GAAG,OAAO,CAAC;IAClE,OAAO,IAAI,OAAO,CAAC;CACpB;AAED,iGAAiG;AACjG,MAAM,WAAW,yBAAyB;IACxC,MAAM,CAAC,EAAE,MAAM,4BAA4B,CAAC;IAC5C,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,8EAA8E;AAC9E,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,0BAA0B,EACnC,MAAM,EAAE,yBAAyB,GAAG,SAAS;;;EAY9C;AAED,UAAU,kBAAkB;IAC1B,MAAM,EAAE,OAAO,CAAC,OAAO,CAAC,CAAC;IACzB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACtB,SAAS,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CAC5B;AAGD,uFAAuF;AACvF,wBAAgB,0BAA0B,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,kBAAkB,GAAG,IAAI,CAEzF;AAED,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,GAAG,kBAAkB,GAAG,SAAS,CAEhF;AAwED,yFAAyF;AACzF,wBAAgB,mBAAmB,CAAC,EAClC,OAAO,EACP,MAAM,EACN,OAAO,EACP,SAAS,EACT,cAAc,EACd,UAAU,GACX,EAAE;IACD,OAAO,EAAE,0BAA0B,CAAC;IACpC,MAAM,EAAE,yBAAyB,GAAG,SAAS,CAAC;IAC9C,kEAAkE;IAClE,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,UAAU,KAAK,IAAI,CAAC;IACtC,wDAAwD;IACxD,SAAS,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;IAClC,kEAAkE;IAClE,cAAc,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;IACvC,yEAAyE;IACzE,UAAU,EAAE,CAAC,SAAS,EAAE,sBAAsB,KAAK,IAAI,CAAC;CACzD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA0DA"}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type OwnedChild } from './bun-child.js';
|
|
2
2
|
import type { NativeCommandLaunchedProcess } from './launch.js';
|
|
3
|
+
import { type OwnerLossGuardInvocation } from './owner-loss-protocol.js';
|
|
3
4
|
/** Only this native spawn can establish group ownership; structural PIDs cannot. */
|
|
4
5
|
export declare function spawnOwnedCommand(input: {
|
|
5
6
|
executable: string;
|
|
@@ -7,6 +8,7 @@ export declare function spawnOwnedCommand(input: {
|
|
|
7
8
|
cwd?: string;
|
|
8
9
|
env?: NodeJS.ProcessEnv;
|
|
9
10
|
group: boolean;
|
|
11
|
+
ownerLoss?: OwnerLossGuardInvocation;
|
|
10
12
|
}): OwnedChild;
|
|
11
13
|
/** The same ownership rule as {@link spawnOwnedCommand}, for a child that inherits stdio. */
|
|
12
14
|
export declare function spawnInheritedCommand(input: {
|
|
@@ -15,6 +17,7 @@ export declare function spawnInheritedCommand(input: {
|
|
|
15
17
|
cwd?: string;
|
|
16
18
|
env?: NodeJS.ProcessEnv;
|
|
17
19
|
group: boolean;
|
|
20
|
+
ownerLoss?: OwnerLossGuardInvocation;
|
|
18
21
|
}): NativeCommandLaunchedProcess;
|
|
19
22
|
export declare function ownsCommandGroup(child: object): boolean;
|
|
20
23
|
//# sourceMappingURL=owned-child.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"owned-child.d.ts","sourceRoot":"","sources":["../../src/process/owned-child.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"owned-child.d.ts","sourceRoot":"","sources":["../../src/process/owned-child.ts"],"names":[],"mappings":"AAEA,OAAO,EAGL,KAAK,UAAU,EAGhB,MAAM,aAAa,CAAC;AACrB,OAAO,KAAK,EAA+B,4BAA4B,EAAE,MAAM,UAAU,CAAC;AAM1F,OAAO,EAAE,KAAK,wBAAwB,EAAyB,MAAM,uBAAuB,CAAC;AAI7F,oFAAoF;AACpF,wBAAgB,iBAAiB,CAAC,KAAK,EAAE;IACvC,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,SAAS,MAAM,EAAE,CAAC;IACxB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB,KAAK,EAAE,OAAO,CAAC;IACf,SAAS,CAAC,EAAE,wBAAwB,CAAC;CACtC,GAAG,UAAU,CAIb;AAsFD,6FAA6F;AAC7F,wBAAgB,qBAAqB,CAAC,KAAK,EAAE;IAC3C,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,SAAS,MAAM,EAAE,CAAC;IACxB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB,KAAK,EAAE,OAAO,CAAC;IACf,SAAS,CAAC,EAAE,wBAAwB,CAAC;CACtC,GAAG,4BAA4B,CAkC/B;AAED,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAEvD"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { Duplex } from 'node:stream';
|
|
2
|
+
interface GuardedChild {
|
|
3
|
+
readonly pid?: number;
|
|
4
|
+
on(event: 'exit', listener: (code: number | null, signal: string | null) => void): unknown;
|
|
5
|
+
}
|
|
6
|
+
/** Tear down a newly created guard group if its private control channel is unavailable. */
|
|
7
|
+
export declare function abortOwnerLossLaunch(pid: number | undefined): void;
|
|
8
|
+
/** Bind one private inherited control socket to the guard process that owns it. */
|
|
9
|
+
export declare function attachOwnerLossControl(child: GuardedChild, control: Duplex): void;
|
|
10
|
+
/** Preserve the guard registration when an inherited-stdio child gets its transport wrapper. */
|
|
11
|
+
export declare function transferOwnerLossControl(source: object, target: object): void;
|
|
12
|
+
/** Ordinary children are already started; guarded children first prove that their target exists. */
|
|
13
|
+
export declare function awaitCommandStart(child: GuardedChild): Promise<number | undefined>;
|
|
14
|
+
export {};
|
|
15
|
+
//# sourceMappingURL=owner-loss-control.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"owner-loss-control.d.ts","sourceRoot":"","sources":["../../src/process/owner-loss-control.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAe1C,UAAU,YAAY;IACpB,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IACtB,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,GAAG,OAAO,CAAC;CAC5F;AASD,2FAA2F;AAC3F,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,CAOlE;AAQD,mFAAmF;AACnF,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,YAAY,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,CAuDjF;AAED,gGAAgG;AAChG,wBAAgB,wBAAwB,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI,CAG7E;AAED,oGAAoG;AACpG,wBAAsB,iBAAiB,CAAC,KAAK,EAAE,YAAY,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAIxF"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"owner-loss-guard.d.ts","sourceRoot":"","sources":["../../src/process/owner-loss-guard.ts"],"names":[],"mappings":"AA+KA,0FAA0F;AAC1F,wBAAgB,mBAAmB,IAAI,MAAM,CAE5C"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export declare const OWNER_LOSS_GUARD_FLAG = "--stitchkit-owner-loss-guard-v1";
|
|
2
|
+
export interface OwnerLossGuardInvocation {
|
|
3
|
+
readonly entry: string;
|
|
4
|
+
readonly signalTarget: 'group' | 'leader';
|
|
5
|
+
}
|
|
6
|
+
/** The owner-loss guard is certified only on the two POSIX kernels exercised by release CI. */
|
|
7
|
+
export declare function ownerLossAvailable(platform: NodeJS.Platform): boolean;
|
|
8
|
+
/** Replace a direct command with the package-owned guard that becomes its process-group leader. */
|
|
9
|
+
export declare function ownerLossGuardCommand(guard: OwnerLossGuardInvocation, executable: string, args: readonly string[]): [string, ...string[]];
|
|
10
|
+
//# sourceMappingURL=owner-loss-protocol.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"owner-loss-protocol.d.ts","sourceRoot":"","sources":["../../src/process/owner-loss-protocol.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,qBAAqB,oCAAoC,CAAC;AAEvE,MAAM,WAAW,wBAAwB;IACvC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,YAAY,EAAE,OAAO,GAAG,QAAQ,CAAC;CAC3C;AAED,+FAA+F;AAC/F,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,CAAC,QAAQ,GAAG,OAAO,CAErE;AAED,mGAAmG;AACnG,wBAAgB,qBAAqB,CACnC,KAAK,EAAE,wBAAwB,EAC/B,UAAU,EAAE,MAAM,EAClB,IAAI,EAAE,SAAS,MAAM,EAAE,GACtB,CAAC,MAAM,EAAE,GAAG,MAAM,EAAE,CAAC,CASvB"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"owner-loss-socket.d.ts","sourceRoot":"","sources":["../../src/process/owner-loss-socket.ts"],"names":[],"mappings":"AAAA,OAAO,EAAW,KAAK,MAAM,EAAE,MAAM,UAAU,CAAC;AAEhD,mGAAmG;AACnG,wBAAgB,sBAAsB,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,CAEzD"}
|
package/llms/process.txt
CHANGED
|
@@ -139,6 +139,7 @@ console.log(captured.exitCode, new TextDecoder().decode(captured.stdout))
|
|
|
139
139
|
const lifetime = new AbortController()
|
|
140
140
|
await runNativeCommand({
|
|
141
141
|
executable: '/usr/bin/git', args: ['status'], signal: lifetime.signal,
|
|
142
|
+
ownerLoss: 'terminate', // also stop its group if this JavaScript process is SIGKILLed
|
|
142
143
|
onOutput: async (bytes, channel, outputSignal) => {
|
|
143
144
|
outputSignal.throwIfAborted()
|
|
144
145
|
await output.write(bytes, channel, outputSignal) // application-owned bounded sink
|
|
@@ -201,6 +202,17 @@ sink cause is kept; a cleanup failure is a `NativeCommandError` with `COMMAND_CL
|
|
|
201
202
|
refuses with `COMMAND_UNAVAILABLE`; a descendant that has left the group on its own with `setsid` is
|
|
202
203
|
outside this capability. This is one-shot execution, not a PTY, a supervisor or a daemon host.
|
|
203
204
|
|
|
205
|
+
`ownerLoss: 'terminate'` binds the command group to this JavaScript process even when it cannot run
|
|
206
|
+
cleanup. A package-owned guard becomes the process-group leader, starts the requested executable in
|
|
207
|
+
that group and holds one end of a private inherited kernel channel. The caller holds the other end.
|
|
208
|
+
`SIGKILL`, a crash or any abrupt caller exit closes that end; EOF makes the guard KILL its own group.
|
|
209
|
+
There is no liveness polling and no later PID lookup. Because the guard is still the group leader
|
|
210
|
+
when it signals, its PGID cannot have been reused by a neighbour. The option is available on Linux
|
|
211
|
+
and Darwin under Bun and Node, requires `group: 'own'`, and other platforms refuse it with
|
|
212
|
+
`COMMAND_UNAVAILABLE`. It adds one short-lived process and one descriptor per guarded command, so it
|
|
213
|
+
is opt-in. `onLeaderStarted({ pid })` reports the guard/group leader with this option; output, stdin,
|
|
214
|
+
exit status, deadlines, cancellation and `onLeaderSettled` retain the requested command's behaviour.
|
|
215
|
+
|
|
204
216
|
### Stopping a command: the `stop` policy
|
|
205
217
|
|
|
206
218
|
A caller abort, the `timeoutMs` deadline, an exceeded output budget and a failing sink all stop the
|
|
@@ -235,13 +247,13 @@ dropped, never passed to `onOutput` and never captured, so a leader that writes
|
|
|
235
247
|
does not meet a broken pipe. The result rejects with the abort reason once the group is gone and the
|
|
236
248
|
pipes are closed, so it can take `graceMs` plus `cleanupTimeoutMs` after the abort.
|
|
237
249
|
|
|
238
|
-
The grace is a timer inside the process that runs the command.
|
|
239
|
-
timer is gone and the group, which is detached into its own session, keeps running
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
reaches every process in its cgroup, whatever its process group; launchd only kills
|
|
244
|
-
process group, which
|
|
250
|
+
The grace is a timer inside the process that runs the command. Without `ownerLoss`, if that process
|
|
251
|
+
exits first, the timer is gone and the group, which is detached into its own session, keeps running.
|
|
252
|
+
Abort `killOn` in your shutdown path so a deliberate shutdown kills the group before exit. For an
|
|
253
|
+
abrupt-exit guarantee, choose `ownerLoss: 'terminate'`; for all groups in a service, let the service
|
|
254
|
+
supervisor own the wider boundary. Under systemd, keep `KillMode=control-group` (the default) so the
|
|
255
|
+
stop of the unit reaches every process in its cgroup, whatever its process group; launchd only kills
|
|
256
|
+
the job's own process group, which an unguarded detached command's group is not.
|
|
245
257
|
|
|
246
258
|
### Stopping a group and its descendants
|
|
247
259
|
|
|
@@ -302,7 +314,7 @@ becomes `runNativeCommand`; shell policy, command admission, logging, redaction,
|
|
|
302
314
|
decoder and the operator sink stay with the application. Long signal-only commands stay streaming;
|
|
303
315
|
bounded tar or capture commands get their own explicit limits.
|
|
304
316
|
|
|
305
|
-
A child the caller holds until it exits is the same call. A
|
|
317
|
+
A child the caller holds until it exits is the same call. A host that forwards its own SIGTERM
|
|
306
318
|
passes an abort signal and `stop: { target: 'leader', signal: 'SIGTERM', graceMs }`. It records the
|
|
307
319
|
pid in `onLeaderStarted({ pid })`, which runs once right after the leader exists (a throw stops the
|
|
308
320
|
command with that error), and the leader's exit code and signal in `onLeaderSettled`: `'exit'`
|
|
@@ -312,7 +324,9 @@ long the output pipes may stay open after the leader exited: a holder that left
|
|
|
312
324
|
`setsid` keeps them open, and without the option the command waits for it until `timeoutMs` or
|
|
313
325
|
the signal; with it the command ends with `COMMAND_CLEANUP` (a holder inside the group is already
|
|
314
326
|
stopped by `descendants`). A worker whose whole group must stop on abort is the default
|
|
315
|
-
`target: 'group'`.
|
|
327
|
+
`target: 'group'`. Add `ownerLoss: 'terminate'` when the worker must also die after an abrupt host
|
|
328
|
+
loss; in that mode `onLeaderStarted` reports the package guard that leads the same group. A command
|
|
329
|
+
that uses the caller's terminal is the next section. Importing one leaf does not make
|
|
316
330
|
installing the whole npm package smaller; choose a dependency after measuring packed size and its
|
|
317
331
|
dependency closure.
|
|
318
332
|
|
|
@@ -346,6 +360,7 @@ its own, so:
|
|
|
346
360
|
because a group signal would reach the caller;
|
|
347
361
|
- `descendants` does not apply and is refused: what the leader leaves behind belongs to the
|
|
348
362
|
caller's group, and `descendantsStopped` is always `false`;
|
|
363
|
+
- `ownerLoss` is refused: an owner-loss guard must lead a group that belongs only to the command;
|
|
349
364
|
- a Ctrl-C reaches the caller too; handle `SIGINT` in the caller when it must outlive the command.
|
|
350
365
|
|
|
351
366
|
The two options are independent: `stdio: 'inherit'` alone keeps colour and `isatty` for a command
|
|
@@ -677,7 +692,7 @@ Native POSIX Bun/Node one-shot command execution. See [native IO](../guide/nativ
|
|
|
677
692
|
| Export | Kind | Purpose |
|
|
678
693
|
|---|---|---|
|
|
679
694
|
| `runNativeCommand` | function | caller-lifetime streaming or explicitly bounded binary capture |
|
|
680
|
-
| `NativeCommandOptions` | type | executable/args, env policy, signal/deadline, bytes sink, finite cleanup limits, the `stop` policy and `descendants` (`'terminate-after-leader'`, the default: stop the group after the leader exits, or `'leave'`); `stdio` (`'pipe'`, or `'inherit'` for the caller's descriptors, refusing the pipe-only options) and `group` (`'own'`, or `'caller'`: joins the caller's process group, stops only the leader, refuses `descendants` and a group stop); `onLeaderStarted({ pid })` (once
|
|
695
|
+
| `NativeCommandOptions` | type | executable/args, env policy, signal/deadline, bytes sink, finite cleanup limits, the `stop` policy and `descendants` (`'terminate-after-leader'`, the default: stop the group after the leader exits, or `'leave'`); `stdio` (`'pipe'`, or `'inherit'` for the caller's descriptors, refusing the pipe-only options) and `group` (`'own'`, or `'caller'`: joins the caller's process group, stops only the leader, refuses `descendants`, `ownerLoss` and a group stop); `ownerLoss: 'terminate'` (optional Linux/Darwin Bun/Node guard: kernel channel EOF after abrupt caller death KILLs its owned group, unsupported platforms refuse); `onLeaderStarted({ pid })` (once after the group leader exists; the package guard's PID under `ownerLoss`), `drainTimeoutMs` (bound on the pipes staying open after the leader exited, `COMMAND_CLEANUP`); an unknown key is refused by name, a removed one (`killGraceMs`) with its replacement |
|
|
681
696
|
| `NativeCommandStopPolicy` | type | how a cancelled command stops: `target` `'group'` (signal every member) or `'leader'` (signal only the leader; its exit ends the grace), `signal` (default `SIGTERM`), `graceMs` (at most one hour) before KILL to the group, and `killOn`, an `AbortSignal` that skips the grace; default `{ target: 'group', signal: 'SIGTERM', graceMs: 100 }` |
|
|
682
697
|
| `NativeCommandResult` | type | observed nullable exit code/signal, `descendantsStopped` (members left in the group after the leader exited were stopped) and captured stdout/stderr bytes |
|
|
683
698
|
| `NativeCommandError` | class | COMMAND_LIMIT with machine reason `deadline` or `output-budget`; COMMAND_UNAVAILABLE / COMMAND_CLEANUP have no limit reason; COMMAND_CLEANUP means the command's resources were not proven closed, either past `cleanupTimeoutMs` after a stop or past `drainTimeoutMs` after the leader exited, and `cause` holds that bound's failure. Preserves internal cause and existing constructor; caller-created limits without supplied reason remain unclassified |
|
package/llms/stitchkit.txt
CHANGED
|
@@ -69,7 +69,7 @@ own, recorded as an ADR.
|
|
|
69
69
|
| `stitchkit/cli` | server | stable | `createCli` / `createCliInvoker` — runtime and strict NodeNext declarations without MCP SDK / `ai` |
|
|
70
70
|
| `stitchkit/cli/publish` | build and release tooling (Bun or Node, POSIX) | evolving | `publishCli` — commit a verified CLI distribution and move its public manifest last; see [CLI guide](cli.md#publishing-a-complete-version) |
|
|
71
71
|
| `stitchkit/remote` | browser **and** server | stable | peer-free `implementRemote` for thin HTTP proxy processes |
|
|
72
|
-
| `stitchkit/process` | server (Bun or Node, POSIX) | evolving | finite native commands, byte sinks, bounded capture
|
|
72
|
+
| `stitchkit/process` | server (Bun or Node, POSIX) | evolving | finite native commands, byte sinks, bounded capture, process-group cancellation and opt-in Linux/Darwin owner-loss cleanup |
|
|
73
73
|
| `stitchkit/files/packaging` | build time (Bun or Node) | evolving | installed native asset graph, integrity, Bun packaging plugin and the artifact loader check; no runtime install policy |
|
|
74
74
|
| `stitchkit/files` | server (Bun or Node) | stable | peer-free managed local-file boundary |
|
|
75
75
|
| `stitchkit/telegram` | server (Bun or Node) | evolving | peer-free Telegram platform primitives — Mini App `initData` verification, send-failure classification, resumable broadcasts, the operator channel with dedupe, a Bot API transport that separates a request that never left from an unknown outcome, local Bot API files, webhook ownership and durable webhook update intake |
|
package/llms-full.txt
CHANGED
|
@@ -61,7 +61,7 @@ own, recorded as an ADR.
|
|
|
61
61
|
| `stitchkit/cli` | server | stable | `createCli` / `createCliInvoker` — runtime and strict NodeNext declarations without MCP SDK / `ai` |
|
|
62
62
|
| `stitchkit/cli/publish` | build and release tooling (Bun or Node, POSIX) | evolving | `publishCli` — commit a verified CLI distribution and move its public manifest last; see [CLI guide](cli.md#publishing-a-complete-version) |
|
|
63
63
|
| `stitchkit/remote` | browser **and** server | stable | peer-free `implementRemote` for thin HTTP proxy processes |
|
|
64
|
-
| `stitchkit/process` | server (Bun or Node, POSIX) | evolving | finite native commands, byte sinks, bounded capture
|
|
64
|
+
| `stitchkit/process` | server (Bun or Node, POSIX) | evolving | finite native commands, byte sinks, bounded capture, process-group cancellation and opt-in Linux/Darwin owner-loss cleanup |
|
|
65
65
|
| `stitchkit/files/packaging` | build time (Bun or Node) | evolving | installed native asset graph, integrity, Bun packaging plugin and the artifact loader check; no runtime install policy |
|
|
66
66
|
| `stitchkit/files` | server (Bun or Node) | stable | peer-free managed local-file boundary |
|
|
67
67
|
| `stitchkit/telegram` | server (Bun or Node) | evolving | peer-free Telegram platform primitives — Mini App `initData` verification, send-failure classification, resumable broadcasts, the operator channel with dedupe, a Bot API transport that separates a request that never left from an unknown outcome, local Bot API files, webhook ownership and durable webhook update intake |
|
|
@@ -8322,6 +8322,7 @@ console.log(captured.exitCode, new TextDecoder().decode(captured.stdout))
|
|
|
8322
8322
|
const lifetime = new AbortController()
|
|
8323
8323
|
await runNativeCommand({
|
|
8324
8324
|
executable: '/usr/bin/git', args: ['status'], signal: lifetime.signal,
|
|
8325
|
+
ownerLoss: 'terminate', // also stop its group if this JavaScript process is SIGKILLed
|
|
8325
8326
|
onOutput: async (bytes, channel, outputSignal) => {
|
|
8326
8327
|
outputSignal.throwIfAborted()
|
|
8327
8328
|
await output.write(bytes, channel, outputSignal) // application-owned bounded sink
|
|
@@ -8384,6 +8385,17 @@ sink cause is kept; a cleanup failure is a `NativeCommandError` with `COMMAND_CL
|
|
|
8384
8385
|
refuses with `COMMAND_UNAVAILABLE`; a descendant that has left the group on its own with `setsid` is
|
|
8385
8386
|
outside this capability. This is one-shot execution, not a PTY, a supervisor or a daemon host.
|
|
8386
8387
|
|
|
8388
|
+
`ownerLoss: 'terminate'` binds the command group to this JavaScript process even when it cannot run
|
|
8389
|
+
cleanup. A package-owned guard becomes the process-group leader, starts the requested executable in
|
|
8390
|
+
that group and holds one end of a private inherited kernel channel. The caller holds the other end.
|
|
8391
|
+
`SIGKILL`, a crash or any abrupt caller exit closes that end; EOF makes the guard KILL its own group.
|
|
8392
|
+
There is no liveness polling and no later PID lookup. Because the guard is still the group leader
|
|
8393
|
+
when it signals, its PGID cannot have been reused by a neighbour. The option is available on Linux
|
|
8394
|
+
and Darwin under Bun and Node, requires `group: 'own'`, and other platforms refuse it with
|
|
8395
|
+
`COMMAND_UNAVAILABLE`. It adds one short-lived process and one descriptor per guarded command, so it
|
|
8396
|
+
is opt-in. `onLeaderStarted({ pid })` reports the guard/group leader with this option; output, stdin,
|
|
8397
|
+
exit status, deadlines, cancellation and `onLeaderSettled` retain the requested command's behaviour.
|
|
8398
|
+
|
|
8387
8399
|
### Stopping a command: the `stop` policy
|
|
8388
8400
|
|
|
8389
8401
|
A caller abort, the `timeoutMs` deadline, an exceeded output budget and a failing sink all stop the
|
|
@@ -8418,13 +8430,13 @@ dropped, never passed to `onOutput` and never captured, so a leader that writes
|
|
|
8418
8430
|
does not meet a broken pipe. The result rejects with the abort reason once the group is gone and the
|
|
8419
8431
|
pipes are closed, so it can take `graceMs` plus `cleanupTimeoutMs` after the abort.
|
|
8420
8432
|
|
|
8421
|
-
The grace is a timer inside the process that runs the command.
|
|
8422
|
-
timer is gone and the group, which is detached into its own session, keeps running
|
|
8423
|
-
|
|
8424
|
-
|
|
8425
|
-
|
|
8426
|
-
reaches every process in its cgroup, whatever its process group; launchd only kills
|
|
8427
|
-
process group, which
|
|
8433
|
+
The grace is a timer inside the process that runs the command. Without `ownerLoss`, if that process
|
|
8434
|
+
exits first, the timer is gone and the group, which is detached into its own session, keeps running.
|
|
8435
|
+
Abort `killOn` in your shutdown path so a deliberate shutdown kills the group before exit. For an
|
|
8436
|
+
abrupt-exit guarantee, choose `ownerLoss: 'terminate'`; for all groups in a service, let the service
|
|
8437
|
+
supervisor own the wider boundary. Under systemd, keep `KillMode=control-group` (the default) so the
|
|
8438
|
+
stop of the unit reaches every process in its cgroup, whatever its process group; launchd only kills
|
|
8439
|
+
the job's own process group, which an unguarded detached command's group is not.
|
|
8428
8440
|
|
|
8429
8441
|
### Stopping a group and its descendants
|
|
8430
8442
|
|
|
@@ -8485,7 +8497,7 @@ becomes `runNativeCommand`; shell policy, command admission, logging, redaction,
|
|
|
8485
8497
|
decoder and the operator sink stay with the application. Long signal-only commands stay streaming;
|
|
8486
8498
|
bounded tar or capture commands get their own explicit limits.
|
|
8487
8499
|
|
|
8488
|
-
A child the caller holds until it exits is the same call. A
|
|
8500
|
+
A child the caller holds until it exits is the same call. A host that forwards its own SIGTERM
|
|
8489
8501
|
passes an abort signal and `stop: { target: 'leader', signal: 'SIGTERM', graceMs }`. It records the
|
|
8490
8502
|
pid in `onLeaderStarted({ pid })`, which runs once right after the leader exists (a throw stops the
|
|
8491
8503
|
command with that error), and the leader's exit code and signal in `onLeaderSettled`: `'exit'`
|
|
@@ -8495,7 +8507,9 @@ long the output pipes may stay open after the leader exited: a holder that left
|
|
|
8495
8507
|
`setsid` keeps them open, and without the option the command waits for it until `timeoutMs` or
|
|
8496
8508
|
the signal; with it the command ends with `COMMAND_CLEANUP` (a holder inside the group is already
|
|
8497
8509
|
stopped by `descendants`). A worker whose whole group must stop on abort is the default
|
|
8498
|
-
`target: 'group'`.
|
|
8510
|
+
`target: 'group'`. Add `ownerLoss: 'terminate'` when the worker must also die after an abrupt host
|
|
8511
|
+
loss; in that mode `onLeaderStarted` reports the package guard that leads the same group. A command
|
|
8512
|
+
that uses the caller's terminal is the next section. Importing one leaf does not make
|
|
8499
8513
|
installing the whole npm package smaller; choose a dependency after measuring packed size and its
|
|
8500
8514
|
dependency closure.
|
|
8501
8515
|
|
|
@@ -8529,6 +8543,7 @@ its own, so:
|
|
|
8529
8543
|
because a group signal would reach the caller;
|
|
8530
8544
|
- `descendants` does not apply and is refused: what the leader leaves behind belongs to the
|
|
8531
8545
|
caller's group, and `descendantsStopped` is always `false`;
|
|
8546
|
+
- `ownerLoss` is refused: an owner-loss guard must lead a group that belongs only to the command;
|
|
8532
8547
|
- a Ctrl-C reaches the caller too; handle `SIGINT` in the caller when it must outlive the command.
|
|
8533
8548
|
|
|
8534
8549
|
The two options are independent: `stdio: 'inherit'` alone keeps colour and `isatty` for a command
|
|
@@ -23044,7 +23059,7 @@ Native POSIX Bun/Node one-shot command execution. See [native IO](../guide/nativ
|
|
|
23044
23059
|
| Export | Kind | Purpose |
|
|
23045
23060
|
|---|---|---|
|
|
23046
23061
|
| `runNativeCommand` | function | caller-lifetime streaming or explicitly bounded binary capture |
|
|
23047
|
-
| `NativeCommandOptions` | type | executable/args, env policy, signal/deadline, bytes sink, finite cleanup limits, the `stop` policy and `descendants` (`'terminate-after-leader'`, the default: stop the group after the leader exits, or `'leave'`); `stdio` (`'pipe'`, or `'inherit'` for the caller's descriptors, refusing the pipe-only options) and `group` (`'own'`, or `'caller'`: joins the caller's process group, stops only the leader, refuses `descendants` and a group stop); `onLeaderStarted({ pid })` (once
|
|
23062
|
+
| `NativeCommandOptions` | type | executable/args, env policy, signal/deadline, bytes sink, finite cleanup limits, the `stop` policy and `descendants` (`'terminate-after-leader'`, the default: stop the group after the leader exits, or `'leave'`); `stdio` (`'pipe'`, or `'inherit'` for the caller's descriptors, refusing the pipe-only options) and `group` (`'own'`, or `'caller'`: joins the caller's process group, stops only the leader, refuses `descendants`, `ownerLoss` and a group stop); `ownerLoss: 'terminate'` (optional Linux/Darwin Bun/Node guard: kernel channel EOF after abrupt caller death KILLs its owned group, unsupported platforms refuse); `onLeaderStarted({ pid })` (once after the group leader exists; the package guard's PID under `ownerLoss`), `drainTimeoutMs` (bound on the pipes staying open after the leader exited, `COMMAND_CLEANUP`); an unknown key is refused by name, a removed one (`killGraceMs`) with its replacement |
|
|
23048
23063
|
| `NativeCommandStopPolicy` | type | how a cancelled command stops: `target` `'group'` (signal every member) or `'leader'` (signal only the leader; its exit ends the grace), `signal` (default `SIGTERM`), `graceMs` (at most one hour) before KILL to the group, and `killOn`, an `AbortSignal` that skips the grace; default `{ target: 'group', signal: 'SIGTERM', graceMs: 100 }` |
|
|
23049
23064
|
| `NativeCommandResult` | type | observed nullable exit code/signal, `descendantsStopped` (members left in the group after the leader exited were stopped) and captured stdout/stderr bytes |
|
|
23050
23065
|
| `NativeCommandError` | class | COMMAND_LIMIT with machine reason `deadline` or `output-budget`; COMMAND_UNAVAILABLE / COMMAND_CLEANUP have no limit reason; COMMAND_CLEANUP means the command's resources were not proven closed, either past `cleanupTimeoutMs` after a stop or past `drainTimeoutMs` after the leader exited, and `cause` holds that bound's failure. Preserves internal cause and existing constructor; caller-created limits without supplied reason remain unclassified |
|
package/llms.txt
CHANGED
|
@@ -56,7 +56,7 @@ Load the slice for each entrypoint your code imports — `import … from 'stitc
|
|
|
56
56
|
- **stitchkit/node**: [llms/node.txt](llms/node.txt) 3.1 KB
|
|
57
57
|
- **stitchkit/agent-runtime/react**: [llms/agent-runtime-react.txt](llms/agent-runtime-react.txt) 1.0 KB
|
|
58
58
|
- **stitchkit/agent-runtime/realtime**: [llms/agent-runtime-realtime.txt](llms/agent-runtime-realtime.txt) 2.6 KB
|
|
59
|
-
- **stitchkit/process**: [llms/process.txt](llms/process.txt)
|
|
59
|
+
- **stitchkit/process**: [llms/process.txt](llms/process.txt) 44.9 KB
|
|
60
60
|
- **stitchkit/files/packaging**: [llms/files-packaging.txt](llms/files-packaging.txt) 3.5 KB
|
|
61
61
|
|
|
62
62
|
## Upgrading slices
|
package/package.json
CHANGED