omp-conductor 0.18.1 → 0.18.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/README.md +1 -1
- package/REFERENCE.md +1 -1
- package/agents/to-spec.md +6 -2
- package/package.json +1 -1
- package/schema/config.schema.json +6 -1
- package/src/arm-challenge.ts +250 -57
- package/src/briefs/orchestrator.md +21 -8
- package/src/briefs/to-spec.md +6 -2
- package/src/cli.ts +122 -1
- package/src/command-manifest.ts +16 -5
- package/src/commands/arm.ts +1 -1
- package/src/commands/intake.ts +4 -19
- package/src/commands/watch.ts +4 -17
- package/src/config-schema.ts +19 -6
- package/src/config.ts +23 -8
- package/src/daemon.ts +150 -241
- package/src/decisions.ts +19 -11
- package/src/doctor.ts +55 -129
- package/src/escalate.ts +22 -11
- package/src/fleet.ts +93 -218
- package/src/host.ts +7 -332
- package/src/omp-settings.ts +19 -0
- package/src/omp.ts +11 -56
- package/src/orchestrator-tick.ts +225 -37
- package/src/session-host.ts +6 -41
- package/src/setup-host.ts +27 -10
- package/src/setup-wizard.ts +141 -1
- package/src/setup.ts +54 -4
- package/src/status-render.ts +138 -1
- package/src/to-spec.ts +23 -2
- package/src/types.ts +62 -6
- package/src/verbs/server.ts +38 -3
- package/src/worker.ts +0 -13
package/src/host.ts
CHANGED
|
@@ -187,35 +187,22 @@ export function rssBytesFromHealthz(body: string | undefined): number | undefine
|
|
|
187
187
|
}
|
|
188
188
|
}
|
|
189
189
|
|
|
190
|
-
// ------------------------------------------------------- worker
|
|
190
|
+
// ------------------------------------------------------- worker host state (#894) --
|
|
191
191
|
|
|
192
192
|
/**
|
|
193
|
-
* The dedicated
|
|
193
|
+
* The dedicated worker account earlier boundary slices created on this host.
|
|
194
194
|
*
|
|
195
|
-
*
|
|
196
|
-
*
|
|
197
|
-
*
|
|
198
|
-
*
|
|
199
|
-
*
|
|
200
|
-
* it cannot migrate itself into a daemon-owned cgroup (every cgroup.procs on
|
|
201
|
-
* the host is root-owned, and cgroupfs directory ownership is what v2 gates
|
|
202
|
-
* writes with). This is the closure the rejected cgroup-namespace boundary
|
|
203
|
-
* named as required: kuid 0 with CAP_SYS_ADMIN could re-enter the initial
|
|
204
|
-
* cgroup namespace and fabricate a top-level cgroup, and a different uid is
|
|
205
|
-
* what takes that capability away.
|
|
195
|
+
* Worker sessions no longer launch under it (#894 restored the fleet-account
|
|
196
|
+
* runtime v0.18.0 used): the constants below survive only so `setup host` can
|
|
197
|
+
* keep staging and recognising the units it once installed until #895 retires
|
|
198
|
+
* that host lifecycle end to end. Nothing on the session-launch path reads
|
|
199
|
+
* them.
|
|
206
200
|
*/
|
|
207
201
|
export const WORKER_ACCOUNT = "omp-worker";
|
|
208
202
|
|
|
209
203
|
/** The worker account's system home: its harness config, caches and state. */
|
|
210
204
|
export const WORKER_HOME_DIR = "/var/lib/omp-worker";
|
|
211
205
|
|
|
212
|
-
/**
|
|
213
|
-
* The util-linux launcher worker sessions are spawned through. Pinned to an
|
|
214
|
-
* absolute path, never PATH-resolved: a launcher found later in the search
|
|
215
|
-
* order is a launcher an operator's environment could shadow.
|
|
216
|
-
*/
|
|
217
|
-
export const WORKER_SETPRIV_PATH = "/usr/bin/setpriv";
|
|
218
|
-
|
|
219
206
|
/**
|
|
220
207
|
* The harness package omp-conductor loads a session from. Held here, beside
|
|
221
208
|
* the identity that has to reach it, so `omp.ts` (which imports it) and the
|
|
@@ -357,315 +344,3 @@ export function harnessBindingProblem(deps: HarnessBindingDeps = {}): string | u
|
|
|
357
344
|
return undefined;
|
|
358
345
|
}
|
|
359
346
|
|
|
360
|
-
/** One resolved worker identity: the account exactly as the host knows it. */
|
|
361
|
-
export interface WorkerIdentity {
|
|
362
|
-
account: string;
|
|
363
|
-
uid: number;
|
|
364
|
-
gid: number;
|
|
365
|
-
/** The account's passwd home. */
|
|
366
|
-
home: string;
|
|
367
|
-
/** Absolute path of the identity-transition launcher. */
|
|
368
|
-
setpriv: string;
|
|
369
|
-
/** The harness agent dir for this identity: `<home>/.omp/agent`. */
|
|
370
|
-
agentDir: string;
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
export type WorkerIdentityResolution =
|
|
374
|
-
| { ok: true; identity: WorkerIdentity }
|
|
375
|
-
| { ok: false; reason: string };
|
|
376
|
-
|
|
377
|
-
export interface WorkerIdentityDeps {
|
|
378
|
-
/** The resolving process's own uid; injected so tests pin the verdict. */
|
|
379
|
-
daemonUid?: number;
|
|
380
|
-
/** Whether the transition launcher exists; injected so tests pin the verdict. */
|
|
381
|
-
setprivInstalled?: boolean;
|
|
382
|
-
/** The contents of /etc/passwd; injected so tests pin the verdict. */
|
|
383
|
-
passwd?: string;
|
|
384
|
-
/**
|
|
385
|
-
* Why the worker harness binding is unusable, or `undefined` when it is
|
|
386
|
-
* live. Injected so tests pin the verdict without a real mount; production
|
|
387
|
-
* reads the host through {@link harnessBindingProblem}.
|
|
388
|
-
*/
|
|
389
|
-
harnessProblem?: () => string | undefined;
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
/** Is a process uid "unprivileged" for the worker identity's purposes — never
|
|
393
|
-
* the daemon's uid, and never root: an account that keeps uid 0 is not a
|
|
394
|
-
* boundary, it is a costume. */
|
|
395
|
-
function usableWorkerUid(uid: number, daemonUid: number): string | undefined {
|
|
396
|
-
if (uid === 0) return "the worker account must not be uid 0 (root) — an identity transition to root is not a boundary";
|
|
397
|
-
if (uid === daemonUid) return `the worker account must not share the daemon's uid ${daemonUid}`;
|
|
398
|
-
return undefined;
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
/**
|
|
402
|
-
* Resolve the worker identity the daemon launches worker sessions under.
|
|
403
|
-
*
|
|
404
|
-
* This is the launch gate's input, resolved before every worker launch and at
|
|
405
|
-
* daemon startup for the banner. Every failure mode is a *reason*, never a
|
|
406
|
-
* throw: dispatch turns a missing account into a failed closed run (with this
|
|
407
|
-
* reason in the report and the escalation), so the operator hears exactly
|
|
408
|
-
* which host change is missing. A daemon that cannot transition — not running
|
|
409
|
-
* as root, or a host without setpriv — refuses to launch workers rather than
|
|
410
|
-
* running them unbound.
|
|
411
|
-
*/
|
|
412
|
-
export function resolveWorkerIdentity(deps: WorkerIdentityDeps = {}): WorkerIdentityResolution {
|
|
413
|
-
const daemonUid = deps.daemonUid ?? process.getuid?.() ?? 0;
|
|
414
|
-
if (daemonUid !== 0) {
|
|
415
|
-
return {
|
|
416
|
-
ok: false,
|
|
417
|
-
reason:
|
|
418
|
-
`the daemon runs as uid ${daemonUid}; only root can transition a child to another uid, ` +
|
|
419
|
-
"so worker sessions cannot be launched under the dedicated identity",
|
|
420
|
-
};
|
|
421
|
-
}
|
|
422
|
-
const setpriv = WORKER_SETPRIV_PATH;
|
|
423
|
-
const setprivInstalled = deps.setprivInstalled ?? existsSync(setpriv);
|
|
424
|
-
if (!setprivInstalled) {
|
|
425
|
-
return {
|
|
426
|
-
ok: false,
|
|
427
|
-
reason: `${setpriv} (util-linux setpriv) is not installed on this host — worker sessions cannot be launched under the dedicated identity`,
|
|
428
|
-
};
|
|
429
|
-
}
|
|
430
|
-
let passwd: string;
|
|
431
|
-
try {
|
|
432
|
-
passwd = deps.passwd ?? readFileSync("/etc/passwd", "utf8");
|
|
433
|
-
} catch {
|
|
434
|
-
return { ok: false, reason: `cannot read /etc/passwd while resolving the ${WORKER_ACCOUNT} identity` };
|
|
435
|
-
}
|
|
436
|
-
// `user:passwd:uid:gid:gecos:home:shell` — the whole line, never a prefix
|
|
437
|
-
// match, so a friendly lookalike account cannot satisfy the resolution.
|
|
438
|
-
const line = passwd.split("\n").find((entry) => entry.startsWith(`${WORKER_ACCOUNT}:`));
|
|
439
|
-
if (line === undefined) {
|
|
440
|
-
return {
|
|
441
|
-
ok: false,
|
|
442
|
-
reason:
|
|
443
|
-
`the ${WORKER_ACCOUNT} account does not exist on this host — run \`omp-conductor setup host\` ` +
|
|
444
|
-
"to create the dedicated worker identity",
|
|
445
|
-
};
|
|
446
|
-
}
|
|
447
|
-
const fields = line.split(":");
|
|
448
|
-
const uid = Number(fields[2]);
|
|
449
|
-
const gid = Number(fields[3]);
|
|
450
|
-
const home = fields[5] ?? "";
|
|
451
|
-
if (!Number.isInteger(uid) || uid < 0) {
|
|
452
|
-
return { ok: false, reason: `the ${WORKER_ACCOUNT} account has an unusable uid in /etc/passwd` };
|
|
453
|
-
}
|
|
454
|
-
if (!Number.isInteger(gid) || gid < 0) {
|
|
455
|
-
return { ok: false, reason: `the ${WORKER_ACCOUNT} account has an unusable gid in /etc/passwd` };
|
|
456
|
-
}
|
|
457
|
-
const problem = usableWorkerUid(uid, daemonUid);
|
|
458
|
-
if (problem !== undefined) return { ok: false, reason: problem };
|
|
459
|
-
if (home === "" || !home.startsWith("/")) {
|
|
460
|
-
return { ok: false, reason: `the ${WORKER_ACCOUNT} account has no absolute home in /etc/passwd` };
|
|
461
|
-
}
|
|
462
|
-
// Last, because it is the check the operator can only act on once the
|
|
463
|
-
// account is there: without a live bind of the operator's install, a worker
|
|
464
|
-
// child resolves its harness from Bun's auto-install cache instead — a
|
|
465
|
-
// different build whose native addon does not load (#828). Refusing here
|
|
466
|
-
// fails the launch closed with the fix, rather than letting every dispatch
|
|
467
|
-
// die inside a session it already paid to start.
|
|
468
|
-
const harnessProblem = deps.harnessProblem === undefined ? harnessBindingProblem() : deps.harnessProblem();
|
|
469
|
-
if (harnessProblem !== undefined) return { ok: false, reason: harnessProblem };
|
|
470
|
-
return {
|
|
471
|
-
ok: true,
|
|
472
|
-
identity: {
|
|
473
|
-
account: WORKER_ACCOUNT,
|
|
474
|
-
uid,
|
|
475
|
-
gid,
|
|
476
|
-
home,
|
|
477
|
-
setpriv,
|
|
478
|
-
agentDir: join(home, ".omp", "agent"),
|
|
479
|
-
},
|
|
480
|
-
};
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
/**
|
|
484
|
-
* The argv that launches a payload under the worker identity — the transition
|
|
485
|
-
* happens in the kernel at setpriv, BEFORE the first byte of any payload code
|
|
486
|
-
* runs, and setpriv does not exec the payload unless the transition succeeded
|
|
487
|
-
* (a failure exits non-zero with the reason on stderr, and the calling run
|
|
488
|
-
* settles failed with no live child). `--` protects a payload argv[0] that
|
|
489
|
-
* starts with `-`.
|
|
490
|
-
*
|
|
491
|
-
* A payload whose target uid/gid already equal this process's is returned
|
|
492
|
-
* unchanged: the identity is already in force, and spawning setpriv would
|
|
493
|
-
* only fail needlessly on a host without setuid privileges. That is also the
|
|
494
|
-
* only way the parity suite can drive the full spawn path as a non-root CI
|
|
495
|
-
* user.
|
|
496
|
-
*/
|
|
497
|
-
export function workerLaunchArgv(
|
|
498
|
-
payloadArgv: readonly string[],
|
|
499
|
-
identity: WorkerIdentity | undefined,
|
|
500
|
-
currentUid: number = process.getuid?.() ?? 0,
|
|
501
|
-
currentGid: number = process.getgid?.() ?? 0,
|
|
502
|
-
): string[] {
|
|
503
|
-
if (identity === undefined) return [...payloadArgv];
|
|
504
|
-
if (identity.uid === currentUid && identity.gid === currentGid) return [...payloadArgv];
|
|
505
|
-
// `--init-groups` initialises the supplementary groups from the identity
|
|
506
|
-
// being dropped to. It deliberately takes NO account operand (util-linux
|
|
507
|
-
// 2.39; newer versions accept an optional one): appending the account after
|
|
508
|
-
// it would be parsed as the program to exec on 2.39 and the launch would
|
|
509
|
-
// die with "failed to execute <account>" before any identity was dropped.
|
|
510
|
-
return [
|
|
511
|
-
identity.setpriv,
|
|
512
|
-
"--reuid",
|
|
513
|
-
identity.account,
|
|
514
|
-
"--regid",
|
|
515
|
-
identity.account,
|
|
516
|
-
"--init-groups",
|
|
517
|
-
"--",
|
|
518
|
-
...payloadArgv,
|
|
519
|
-
];
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
/**
|
|
523
|
-
* The environment a worker session child runs with: the daemon's, with the
|
|
524
|
-
* worker's own HOME (a worker under /root's HOME would try to write the
|
|
525
|
-
* daemon's home), its own harness agent dir (so config discovery is
|
|
526
|
-
* deterministic whatever the parent's environment says), and its own
|
|
527
|
-
* conductor state root (the worker's `omp-conductor` CLI reads its own empty
|
|
528
|
-
* store instead of the daemon's — the daemon's DB is daemon state, not
|
|
529
|
-
* worker input).
|
|
530
|
-
*/
|
|
531
|
-
export function workerSessionEnv(
|
|
532
|
-
base: Record<string, string | undefined>,
|
|
533
|
-
identity: WorkerIdentity,
|
|
534
|
-
): Record<string, string | undefined> {
|
|
535
|
-
return {
|
|
536
|
-
...base,
|
|
537
|
-
HOME: identity.home,
|
|
538
|
-
PI_CODING_AGENT_DIR: identity.agentDir,
|
|
539
|
-
OMP_CONDUCTOR_HOME: join(identity.home, ".omp", "conductor"),
|
|
540
|
-
};
|
|
541
|
-
}
|
|
542
|
-
|
|
543
|
-
export interface WorkerHarnessImportOptions {
|
|
544
|
-
bun?: string;
|
|
545
|
-
moduleDir?: string;
|
|
546
|
-
bindingRoot?: string;
|
|
547
|
-
identity?: WorkerIdentity;
|
|
548
|
-
timeoutMs?: number;
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
/**
|
|
552
|
-
* Run the anchored harness loader through the worker's real HOME and setpriv
|
|
553
|
-
* transition. Setup uses this after the inode checks: a mount can be current
|
|
554
|
-
* while ambient Bun resolution still chooses its install cache (#828).
|
|
555
|
-
*/
|
|
556
|
-
export function workerHarnessImportProblem(
|
|
557
|
-
options: WorkerHarnessImportOptions = {},
|
|
558
|
-
): string | undefined {
|
|
559
|
-
const bun = options.bun ?? process.execPath;
|
|
560
|
-
const moduleDir = options.moduleDir ?? import.meta.dir;
|
|
561
|
-
const bindingRoot = options.bindingRoot ?? WORKER_HARNESS_NODE_MODULES;
|
|
562
|
-
const timeoutMs = options.timeoutMs ?? 10_000;
|
|
563
|
-
const packageRoot = packageNodeModulesRoot(moduleDir);
|
|
564
|
-
if (packageRoot === undefined) {
|
|
565
|
-
return `cannot probe the worker harness import because ${moduleDir} is not inside a node_modules install root`;
|
|
566
|
-
}
|
|
567
|
-
let identity = options.identity;
|
|
568
|
-
if (identity === undefined) {
|
|
569
|
-
const resolved = resolveWorkerIdentity({ harnessProblem: () => undefined });
|
|
570
|
-
if (!resolved.ok) return resolved.reason;
|
|
571
|
-
identity = resolved.identity;
|
|
572
|
-
}
|
|
573
|
-
|
|
574
|
-
let sourceHarness: string;
|
|
575
|
-
let sourceNative: string;
|
|
576
|
-
let expectedVersion: string;
|
|
577
|
-
try {
|
|
578
|
-
sourceHarness = Bun.resolveSync(OMP_HARNESS_PACKAGE, moduleDir);
|
|
579
|
-
sourceNative = Bun.resolveSync(OMP_NATIVES_PACKAGE, dirname(sourceHarness));
|
|
580
|
-
const parsed: unknown = JSON.parse(
|
|
581
|
-
readFileSync(join(packageRoot, OMP_HARNESS_PACKAGE, "package.json"), "utf8"),
|
|
582
|
-
);
|
|
583
|
-
if (parsed === null || typeof parsed !== "object") throw new Error("package metadata is not an object");
|
|
584
|
-
const version = Reflect.get(parsed, "version");
|
|
585
|
-
if (typeof version !== "string" || version === "") throw new Error("package version is absent");
|
|
586
|
-
expectedVersion = version;
|
|
587
|
-
} catch (cause) {
|
|
588
|
-
return `cannot resolve the operator's installed harness for the worker probe: ${cause instanceof Error ? cause.message : String(cause)}`;
|
|
589
|
-
}
|
|
590
|
-
|
|
591
|
-
const probe = workerHarnessPath(join(moduleDir, "harness-loader.ts"), packageRoot, bindingRoot);
|
|
592
|
-
const expectedHarness = workerHarnessPath(sourceHarness, packageRoot, bindingRoot);
|
|
593
|
-
const expectedNative = workerHarnessPath(sourceNative, packageRoot, bindingRoot);
|
|
594
|
-
if (probe === undefined || expectedHarness === undefined || expectedNative === undefined) {
|
|
595
|
-
return "cannot map the installed harness probe through the worker binding";
|
|
596
|
-
}
|
|
597
|
-
const argv = workerLaunchArgv([bun, "--no-install", probe], identity);
|
|
598
|
-
const command = argv[0];
|
|
599
|
-
if (command === undefined) return "cannot launch the worker harness probe: its argv is empty";
|
|
600
|
-
const ran = spawnSync(command, argv.slice(1), {
|
|
601
|
-
cwd: identity.home,
|
|
602
|
-
env: workerSessionEnv(process.env, identity),
|
|
603
|
-
encoding: "utf8",
|
|
604
|
-
timeout: timeoutMs,
|
|
605
|
-
killSignal: "SIGKILL",
|
|
606
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
607
|
-
});
|
|
608
|
-
const errorCode = ran.error === undefined ? undefined : Reflect.get(ran.error, "code");
|
|
609
|
-
if (errorCode === "ETIMEDOUT") {
|
|
610
|
-
return `the worker harness import probe timed out after ${timeoutMs} ms`;
|
|
611
|
-
}
|
|
612
|
-
if (ran.status !== 0) {
|
|
613
|
-
const detail =
|
|
614
|
-
`${ran.error?.message ?? ""}\n${ran.stderr ?? ""}${ran.stdout ?? ""}`.trim();
|
|
615
|
-
return (
|
|
616
|
-
"the worker identity cannot import the operator's harness and native addon through the binding" +
|
|
617
|
-
(detail === "" ? "" : `: ${detail}`)
|
|
618
|
-
);
|
|
619
|
-
}
|
|
620
|
-
|
|
621
|
-
let report: unknown;
|
|
622
|
-
try {
|
|
623
|
-
const line = (ran.stdout ?? "").trim().split("\n").at(-1);
|
|
624
|
-
report = JSON.parse(line ?? "");
|
|
625
|
-
} catch {
|
|
626
|
-
return "the worker harness import probe returned no readable attestation";
|
|
627
|
-
}
|
|
628
|
-
if (report === null || typeof report !== "object") {
|
|
629
|
-
return "the worker harness import probe returned no readable attestation";
|
|
630
|
-
}
|
|
631
|
-
const loadedPath = Reflect.get(report, "path");
|
|
632
|
-
const loadedVersion = Reflect.get(report, "version");
|
|
633
|
-
const nativePath = Reflect.get(report, "nativePath");
|
|
634
|
-
if (
|
|
635
|
-
loadedPath !== expectedHarness ||
|
|
636
|
-
loadedVersion !== expectedVersion ||
|
|
637
|
-
nativePath !== expectedNative
|
|
638
|
-
) {
|
|
639
|
-
return (
|
|
640
|
-
`the worker harness import resolved ${String(loadedPath)} at version ${String(loadedVersion)} ` +
|
|
641
|
-
`with native addon ${String(nativePath)}, expected ${expectedHarness} at version ${expectedVersion} ` +
|
|
642
|
-
`with ${expectedNative}`
|
|
643
|
-
);
|
|
644
|
-
}
|
|
645
|
-
return undefined;
|
|
646
|
-
}
|
|
647
|
-
|
|
648
|
-
/**
|
|
649
|
-
* Why the current process does not satisfy the identity its launch spec
|
|
650
|
-
* required, or `undefined` when it does. The session child's first act, as
|
|
651
|
-
* early as there is a socket to report over: a child whose kernel identity is
|
|
652
|
-
* not the one it was launched for proves the boundary did not hold and must
|
|
653
|
-
* refuse to run worker code. The uid/gid getters default to this process's
|
|
654
|
-
* own, and are parameters so the check is testable without spawning.
|
|
655
|
-
*/
|
|
656
|
-
export function identityMismatch(
|
|
657
|
-
expected: { uid: number; gid: number } | undefined,
|
|
658
|
-
getuid: () => number = () => process.getuid?.() ?? 0,
|
|
659
|
-
getgid: () => number = () => process.getgid?.() ?? 0,
|
|
660
|
-
): string | undefined {
|
|
661
|
-
if (expected === undefined) return undefined;
|
|
662
|
-
const uid = getuid();
|
|
663
|
-
const gid = getgid();
|
|
664
|
-
if (uid !== expected.uid || gid !== expected.gid) {
|
|
665
|
-
return (
|
|
666
|
-
`worker identity mismatch: running as uid ${uid} gid ${gid}, but this session was launched ` +
|
|
667
|
-
`for uid ${expected.uid} gid ${expected.gid} — the identity transition did not hold, refusing to start`
|
|
668
|
-
);
|
|
669
|
-
}
|
|
670
|
-
return undefined;
|
|
671
|
-
}
|
package/src/omp-settings.ts
CHANGED
|
@@ -36,6 +36,25 @@ import type { ProjectConfig } from "./types.ts";
|
|
|
36
36
|
/** The overlay filename inside a run's session directory. */
|
|
37
37
|
export const OMP_SETTINGS_FILE = "omp-settings.yml";
|
|
38
38
|
|
|
39
|
+
/**
|
|
40
|
+
* The OMP model-role names one settings map declares — the keys of its
|
|
41
|
+
* `modelRoles` stanza (#875). This is the one grammar every surface that
|
|
42
|
+
* names an adjudicator role reads: omp's own `modelRoles` config, whether it
|
|
43
|
+
* sits in the daemon account's global settings, a project's overlay, or both.
|
|
44
|
+
* Anything that is not a flat string-map names no roles at all — an overlay
|
|
45
|
+
* whose `modelRoles` is, say, a YAML list is omp's to reject, and offering
|
|
46
|
+
* garbage role names here would only train the setup dialog on values OMP
|
|
47
|
+
* would refuse. `null` and any other non-object input — including an empty
|
|
48
|
+
* or comments-only settings file whose YAML parses to `null` — is zero
|
|
49
|
+
* roles, never a fault.
|
|
50
|
+
*/
|
|
51
|
+
export function modelRolesIn(settings: unknown): readonly string[] {
|
|
52
|
+
if (settings === null || typeof settings !== "object" || Array.isArray(settings)) return [];
|
|
53
|
+
const roles = (settings as Record<string, unknown>)["modelRoles"];
|
|
54
|
+
if (roles === null || typeof roles !== "object" || Array.isArray(roles)) return [];
|
|
55
|
+
return Object.keys(roles as Record<string, unknown>);
|
|
56
|
+
}
|
|
57
|
+
|
|
39
58
|
/**
|
|
40
59
|
* The effective overlay map for a project: the opaque `ompSettings` map plus
|
|
41
60
|
* the retry keys (`retry.modelFallback`, `retry.fallbackChains.default`)
|
package/src/omp.ts
CHANGED
|
@@ -8,20 +8,13 @@
|
|
|
8
8
|
* `createAgentSession`, `subscribe` or `abort`, exactly one file breaks.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import { chmodSync,
|
|
11
|
+
import { chmodSync, mkdirSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
|
|
12
12
|
import { createServer, type Server, type Socket } from "node:net";
|
|
13
13
|
import { tmpdir } from "node:os";
|
|
14
14
|
import { dirname, join } from "node:path";
|
|
15
15
|
import { fileURLToPath } from "node:url";
|
|
16
16
|
|
|
17
|
-
import {
|
|
18
|
-
OMP_HARNESS_PACKAGE,
|
|
19
|
-
packageNodeModulesRoot,
|
|
20
|
-
workerHarnessPath,
|
|
21
|
-
workerLaunchArgv,
|
|
22
|
-
workerSessionEnv,
|
|
23
|
-
type WorkerIdentity,
|
|
24
|
-
} from "./host.ts";
|
|
17
|
+
import { OMP_HARNESS_PACKAGE, packageNodeModulesRoot } from "./host.ts";
|
|
25
18
|
import { harnessVersion, resolveHarnessEntry } from "./harness-loader.ts";
|
|
26
19
|
import { readOnlySession, worktreeConfinement } from "./confinement.ts";
|
|
27
20
|
import { observeGraphTools } from "./graph.ts";
|
|
@@ -615,18 +608,6 @@ export interface CreateSessionOptions {
|
|
|
615
608
|
model?: string;
|
|
616
609
|
resume?: boolean;
|
|
617
610
|
role: SessionRole;
|
|
618
|
-
/**
|
|
619
|
-
* The worker identity this session must run under (#798): when set, the
|
|
620
|
-
* child is launched through {@link workerLaunchArgv}'s identity transition
|
|
621
|
-
* (setpriv) before any of its code runs, its environment is re-pointed at
|
|
622
|
-
* the worker's own home/agent path, the control socket is granted to that
|
|
623
|
-
* uid, and the child refuses to build a session unless its kernel uid/gid
|
|
624
|
-
* match. The daemon resolves this identity once and threads it through every
|
|
625
|
-
* worker launch; a worker session is never launched without it. Absent, the
|
|
626
|
-
* session runs as this process's own identity — the orchestrator and every
|
|
627
|
-
* test surface.
|
|
628
|
-
*/
|
|
629
|
-
identity?: WorkerIdentity;
|
|
630
611
|
releaseGrants?: ResolvedGrants;
|
|
631
612
|
onReleaseBlocked?: (shape: GateShape, context: ReleaseBlockContext) => void;
|
|
632
613
|
/**
|
|
@@ -682,12 +663,6 @@ export interface CreateSessionOptions {
|
|
|
682
663
|
* without a live harness or a model bill.
|
|
683
664
|
*/
|
|
684
665
|
hostModule?: string;
|
|
685
|
-
/**
|
|
686
|
-
* Test-only worker binding root. Production always uses
|
|
687
|
-
* `WORKER_HARNESS_NODE_MODULES`; the Linux privilege regression supplies an
|
|
688
|
-
* inode-identical temporary binding so it never mutates the live host mount.
|
|
689
|
-
*/
|
|
690
|
-
harnessNodeModules?: string;
|
|
691
666
|
/**
|
|
692
667
|
* Deny every tool but reading and searching (#307).
|
|
693
668
|
*
|
|
@@ -780,23 +755,15 @@ export async function createSession(opts: CreateSessionOptions): Promise<AgentSe
|
|
|
780
755
|
"daemon shutdown began while the session socket was binding",
|
|
781
756
|
);
|
|
782
757
|
}
|
|
783
|
-
// The socket is the run's own channel, and
|
|
784
|
-
//
|
|
785
|
-
//
|
|
786
|
-
// and the socket must be granted to it or the very first connect fails
|
|
787
|
-
// before the identity could matter.
|
|
758
|
+
// The socket is the run's own channel, and this process's own uid — the
|
|
759
|
+
// fleet account every session runs as (#894) — is the only one that speaks
|
|
760
|
+
// on it.
|
|
788
761
|
chmodSync(socketPath, 0o600);
|
|
789
|
-
if (opts.identity !== undefined) {
|
|
790
|
-
chownSync(socketPath, opts.identity.uid, opts.identity.gid);
|
|
791
|
-
}
|
|
792
762
|
|
|
793
763
|
const spec: SessionHostSpec = {
|
|
794
764
|
socket: socketPath,
|
|
795
765
|
cwd: opts.cwd,
|
|
796
766
|
role: opts.role,
|
|
797
|
-
...(opts.identity === undefined
|
|
798
|
-
? {}
|
|
799
|
-
: { identity: { uid: opts.identity.uid, gid: opts.identity.gid } }),
|
|
800
767
|
...(opts.sessionDir === undefined ? {} : { sessionDir: opts.sessionDir }),
|
|
801
768
|
...(opts.model === undefined ? {} : { model: opts.model }),
|
|
802
769
|
...(opts.resume === undefined ? {} : { resume: opts.resume }),
|
|
@@ -811,30 +778,18 @@ export async function createSession(opts: CreateSessionOptions): Promise<AgentSe
|
|
|
811
778
|
|
|
812
779
|
const log = opts.onChildLog ?? ((line: string) => process.stderr.write(`${line}\n`));
|
|
813
780
|
const hostModule = opts.hostModule ?? SESSION_HOST;
|
|
814
|
-
//
|
|
815
|
-
// the
|
|
816
|
-
//
|
|
817
|
-
//
|
|
818
|
-
|
|
819
|
-
const packageRoot = packageNodeModulesRoot(hostModule);
|
|
820
|
-
const boundEntry =
|
|
821
|
-
opts.harnessNodeModules === undefined
|
|
822
|
-
? workerHarnessPath(hostModule, packageRoot)
|
|
823
|
-
: workerHarnessPath(hostModule, packageRoot, opts.harnessNodeModules);
|
|
824
|
-
const entryModule = opts.identity === undefined ? hostModule : boundEntry ?? hostModule;
|
|
825
|
-
const payloadArgv = [process.execPath, "--no-install", entryModule, JSON.stringify(spec)];
|
|
826
|
-
const argv = workerLaunchArgv(payloadArgv, opts.identity);
|
|
781
|
+
// The child is the same installed tree this process runs from, launched
|
|
782
|
+
// under the fleet account (#894). `--no-install` keeps Bun off its ambient
|
|
783
|
+
// auto-install cache, so the peer and its native addon resolve from the
|
|
784
|
+
// operator's exact install or fail loudly.
|
|
785
|
+
const argv = [process.execPath, "--no-install", hostModule, JSON.stringify(spec)];
|
|
827
786
|
// The session's own role is stamped on the child, so a process the agent
|
|
828
787
|
// runs — `omp-conductor report` from its sandbox — can tell a worker session
|
|
829
788
|
// from the operator's shell. Direct CLI runs outside a spawned session
|
|
830
|
-
// inherit nothing and stay the orchestrator surface.
|
|
831
|
-
// identity, the child's HOME and agent/state roots are re-pointed at the
|
|
832
|
-
// worker account's own, so the harness it boots writes worker state, never
|
|
833
|
-
// the daemon's.
|
|
789
|
+
// inherit nothing and stay the orchestrator surface.
|
|
834
790
|
let env: Record<string, string | undefined> | undefined;
|
|
835
791
|
if (opts.role !== undefined) {
|
|
836
792
|
env = { ...process.env, [SESSION_ROLE_ENV]: opts.role };
|
|
837
|
-
if (opts.identity !== undefined) env = workerSessionEnv(env, opts.identity);
|
|
838
793
|
}
|
|
839
794
|
const child = Bun.spawn(argv, {
|
|
840
795
|
cwd: opts.cwd,
|