omp-conductor 0.4.4 → 0.5.0

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.
@@ -62,6 +62,34 @@ function normalise(state: string): string {
62
62
  * makes every other reading of the row wrong — that is the #362 case, where a
63
63
  * `pushed-green` row outlived its own PR and held the issue out of dispatch.
64
64
  */
65
+ /**
66
+ * Harness start failures, matched on the text it prints.
67
+ *
68
+ * Deliberately a small closed list rather than "anything at turn 0": a run that
69
+ * ended at turn 0 for a reason nobody has named is exactly what `unknown` is for,
70
+ * and quietly declaring it an environment fault would waive an attempt that may
71
+ * have been genuinely spent.
72
+ */
73
+ const START_FAILURE_SIGNATURES = [
74
+ "no model selected",
75
+ "no model configured",
76
+ "invalid api key",
77
+ "authentication failed",
78
+ "could not load its peer dependency",
79
+ ] as const;
80
+
81
+ /**
82
+ * The first line of a harness start failure, or undefined when this error is not
83
+ * one. Exported so the store's one-time repair recognises exactly what the
84
+ * classifier does — two definitions of "the session never started" would drift.
85
+ */
86
+ export function startFailure(lastError: string | undefined): string | undefined {
87
+ if (lastError === undefined) return undefined;
88
+ const text = lastError.toLowerCase();
89
+ const hit = START_FAILURE_SIGNATURES.find((signature) => text.includes(signature));
90
+ return hit === undefined ? undefined : lastError.split("\n")[0]?.trim();
91
+ }
92
+
65
93
  export function classifyRun(run: RunRecord, facts: ClassifyFacts): Classification {
66
94
  const hasArtifacts = run.prUrl !== undefined || run.headSha !== undefined || run.salvageSha !== undefined;
67
95
 
@@ -85,6 +113,22 @@ export function classifyRun(run: RunRecord, facts: ClassifyFacts): Classificatio
85
113
  };
86
114
  }
87
115
 
116
+ // Turn zero with an explicit harness start error is the most classifiable
117
+ // failure there is, and the least deserving of an implementation attempt: the
118
+ // session never got as far as reading the issue. 27 recoveries on the reference
119
+ // fleet were this, logged as `unknown` while the session's own output carried
120
+ // the literal string `No model selected` (#152).
121
+ if ((run.state === "failed" || run.state === "killed") && run.turns === 0) {
122
+ const detail = startFailure(run.lastError);
123
+ if (detail !== undefined) {
124
+ return {
125
+ cls: "env-start-failure",
126
+ recovery: "escalate",
127
+ evidence: `the session never started: ${detail}`,
128
+ };
129
+ }
130
+ }
131
+
88
132
  if (run.state === "blocked") {
89
133
  return {
90
134
  cls: "question",
package/src/fleet.ts CHANGED
@@ -27,8 +27,7 @@ import {
27
27
  import { createInterface } from "node:readline";
28
28
  import { homedir } from "node:os";
29
29
  import { dirname, join } from "node:path";
30
- import { findProject, loadConfig, resolveCredentials, stateDir } from "./config.ts";
31
- import { describeBoundary, probeHost } from "./credentials.ts";
30
+ import { findProject, loadConfig, stateDir } from "./config.ts";
32
31
  import { planUsageLine, readPlanUsage, sharedUsageSource } from "./usage.ts";
33
32
  import { readApprovalSurface } from "./approval-surface.ts";
34
33
  import { inspectBriefLayout } from "./brief-upgrade.ts";
@@ -956,15 +955,6 @@ export function formatCodeGraphHealth(graph: CodeGraphHealth, now = Date.now()):
956
955
  ].join("\n");
957
956
  }
958
957
 
959
- /**
960
- * The credential boundary row (#125): which mechanism is actually live, and the
961
- * residuals it does *not* close. Passed in rather than probed inside the
962
- * formatter so the wording is testable without a host.
963
- */
964
- export interface BoundaryStatus {
965
- headline: string;
966
- detail: string[];
967
- }
968
958
 
969
959
  export function formatFleetStatus(
970
960
  s: StatusSnapshot,
@@ -973,7 +963,6 @@ export function formatFleetStatus(
973
963
  telegram: TelegramHealth = { kind: "unprobed" },
974
964
  now = Date.now(),
975
965
  codeGraph: CodeGraphHealth = { configured: false },
976
- boundary: BoundaryStatus | undefined = undefined,
977
966
  brief: string | undefined = undefined,
978
967
  decisions: string | undefined = undefined,
979
968
  failureClasses: string | undefined = undefined,
@@ -1029,15 +1018,6 @@ export function formatFleetStatus(
1029
1018
  }
1030
1019
 
1031
1020
  const graphBlock = formatCodeGraphHealth(codeGraph, now);
1032
- // Reported every time, never only when it is bad. An operator reading this
1033
- // has to be able to see "unprotected" on the day they assumed otherwise, and
1034
- // a line that appears only in the failure case is one whose absence means
1035
- // nothing (#125). `undefined` here is a status rendered without a host probe,
1036
- // which is itself worth saying rather than silently omitting.
1037
- const boundaryLines =
1038
- boundary === undefined
1039
- ? ["boundary unprobed (no host capability probe was run for this status)"]
1040
- : [`boundary ${boundary.headline}`, ...boundary.detail.map((d) => ` ${d}`)];
1041
1021
 
1042
1022
  return [
1043
1023
  `dispatch ${layers.dispatch}`,
@@ -1047,7 +1027,6 @@ export function formatFleetStatus(
1047
1027
  recoveryLine,
1048
1028
  herdrLine,
1049
1029
  telegramLine,
1050
- ...boundaryLines,
1051
1030
  ...(brief === undefined ? [] : [brief]),
1052
1031
  ...(decisions === undefined ? [] : [decisions]),
1053
1032
  ...(failureClasses === undefined ? [] : [failureClasses]),
@@ -1128,39 +1107,6 @@ export async function renderStatus(projectName?: string): Promise<string> {
1128
1107
  ]);
1129
1108
  const cached = codeGraphFromHealthz(health?.body, project.name);
1130
1109
  const codeGraph = cached ?? (await probeCodeGraph(project));
1131
- // The daemon's own answer wins, and this is not an optimisation.
1132
- //
1133
- // Capabilities are a property of the PROCESS, not the host. `status` runs in
1134
- // an interactive shell with none of the unit's ambient capabilities and none
1135
- // of the conductor groups, so re-probing here reports `group-mode` — or
1136
- // "REFUSING DISPATCH" — on a fleet whose daemon is genuinely `uid-pool`. An
1137
- // operator following the documented verification step would conclude the
1138
- // boundary had failed while it was working perfectly.
1139
- //
1140
- // It is also the only answer that survives a `daemon-reload` without a
1141
- // restart, which no config check can see: this is what the running process
1142
- // actually got at startup.
1143
- //
1144
- // Falling back to a local probe when no daemon is running is still right —
1145
- // there is a real question to answer then — but it is labelled, because it is
1146
- // a different question.
1147
- const credentials = resolveCredentials(project);
1148
- const live = boundaryFromHealthz(health?.body);
1149
- const boundary =
1150
- live ?? {
1151
- ...describeBoundary(credentials.isolation, await probeHost({ slots: s.caps.maxConcurrentWorkers })),
1152
- detail:
1153
- rec === undefined
1154
- ? [
1155
- "No daemon is running, so this is THIS SHELL's view, not the fleet's — an interactive shell",
1156
- "holds none of the unit's capabilities. Start the daemon and re-read before concluding anything.",
1157
- ]
1158
- : [
1159
- "The daemon did not report a boundary (it predates the field), so this is THIS SHELL's view,",
1160
- "not the fleet's — an interactive shell holds none of the unit's capabilities. Upgrade and",
1161
- "restart the daemon before concluding anything.",
1162
- ],
1163
- };
1164
1110
  return formatFleetStatus(
1165
1111
  { ...s, planUsage },
1166
1112
  layers,
@@ -1168,7 +1114,6 @@ export async function renderStatus(projectName?: string): Promise<string> {
1168
1114
  telegram,
1169
1115
  Date.now(),
1170
1116
  codeGraph,
1171
- boundary,
1172
1117
  briefStatusLine(project),
1173
1118
  decisionStatusLine(project.name),
1174
1119
  failureClassBlock(project.name),
@@ -1570,32 +1515,3 @@ function probeOmpPane(
1570
1515
  }
1571
1516
  }
1572
1517
 
1573
- /**
1574
- * The boundary the running daemon resolved, out of its `/healthz` body.
1575
- *
1576
- * Same posture as `codeGraphFromHealthz`: an older daemon that predates the
1577
- * field simply does not answer, and the caller falls back to a local probe with
1578
- * that difference stated rather than papered over.
1579
- */
1580
- export function boundaryFromHealthz(
1581
- body: string | undefined,
1582
- ): { headline: string; detail: string[] } | undefined {
1583
- // `healthCheck` hands back the raw response TEXT, not a parsed object — the
1584
- // same shape `codeGraphFromHealthz` takes. Type-guarding this on `object`
1585
- // meant it never matched, so `status` silently fell back to probing the
1586
- // operator's own shell and printed "No daemon is running" at a fleet whose
1587
- // daemon was up. Unit tests fed it objects and agreed with themselves.
1588
- if (body === undefined || body.length === 0) return undefined;
1589
- let parsed: unknown;
1590
- try {
1591
- parsed = JSON.parse(body);
1592
- } catch {
1593
- return undefined;
1594
- }
1595
- if (typeof parsed !== "object" || parsed === null) return undefined;
1596
- const boundary = (parsed as { boundary?: unknown }).boundary;
1597
- if (typeof boundary !== "object" || boundary === null) return undefined;
1598
- const headline = (boundary as { headline?: unknown }).headline;
1599
- if (typeof headline !== "string" || headline.length === 0) return undefined;
1600
- return { headline, detail: [] };
1601
- }
package/src/gitops.ts ADDED
@@ -0,0 +1,189 @@
1
+ /**
2
+ * The daemon's own git and `gh` operations.
3
+ *
4
+ * Everything here runs as the daemon, with the operator's credentials, and it
5
+ * is the only place in this package that spawns a credentialed process. That is
6
+ * the whole organising idea: "what can reach the operator's `gh` credential" is
7
+ * answered by this file's call sites rather than by auditing every `Bun.spawn`
8
+ * in the tree.
9
+ *
10
+ * Sessions are not gated at the OS level — a worker is a child process of the
11
+ * daemon and inherits its environment. What keeps a run's publication honest is
12
+ * that the dispatcher performs it: a push or a pull request the daemon did not
13
+ * make is a run it cannot account for. The mediated verbs route a worker's
14
+ * intent through here so the settlement record and the branch cannot disagree.
15
+ */
16
+
17
+ import { join } from "node:path";
18
+
19
+ import type { ProjectConfig, RepoTarget } from "./types.ts";
20
+
21
+ /**
22
+ * One process run, captured. Injected everywhere in this module so the
23
+ * daemon-side push is testable without a network or a credential.
24
+ */
25
+ export type Exec = (
26
+ argv: readonly string[],
27
+ opts?: { cwd?: string; env?: Record<string, string>; stdin?: string },
28
+ ) => Promise<{ code: number; stdout: string; stderr: string }>;
29
+
30
+ /** The real one. Never inherits stdin: an unattended daemon must not block on a prompt. */
31
+ export const spawnCaptured: Exec = async (argv, opts = {}) => {
32
+ const [command, ...rest] = argv;
33
+ if (command === undefined) return { code: 127, stdout: "", stderr: "empty argv" };
34
+ const proc = Bun.spawn([command, ...rest], {
35
+ ...(opts.cwd === undefined ? {} : { cwd: opts.cwd }),
36
+ ...(opts.env === undefined ? {} : { env: opts.env }),
37
+ stdin: new Blob([opts.stdin ?? ""]),
38
+ stdout: "pipe",
39
+ stderr: "pipe",
40
+ });
41
+ const [stdout, stderr, code] = await Promise.all([
42
+ new Response(proc.stdout).text(),
43
+ new Response(proc.stderr).text(),
44
+ proc.exited,
45
+ ]);
46
+ return { code, stdout, stderr };
47
+ };
48
+
49
+ /**
50
+ * The daemon's own environment, credentials intact. **The only place this
51
+ * package constructs credential material.**
52
+ *
53
+ * There is nothing clever in the body, and that is deliberate: the value is
54
+ * that it is one named function, so a privileged spawn that does not come
55
+ * through here is a bug, whatever it happens to do.
56
+ *
57
+ * `GIT_TERMINAL_PROMPT=0` rides along because every caller is unattended: a
58
+ * credential prompt nobody can answer is a hang, not a failure.
59
+ */
60
+ export function credentialedEnv(
61
+ extra: Readonly<Record<string, string>> = {},
62
+ base: Readonly<Record<string, string | undefined>> = process.env,
63
+ ): Record<string, string> {
64
+ const env: Record<string, string> = {};
65
+ for (const [key, value] of Object.entries(base)) {
66
+ if (value !== undefined) env[key] = value;
67
+ }
68
+ env["GIT_TERMINAL_PROMPT"] = "0";
69
+ return { ...env, ...extra };
70
+ }
71
+
72
+ // ------------------------------------------------- the privileged publish path
73
+
74
+ /** One run's own repository, as the daemon addresses it. */
75
+ export interface RunRepoRef {
76
+ repo: RepoTarget;
77
+ /** The run's own git repository — not a linked worktree of the mirror. */
78
+ runRepoPath: string;
79
+ branch: string;
80
+ }
81
+
82
+ export type PushOutcome = { ok: true; sha: string } | { ok: false; stderr: string };
83
+ export type PrOutcome = { ok: true; url: string } | { ok: false; stderr: string };
84
+
85
+ /** A clone URL can carry a token, and it lands in git's own error text. */
86
+ const URL_USERINFO = /([a-zA-Z][a-zA-Z0-9+.-]*:\/\/)[^\s/@]+@/g;
87
+
88
+ /** Blank the `user:token@` part of any URL before a string reaches a log or a human. */
89
+ export function scrubUserinfo(text: string): string {
90
+ return text.replace(URL_USERINFO, "$1***@");
91
+ }
92
+
93
+ function mirrorPath(project: Pick<ProjectConfig, "mirrorRoot">, repo: RepoTarget): string {
94
+ return join(project.mirrorRoot, `${repo.name}.git`);
95
+ }
96
+
97
+ /** GitHub slug parsed off the clone URL, falling back to the routing name. */
98
+ export function repoSlugFor(repo: RepoTarget): string {
99
+ const m = /(?:[:/])([^/:]+\/[^/]+?)(?:\.git)?$/.exec(repo.cloneUrl);
100
+ return m?.[1] ?? repo.name;
101
+ }
102
+
103
+ /**
104
+ * Publish a run's branch: run repo → mirror → GitHub, **fast-forward only**.
105
+ *
106
+ * The worker commits into its own repository, which is durable and local; every
107
+ * hop that touches the network happens here, so the dispatcher observes the
108
+ * exact sha it recorded.
109
+ *
110
+ * Both hops are fast-forward only and there is no force path anywhere in this
111
+ * package. The fetch refspec deliberately carries **no leading `+`** — that
112
+ * single character is the difference between "advance the mirror's copy of this
113
+ * branch" and "make the mirror's copy whatever the run says it is", and a run
114
+ * repo is the one place in this system that model-executed code fully controls.
115
+ * The push carries no `--force` and no `--force-with-lease`: a lease is still a
116
+ * force, and a rejected push is a decision for a human, not a retry.
117
+ *
118
+ * A rejection — non-fast-forward, protected ref, unauthorised — comes back as a
119
+ * value carrying git's stderr verbatim, because the caller settles the run
120
+ * `failed` on it and a paraphrase is worthless in that report.
121
+ */
122
+ export async function pushRunBranch(
123
+ project: Pick<ProjectConfig, "mirrorRoot">,
124
+ run: RunRepoRef,
125
+ exec: Exec = spawnCaptured,
126
+ ): Promise<PushOutcome> {
127
+ const mirror = mirrorPath(project, run.repo);
128
+ const ref = `refs/heads/${run.branch}`;
129
+ const env = credentialedEnv();
130
+
131
+ const fetched = await exec(["git", "-C", mirror, "fetch", "--no-tags", run.runRepoPath, `${ref}:${ref}`], { env });
132
+ if (fetched.code !== 0) {
133
+ return { ok: false, stderr: scrubUserinfo(fetched.stderr.trim() || fetched.stdout.trim() || `git fetch exited ${String(fetched.code)}`) };
134
+ }
135
+
136
+ const resolved = await exec(["git", "-C", mirror, "rev-parse", ref], { env });
137
+ if (resolved.code !== 0) {
138
+ return { ok: false, stderr: scrubUserinfo(resolved.stderr.trim() || `git rev-parse ${ref} exited ${String(resolved.code)}`) };
139
+ }
140
+ const sha = resolved.stdout.trim();
141
+
142
+ const pushed = await exec(["git", "-C", mirror, "push", "origin", `${ref}:${ref}`], { env });
143
+ if (pushed.code !== 0) {
144
+ return { ok: false, stderr: scrubUserinfo(pushed.stderr.trim() || pushed.stdout.trim() || `git push exited ${String(pushed.code)}`) };
145
+ }
146
+ return { ok: true, sha };
147
+ }
148
+
149
+ /**
150
+ * Open the run's pull request from the daemon's side.
151
+ *
152
+ * The body goes over stdin, never argv: a body with newlines, backticks or a
153
+ * leading `-` is ordinary, and interpolating it into a command line is how a
154
+ * worker's report would become an argument.
155
+ */
156
+ export async function openRunPr(
157
+ project: Pick<ProjectConfig, "mirrorRoot">,
158
+ run: RunRepoRef,
159
+ opts: { title: string; body: string; base: string },
160
+ exec: Exec = spawnCaptured,
161
+ ): Promise<PrOutcome> {
162
+ void project;
163
+ const out = await exec(
164
+ [
165
+ "gh",
166
+ "pr",
167
+ "create",
168
+ "--repo",
169
+ repoSlugFor(run.repo),
170
+ "--head",
171
+ run.branch,
172
+ "--base",
173
+ opts.base,
174
+ "--title",
175
+ opts.title,
176
+ "--body-file",
177
+ "-",
178
+ ],
179
+ { env: credentialedEnv(), stdin: opts.body },
180
+ );
181
+ if (out.code !== 0) {
182
+ return { ok: false, stderr: scrubUserinfo(out.stderr.trim() || out.stdout.trim() || `gh pr create exited ${String(out.code)}`) };
183
+ }
184
+ const url = /https:\/\/github\.com\/\S+\/pull\/\d+/.exec(out.stdout)?.[0];
185
+ if (url === undefined) {
186
+ return { ok: false, stderr: `gh pr create succeeded but printed no PR URL: ${out.stdout.trim()}` };
187
+ }
188
+ return { ok: true, url };
189
+ }
package/src/omp.ts CHANGED
@@ -19,7 +19,6 @@ import { tmpdir } from "node:os";
19
19
  import { dirname, join } from "node:path";
20
20
 
21
21
  import { worktreeConfinement } from "./confinement.ts";
22
- import type { SessionBoundary } from "./credentials.ts";
23
22
  import { releasePolicyTripwire } from "./release-policy.ts";
24
23
  import type {
25
24
  HostToParent,
@@ -344,14 +343,7 @@ function asRawSession(created: unknown): RawSession {
344
343
 
345
344
  // --------------------------------------------------------- the session proxy
346
345
 
347
- /**
348
- * Absolute path of the child entrypoint, resolved against this package.
349
- *
350
- * Exported so the boundary suite can assert a slot principal may actually read
351
- * it. The launcher execs this path under the slot uid, and on a real deployment
352
- * it sits under the daemon's home — so a home hardened to `0700` makes every
353
- * worker die before it connects, in a way no shell-based probe notices (#125).
354
- */
346
+ /** Absolute path of the child entrypoint, resolved against this package. */
355
347
  export const SESSION_HOST = join(import.meta.dir, "session-host.ts");
356
348
 
357
349
  /**
@@ -381,16 +373,9 @@ export interface CreateSessionOptions {
381
373
  onReleaseBlocked?: (shape: ReleaseShape) => void;
382
374
 
383
375
  /**
384
- * The OS principal and environment this session runs behind (#125). Omitted,
385
- * the child runs as the daemon's own user with the daemon's environment —
386
- * which is the A1 shape: out of process, no security claim. The daemon always
387
- * passes one, and `status` reports which mechanism it names.
388
- */
389
- boundary?: SessionBoundary;
390
- /**
391
- * Where the control socket is bound. The daemon puts it inside the run's own
392
- * boundary root so the slot principal can reach it; omitted, a private temp
393
- * directory is used, which is what the tests and a `none` fleet want.
376
+ * Where the control socket is bound. The daemon puts it beside the run's own
377
+ * session directory; omitted, a private temp directory is used, which is what
378
+ * the tests want.
394
379
  */
395
380
  socketPath?: string;
396
381
  /**
@@ -442,11 +427,10 @@ export interface CreateSessionOptions {
442
427
  * the deployment mistake it is, not as an unexplained exit code.
443
428
  */
444
429
  export async function createSession(opts: CreateSessionOptions): Promise<AgentSessionLike> {
445
- const boundary = opts.boundary;
446
430
  const owned = opts.socketPath === undefined;
447
- // 0711, not 0700: a slot principal has to *traverse* to its own socket, and
448
- // must not be able to list what else is in here or unlink the socket and
449
- // bind its own. Same reasoning as the run workspace parent.
431
+ // 0711, not 0700: searchable, so a session reaches its own socket by name, but
432
+ // not listable nothing here can enumerate the other sockets, unlink one and
433
+ // bind an impostor in its place. Same reasoning as the run workspace parent.
450
434
  const socketDir = owned ? mkdtempSync(join(tmpdir(), "omp-session-")) : dirname(opts.socketPath ?? "");
451
435
  const socketPath = opts.socketPath ?? join(socketDir, "s");
452
436
  mkdirSync(socketDir, { recursive: true, mode: 0o711 });
@@ -469,11 +453,9 @@ export async function createSession(opts: CreateSessionOptions): Promise<AgentSe
469
453
  resolve();
470
454
  });
471
455
  });
472
- // The socket is the run's own channel: only its principal may speak on it.
456
+ // The socket is the run's own channel, and the daemon's own uid is the only
457
+ // one that speaks on it.
473
458
  chmodSync(socketPath, 0o600);
474
- if (boundary?.principal !== undefined) {
475
- chownSync(socketPath, boundary.principal.uid, boundary.principal.gid);
476
- }
477
459
 
478
460
  const spec: SessionHostSpec = {
479
461
  socket: socketPath,
@@ -487,12 +469,7 @@ export async function createSession(opts: CreateSessionOptions): Promise<AgentSe
487
469
  };
488
470
 
489
471
  const log = opts.onChildLog ?? ((line: string) => process.stderr.write(`${line}\n`));
490
- const argv = [
491
- ...(boundary?.launcher ?? []),
492
- process.execPath,
493
- opts.hostModule ?? SESSION_HOST,
494
- JSON.stringify(spec),
495
- ];
472
+ const argv = [process.execPath, opts.hostModule ?? SESSION_HOST, JSON.stringify(spec)];
496
473
  const child = Bun.spawn(argv, {
497
474
  cwd: opts.cwd,
498
475
  stdin: "ignore",
@@ -501,7 +478,6 @@ export async function createSession(opts: CreateSessionOptions): Promise<AgentSe
501
478
  // harness build cannot corrupt it.
502
479
  stdout: "pipe",
503
480
  stderr: "pipe",
504
- ...(boundary === undefined || Object.keys(boundary.env).length === 0 ? {} : { env: boundary.env }),
505
481
  });
506
482
 
507
483
  let stderrTail = "";
@@ -574,7 +550,7 @@ export async function createSession(opts: CreateSessionOptions): Promise<AgentSe
574
550
  new Error(
575
551
  `omp-conductor session child did not connect within ${String(
576
552
  (opts.startupTimeoutMs ?? START_TIMEOUT_MS) / 1000,
577
- )}s (launcher: ${argv.slice(0, Math.max(1, (boundary?.launcher ?? []).length)).join(" ") || "none"})`,
553
+ )}s`,
578
554
  ),
579
555
  );
580
556
  }, opts.startupTimeoutMs ?? START_TIMEOUT_MS).unref?.();
@@ -31,7 +31,6 @@ import { join } from "node:path";
31
31
 
32
32
  import { stateDir } from "./config.ts";
33
33
  import { formatEscalation } from "./escalate.ts";
34
- import type { SessionBoundary } from "./credentials.ts";
35
34
  import { createSession, disposeSession } from "./omp.ts";
36
35
  import type { AgentSessionLike } from "./omp.ts";
37
36
  import type { Escalation, ReleaseShape, ResolvedGrants, SessionRole } from "./types.ts";
@@ -48,7 +47,6 @@ export type CreateSessionFn = (opts: {
48
47
  role: SessionRole;
49
48
  releaseGrants?: ResolvedGrants;
50
49
  onReleaseBlocked?: (shape: ReleaseShape) => void;
51
- boundary?: SessionBoundary;
52
50
  socketPath?: string;
53
51
  verbSocketPath?: string;
54
52
  onChildLog?: (line: string) => void;
@@ -92,7 +90,6 @@ export interface OrchestratorOpts {
92
90
  * must have no read or write access to any run checkout — a property the
93
91
  * adversarial probe asserts rather than assumes.
94
92
  */
95
- boundary?: SessionBoundary;
96
93
  /** Control socket for the session child. See {@link OrchestratorOpts.boundary}. */
97
94
  socketPath?: string;
98
95
  /**
@@ -189,7 +186,6 @@ export async function startOrchestrator(o: OrchestratorOpts): Promise<Orchestrat
189
186
  role: "orchestrator",
190
187
  ...(o.releaseGrants === undefined ? {} : { releaseGrants: o.releaseGrants }),
191
188
  ...(o.onReleaseBlocked === undefined ? {} : { onReleaseBlocked: o.onReleaseBlocked }),
192
- ...(o.boundary === undefined ? {} : { boundary: o.boundary }),
193
189
  ...(o.socketPath === undefined ? {} : { socketPath: o.socketPath }),
194
190
  ...(o.verbSocketPath === undefined ? {} : { verbSocketPath: o.verbSocketPath }),
195
191
  ...(o.onChildLog === undefined ? {} : { onChildLog: o.onChildLog }),
package/src/plugin.ts CHANGED
@@ -20,7 +20,6 @@ import {
20
20
  repairPolicyBannerCrumbs,
21
21
  } from "./brief-upgrade.ts";
22
22
  import { configPath, expandHome, findProject, loadConfig, resolveCaps, saveConfig } from "./config.ts";
23
- import { mechanismSatisfies, probeHost } from "./credentials.ts";
24
23
  import { hostRamBytes, recommendedMaxWorkers } from "./host.ts";
25
24
  import {
26
25
  isPaused,
@@ -81,13 +80,11 @@ import {
81
80
  import {
82
81
  BASE_FRESHNESS,
83
82
  BEHIND_BASE_ACTIONS,
84
- CREDENTIAL_ISOLATIONS,
85
83
  DEFAULT_CAPS,
86
84
  DRAFT_POLICIES,
87
85
  RELEASE_REQUIREMENTS,
88
86
  RELEASE_SHAPES,
89
87
  type Caps,
90
- type CredentialIsolation,
91
88
  type ConductorConfig,
92
89
  type OrchestratorMode,
93
90
  type ProjectConfig,
@@ -864,43 +861,6 @@ const askAuthorityArea: AreaAsker = async (ctx, a) => ({
864
861
  * who may act, then under what conditions (#129). */
865
862
  const askPolicy: AreaAsker = async (ctx, a) => ({ ...a, policy: await askPolicyPreconditions(ctx, a.policy) });
866
863
 
867
- /**
868
- * Whether model-executed code gets its own OS principal (#125).
869
- *
870
- * The host is probed before the question is asked, and the answer the box can
871
- * actually honour is named in the option itself. That matters because the two
872
- * failure modes are asymmetric: choosing `none` gets a fleet that dispatches
873
- * and is unprotected, while choosing `per-run` on a host with no mechanism gets
874
- * a fleet that refuses every issue. An operator should not have to discover
875
- * which one they picked by watching the queue stall.
876
- */
877
- const askCredentials: AreaAsker = async (ctx, a) => {
878
- const probe = await probeHost({ slots: a.caps.maxConcurrentWorkers ?? DEFAULT_CAPS.maxConcurrentWorkers });
879
- // Each option says whether THIS host can honour it, asked of the same
880
- // predicate the dispatch gate uses, so the wizard cannot promise a boundary
881
- // the daemon will then refuse to build.
882
- const offer = (isolation: CredentialIsolation, claim: string): string =>
883
- mechanismSatisfies(isolation, probe.mechanism)
884
- ? `${claim} — this host can build it with ${probe.mechanism}`
885
- : `${claim} — UNAVAILABLE here (${probe.reasons.join("; ") || "no mechanism found"}); dispatch would refuse every issue`;
886
- const described: { readonly [K in CredentialIsolation]: string } = {
887
- "per-run": offer("per-run", "each session runs under its own OS principal; it cannot reach the daemon's credentials"),
888
- "group-mode": offer(
889
- "group-mode",
890
- "same uid as the daemon, cross-run separation by group and mode only; bounds accidents, does NOT contain a bash escape",
891
- ),
892
- none: "sessions run as the daemon's user; env scrubbing only, which same-uid code defeats in one line",
893
- };
894
- const isolation = await askLiteral(
895
- ctx,
896
- "Credential isolation for worker and orchestrator sessions",
897
- CREDENTIAL_ISOLATIONS,
898
- described,
899
- a.credentials.isolation,
900
- );
901
- return { ...a, credentials: { ...a.credentials, isolation } };
902
- };
903
-
904
864
  /** How a stuck run reaches a human, and who triages it when it does. */
905
865
  const askEscalation: AreaAsker = async (ctx, a) => {
906
866
  const telegram = detectTelegram();
@@ -957,7 +917,6 @@ const AREA_ASKERS: { readonly [K in AmendAreaId]: AreaAsker } = {
957
917
  graph: askGraph,
958
918
  authority: askAuthorityArea,
959
919
  policy: askPolicy,
960
- credentials: askCredentials,
961
920
  escalation: askEscalation,
962
921
  reporting: askReporting,
963
922
  brief: askBrief,
@@ -1048,7 +1007,6 @@ async function collectAnswers(
1048
1007
  a = await askCaps(ctx, a);
1049
1008
  a = await askAuthorityArea(ctx, a);
1050
1009
  a = await askPolicy(ctx, a);
1051
- a = await askCredentials(ctx, a);
1052
1010
  a = await askWorkerModel(ctx, a);
1053
1011
  a = await askEscalation(ctx, a);
1054
1012
  a = await askReporting(ctx, a);
@@ -1166,6 +1124,7 @@ async function setup(ctx: CommandContext, projectArg: string | undefined): Promi
1166
1124
  const telegram = detectTelegram();
1167
1125
  const nextConfig = buildConfig(answers, existing);
1168
1126
  const project = findProject(nextConfig, answers.projectName);
1127
+ // The same project as it is configured right now, so a moved `workspaceRoot`
1169
1128
  if (
1170
1129
  project.escalation.orchestrator === "external" &&
1171
1130
  !answers.writeOrchestratorBrief &&
@@ -1,20 +1,19 @@
1
1
  #!/usr/bin/env bun
2
2
  /**
3
- * The child process one omp session runs in (#125 A1).
3
+ * The child process one omp session runs in.
4
4
  *
5
- * Before this file existed, `createSession` called the harness in the daemon's
6
- * own process, so every session inherited the daemon's `process.env`, its
7
- * `$HOME` and its whole filesystem view and the daemon authenticates by
8
- * shelling out to the operator's logged-in `gh`. No amount of tool-layer
9
- * confinement closes that, because `bash` is deliberately unconfined and the
10
- * credential is reachable by running the same binary the daemon runs.
5
+ * Sessions run out of process so that one session's memory, crash or runaway
6
+ * turn is the child's and not the dispatcher's: a harness that exits, hangs or
7
+ * exhausts its heap takes down a process the daemon supervises rather than the
8
+ * daemon itself. It is also what lets `MemoryMax` on the unit govern the whole
9
+ * fleet, and what makes a kill a real kill.
11
10
  *
12
- * So the session moves out of process. This file is the far side: it loads the
13
- * harness, runs the real session, and speaks a five-verb protocol back over a
14
- * unix socket to the {@link AgentSessionLike} proxy in `omp.ts`. It holds no
15
- * conductor state, opens no database, and reads no config — everything it needs
16
- * arrives in {@link SessionHostSpec}, because under `uid-pool` this process runs
17
- * as a principal that cannot read the daemon's state directory at all.
11
+ * This file is the far side: it loads the harness, runs the real session, and
12
+ * speaks a five-verb protocol back over a unix socket to the
13
+ * {@link AgentSessionLike} proxy in `omp.ts`. It holds no conductor state, opens
14
+ * no database, and reads no config — everything it needs arrives in
15
+ * {@link SessionHostSpec}, so the child's inputs are data a caller can see
16
+ * rather than ambient state it inherits.
18
17
  *
19
18
  * The protocol is deliberately tiny. `AgentSessionLike` has five members, so
20
19
  * there are five things to carry, and every one of them is data.
@@ -156,12 +155,6 @@ export async function runSessionHost(
156
155
  spec: SessionHostSpec,
157
156
  deps: SessionHostDeps = { createSession: createLocalSession },
158
157
  ): Promise<void> {
159
- // umask before anything is created. Under uid-pool the run tree is
160
- // `2770 slot:conductor-daemon`, and the setgid bit only decides the *group*
161
- // of what this process writes — without `0007` the mode still lands `0644`
162
- // and the daemon's access to a file the worker created is silently partial,
163
- // which surfaces as a failed salvage in production rather than here.
164
- process.umask(0o007);
165
158
 
166
159
  const socket = connect(spec.socket);
167
160
  socket.setNoDelay(true);