omp-conductor 0.5.4 → 0.5.6
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/README.md +3 -2
- package/package.json +1 -1
- package/src/omp.ts +44 -4
- package/src/verbs/socket.ts +21 -2
package/README.md
CHANGED
|
@@ -2166,8 +2166,9 @@ refuses a connection from anything else without answering it, logged the way an
|
|
|
2166
2166
|
impersonation is. Until a channel is bound it refuses everything, because the
|
|
2167
2167
|
socket necessarily exists before the child that connects to it. The kernel
|
|
2168
2168
|
supplies the pid: `SO_PEERCRED` on Linux, `LOCAL_PEERPID` on macOS. A host where
|
|
2169
|
-
neither can be asked
|
|
2170
|
-
|
|
2169
|
+
neither can be asked — no loadable libc, or the call refused — refuses every
|
|
2170
|
+
connection on a bound channel and says so at startup, rather than falling back to
|
|
2171
|
+
the uid, which under one shared uid is no check at all.
|
|
2171
2172
|
|
|
2172
2173
|
The residual is narrow, real, and worth stating: one uid can `ptrace` and signal
|
|
2173
2174
|
its siblings, so a determined session can still interfere with the process that
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "omp-conductor",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.6",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"description": "A 24/7 dispatcher that takes ready GitHub issues to green, mergeable PRs using omp coding sessions, with tiered escalation first to an orchestrator session and then to a human.",
|
package/src/omp.ts
CHANGED
|
@@ -361,6 +361,13 @@ const START_TIMEOUT_MS = 180_000;
|
|
|
361
361
|
/** Bounded tail of the child's stderr, so a crash is legible without unbounded buffering. */
|
|
362
362
|
const STDERR_TAIL = 8_000;
|
|
363
363
|
|
|
364
|
+
/**
|
|
365
|
+
* How long a dying child's pipes get to finish before its failure is reported
|
|
366
|
+
* without them. Generous because it only ever delays an already-failed startup,
|
|
367
|
+
* and the streams normally reach EOF the instant the process does.
|
|
368
|
+
*/
|
|
369
|
+
const DRAIN_GRACE_MS = 2_000;
|
|
370
|
+
|
|
364
371
|
/** How long a disposed child gets to exit before it is signalled. */
|
|
365
372
|
const DISPOSE_GRACE_MS = 5_000;
|
|
366
373
|
|
|
@@ -511,8 +518,35 @@ export async function createSession(opts: CreateSessionOptions): Promise<AgentSe
|
|
|
511
518
|
}
|
|
512
519
|
}
|
|
513
520
|
};
|
|
514
|
-
|
|
515
|
-
|
|
521
|
+
// Kept, not discarded: the child's own stderr is the whole value of the startup
|
|
522
|
+
// failure path, and it is a *race* against `child.exited` rather than a
|
|
523
|
+
// guarantee. A child that dies before connecting resolves `exited` as soon as
|
|
524
|
+
// the process is gone, which can be before these loops have read what it
|
|
525
|
+
// printed on the way out — and then the operator gets "exited 1" instead of the
|
|
526
|
+
// reason, which is precisely the diagnosis this path exists to give. Observed
|
|
527
|
+
// failing on ubuntu and passing on darwin, which is what a race looks like.
|
|
528
|
+
const drained = Promise.all([drain(child.stdout, "session: "), drain(child.stderr, "session: ")]);
|
|
529
|
+
drained.catch(() => undefined);
|
|
530
|
+
|
|
531
|
+
/**
|
|
532
|
+
* The child's output, once the pipes have actually finished — the whole point of
|
|
533
|
+
* every failure path below.
|
|
534
|
+
*
|
|
535
|
+
* Bounded rather than awaited outright: a grandchild that inherited stderr keeps
|
|
536
|
+
* the pipe open after the child is gone, and a failure nobody reports is worse
|
|
537
|
+
* than one reported without its tail. Both exit routes go through here so they
|
|
538
|
+
* cannot drift apart again; the pre-connect one did, which is how a release run
|
|
539
|
+
* caught this.
|
|
540
|
+
*/
|
|
541
|
+
const settledTail = async (): Promise<string> => {
|
|
542
|
+
await Promise.race([
|
|
543
|
+
drained,
|
|
544
|
+
new Promise<void>((resolve) => {
|
|
545
|
+
setTimeout(resolve, DRAIN_GRACE_MS).unref?.();
|
|
546
|
+
}),
|
|
547
|
+
]);
|
|
548
|
+
return stderrTail.trim();
|
|
549
|
+
};
|
|
516
550
|
|
|
517
551
|
const cleanup = (): void => {
|
|
518
552
|
server.close();
|
|
@@ -544,7 +578,7 @@ export async function createSession(opts: CreateSessionOptions): Promise<AgentSe
|
|
|
544
578
|
pending.clear();
|
|
545
579
|
};
|
|
546
580
|
|
|
547
|
-
void child.exited.then((code) => {
|
|
581
|
+
void child.exited.then(async (code) => {
|
|
548
582
|
onExit();
|
|
549
583
|
cleanup();
|
|
550
584
|
// A child that exits during teardown exited because we asked it to. Only an
|
|
@@ -552,6 +586,8 @@ export async function createSession(opts: CreateSessionOptions): Promise<AgentSe
|
|
|
552
586
|
// to reach whoever is awaiting a prompt, or the dispatcher waits forever
|
|
553
587
|
// for a turn from a process that is gone.
|
|
554
588
|
if (disposing) return;
|
|
589
|
+
// The tail first, so the message carries the child's own words.
|
|
590
|
+
await settledTail();
|
|
555
591
|
fail(`omp-conductor session child exited ${String(code)} before the session ended`);
|
|
556
592
|
});
|
|
557
593
|
|
|
@@ -577,7 +613,11 @@ export async function createSession(opts: CreateSessionOptions): Promise<AgentSe
|
|
|
577
613
|
} catch (err) {
|
|
578
614
|
child.kill("SIGKILL");
|
|
579
615
|
cleanup();
|
|
580
|
-
|
|
616
|
+
// Killed first, so the pipes are already closing, and only then read. This is
|
|
617
|
+
// the route a child that dies before connecting takes — a missing peer
|
|
618
|
+
// dependency, say — and reading `stderrTail` synchronously here raced the
|
|
619
|
+
// drain loops and reported a bare exit code instead of the reason.
|
|
620
|
+
const tail = await settledTail();
|
|
581
621
|
throw new Error(
|
|
582
622
|
`${err instanceof Error ? err.message : String(err)}${tail === "" ? "" : `\nchild output:\n${tail}`}`,
|
|
583
623
|
);
|
package/src/verbs/socket.ts
CHANGED
|
@@ -413,10 +413,29 @@ export function peerVerdict(
|
|
|
413
413
|
};
|
|
414
414
|
}
|
|
415
415
|
if (peer === undefined) {
|
|
416
|
+
// A channel bound to a process and no way to identify the caller is the one
|
|
417
|
+
// combination that must never pass. Under one uid the mode says nothing about
|
|
418
|
+
// *which* session is connecting, so allowing here would hand any session the
|
|
419
|
+
// authority of whichever channel it reached — on exactly the hosts where the
|
|
420
|
+
// check silently could not run. This ordering was the bug: the previous
|
|
421
|
+
// release returned `socket-ownership` here before ever looking at the
|
|
422
|
+
// expected pid, so a host whose libc would not load failed OPEN while the
|
|
423
|
+
// startup banner claimed it refused everything.
|
|
424
|
+
if (expected.pid !== undefined) {
|
|
425
|
+
return {
|
|
426
|
+
ok: false,
|
|
427
|
+
peerUid: -1,
|
|
428
|
+
expectedUid: expected.uid,
|
|
429
|
+
detail:
|
|
430
|
+
`this channel belongs to pid ${expected.pid} and this host reports no peer credentials at all, ` +
|
|
431
|
+
`so the caller cannot be identified — every session here shares one uid, and the socket mode ` +
|
|
432
|
+
`cannot tell them apart`,
|
|
433
|
+
};
|
|
434
|
+
}
|
|
416
435
|
return {
|
|
417
436
|
ok: true,
|
|
418
437
|
basis: "socket-ownership",
|
|
419
|
-
why: "this host exposes no peer-credential call, so the 0600 socket under the daemon-owned 0711 parent is all that keeps other local accounts out",
|
|
438
|
+
why: "this host exposes no peer-credential call and no pid was expected, so the 0600 socket under the daemon-owned 0711 parent is all that keeps other local accounts out",
|
|
420
439
|
};
|
|
421
440
|
}
|
|
422
441
|
if (peer.uid !== expected.uid) {
|
|
@@ -457,7 +476,7 @@ export function peerVerdict(
|
|
|
457
476
|
export function transportBanner(dir: string, peerReader: PeerReader | undefined): string {
|
|
458
477
|
const peers =
|
|
459
478
|
peerReader === undefined
|
|
460
|
-
? `no peer-credential call on ${process.platform} —
|
|
479
|
+
? `no peer-credential call on ${process.platform} — every bound channel REFUSES every connection`
|
|
461
480
|
: process.platform === "darwin"
|
|
462
481
|
? "caller pid+uid asserted with getpeereid and LOCAL_PEERPID; each channel bound to the session the daemon launched"
|
|
463
482
|
: "caller pid+uid asserted with SO_PEERCRED; each channel bound to the session the daemon launched";
|