omp-conductor 0.4.5 → 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.
package/src/daemon.ts CHANGED
@@ -14,12 +14,9 @@ import {
14
14
  configPath,
15
15
  findProject,
16
16
  loadConfig,
17
- migrateCredentialsOnDisk,
18
17
  resolveCaps,
19
- resolveCredentials,
20
18
  resolveReleaseGrants,
21
19
  stateDir,
22
- sharedRoot,
23
20
  } from "./config.ts";
24
21
  import {
25
22
  analyseSettlement,
@@ -49,7 +46,6 @@ import type {
49
46
  Escalation,
50
47
  OpenCloser,
51
48
  PrState,
52
- CredentialIsolation,
53
49
  ProjectConfig,
54
50
  ReadyIssue,
55
51
  RepoTarget,
@@ -64,7 +60,6 @@ import type {
64
60
  Tracker,
65
61
  VerbLedgerEntry,
66
62
  } from "./types.ts";
67
- import { CONDUCTOR_GROUPS, type IsolationMechanism } from "./types.ts";
68
63
  import { type KilledBy, type WorkerResult, renderBrief, runWorker } from "./worker.ts";
69
64
  import {
70
65
  addRunRepo,
@@ -90,31 +85,7 @@ import {
90
85
  } from "./verbs/socket.ts";
91
86
  import { homedir } from "node:os";
92
87
 
93
- import {
94
- applyRunOwnership,
95
- applyMirrorReadAccess,
96
- auditMcpRoots,
97
- mirrorTraversalRefusal,
98
- credentialReadRefusal,
99
- spawnCaptured,
100
- boundaryRefusal,
101
- buildSessionBoundary,
102
- createSlotPool,
103
- describeBoundary,
104
- liveDaemonCredentials,
105
- mcpRefusal,
106
- mechanismSatisfies,
107
- openRunPr,
108
- probeHost,
109
- pushRunBranch,
110
- resolveModelCredential,
111
- verifyDaemonAccess,
112
- type HostProbe,
113
- type ModelCredential,
114
- type RunRepoRef,
115
- type SessionBoundary,
116
- type SlotPool,
117
- } from "./credentials.ts";
88
+ import { pushRunBranch, type RunRepoRef } from "./gitops.ts";
118
89
  import {
119
90
  planUsageLine,
120
91
  readPlanUsage,
@@ -152,44 +123,9 @@ export interface DaemonOpts {
152
123
 
153
124
  /** Everything one tick touches, resolved once at startup so a tick never
154
125
  * re-reads config mid-flight and changes its own limits underneath itself. */
155
- /**
156
- * The credential boundary this fleet is running behind (#125), resolved once at
157
- * startup like every other dep so a tick cannot change its own protection.
158
- *
159
- * `paged` is the same one-page-per-episode gate the integrity tripwire uses: a
160
- * host that cannot build the boundary the operator asked for holds *every*
161
- * candidate on *every* tick, and paging per five minutes forever is paging
162
- * nobody reads.
163
- */
164
- export interface FleetBoundary {
165
- isolation: CredentialIsolation;
166
- probe: HostProbe;
167
- slots: SlotPool;
168
- paged: boolean;
169
- }
170
-
171
- /**
172
- * True when the operator asked for a boundary this host cannot build.
173
- *
174
- * Asks {@link mechanismSatisfies} rather than testing for `none`, because the
175
- * interesting refusal is the near miss: a host offering `group-mode` against a
176
- * `per-run` config used to dispatch happily under the weaker boundary, which is
177
- * the silent downgrade #125 forbids.
178
- */
179
- export function boundaryRefusesDispatch(boundary: FleetBoundary | undefined): boolean {
180
- return boundary !== undefined && !mechanismSatisfies(boundary.isolation, boundary.probe.mechanism);
181
- }
182
-
183
126
  interface Deps {
184
127
  project: ProjectConfig;
185
128
  caps: Caps;
186
- /**
187
- * Optional only so a test can build a `Deps` without a host probe; production
188
- * always resolves one in `runDaemon`. Absent means the A1 shape — sessions
189
- * are still child processes, but they run as the daemon's own user and no
190
- * security claim is made for them.
191
- */
192
- boundary?: FleetBoundary;
193
129
  tracker: Tracker;
194
130
  store: Store;
195
131
  /** Provider-reported plan allowance, cached with a TTL. Resolved once at
@@ -200,17 +136,6 @@ interface Deps {
200
136
  integrity: IntegrityGate;
201
137
  stall: StallGate;
202
138
  cleanup?: RetainedCleanupCursor;
203
- /**
204
- * How sessions authenticate a model, resolved once at startup so every child of
205
- * one tick agrees — and so a fleet with none is refused at admission instead of
206
- * discovering it at turn 0 (#152).
207
- *
208
- * Optional exactly as {@link Deps.boundary} is: absent means no resolution was
209
- * made, which is the shape a unit test that does not exercise this gate wants.
210
- * `runDaemon` always resolves one. `paged` is the page-once latch, so a fleet
211
- * held for an environment fault says so once rather than every five minutes.
212
- */
213
- model?: { credential?: ModelCredential; paged: boolean };
214
139
  /**
215
140
  * Reads the connecting uid off a verb socket (#126). Resolved once at startup
216
141
  * so the mechanism is logged before the first socket exists; absent on a host
@@ -682,48 +607,6 @@ export async function settleWorktree(
682
607
  };
683
608
  }
684
609
 
685
- /**
686
- * The probe a session gets when no boundary was resolved at all — a test, or a
687
- * `--once` run built without one. Named rather than inlined so the "no
688
- * mechanism, no claim" shape is one object every call site shares.
689
- */
690
- const NO_BOUNDARY_PROBE: HostProbe = {
691
- mechanism: "none",
692
- reasons: ["no host probe was run"],
693
- residuals: [],
694
- boundingSetDropped: false,
695
- slots: [],
696
- };
697
-
698
- /**
699
- * Credential paths the macOS profile denies outright.
700
- *
701
- * Derived from the operator's real home rather than from the session's
702
- * redirected one: the point is that the *operator's* `gh` config and keys stay
703
- * unreachable even when a session names their absolute path, which is exactly
704
- * the probe `GH_CONFIG_DIR=<operator home>/.config/gh gh auth status` in #125.
705
- */
706
- function credentialDenyRoots(): string[] {
707
- const home = homedir();
708
- return [join(home, ".ssh"), join(home, ".config", "gh"), join(home, ".gnupg"), join(home, ".aws")];
709
- }
710
-
711
- function credentialDenyFiles(): string[] {
712
- const home = homedir();
713
- return [
714
- join(home, ".git-credentials"),
715
- join(home, ".npmrc"),
716
- join(home, ".netrc"),
717
- // Both git config locations, because a token does not only live in
718
- // `.git-credentials`: `url.https://<token>@github.com/.insteadOf` is a
719
- // perfectly ordinary way to wire one up, and an ordinary `.gitconfig` is
720
- // 0644. Hardening chmods it, but hardening is not the claim — this list is
721
- // what the empirical recheck actually asks the slot principal about, so a
722
- // file missing from here is a credential nobody verified was out of reach.
723
- join(home, ".gitconfig"),
724
- join(home, ".config", "git", "config"),
725
- ];
726
- }
727
610
 
728
611
  /**
729
612
  * How a run's end is named — in the salvage commit, and to whoever reads it.
@@ -962,24 +845,21 @@ async function handleIssue(d: Deps, r: Routed, attempt: number): Promise<void> {
962
845
  const mirrorPath = mirrorPathFor(r.repo, project.mirrorRoot);
963
846
  let worktreePath: string | undefined;
964
847
  let turnLimit: TurnLimitController | undefined;
965
- // #125: the run's own repository, the slot it holds, and the boundary its
966
- // session runs behind. Hoisted for the same reason `worktreePath` is — the
967
- // catch and finally paths have to release the slot and publish the branch.
848
+ // The run's own repository. Hoisted for the same reason `worktreePath` is
849
+ // the catch and finally paths have to publish the branch.
968
850
  let runRepo: RunRepoRef | undefined;
969
- let slotIndex: number | undefined;
970
- let boundary: SessionBoundary | undefined;
971
- // #126: the run's own verb socket. Hoisted like the slot, because the catch
851
+ // #126: the run's own verb socket. Hoisted like the run repo, because the catch
972
852
  // and finally paths have to close it — a socket outliving its run is a
973
853
  // channel nobody is authenticating any more.
974
854
  let verbListener: VerbListener | undefined;
975
- const credentials = resolveCredentials(project);
976
855
 
977
856
  /**
978
857
  * Publishes the run's branch on the privileged side: run repo → mirror →
979
- * GitHub, fast-forward only. The worker holds no credential and never
980
- * performs a network git operation itself (#125 item 3); this is the only
981
- * route its commits take out, and it is also what stops a per-run repository
982
- * from being the *only* copy when the tree is removed.
858
+ * GitHub, fast-forward only. The dispatcher performs every network git
859
+ * operation for a run so the settlement record and the branch cannot
860
+ * disagree (#126); this is the only route a worker's commits take out, and it
861
+ * is also what stops a per-run repository from being the *only* copy when the
862
+ * tree is removed.
983
863
  */
984
864
  const publish: RunPublisher = async () => {
985
865
  if (runRepo === undefined) return { ok: false, stderr: "the run repository was never provisioned" };
@@ -1035,126 +915,12 @@ async function handleIssue(d: Deps, r: Routed, attempt: number): Promise<void> {
1035
915
  // directory and learns the real path back from the result. Inventing one
1036
916
  // here would put a file that never gets written into an escalation.
1037
917
  //
1038
- // Per run rather than one shared directory: under `uid-pool` the session
1039
- // writes its own transcript as its slot principal, and a directory shared
1040
- // with every other run would let each read the others'. The daemon still
1041
- // reads all of them — it owns the group.
1042
- //
1043
- // Under `uid-pool` the session runs as a *different uid*, so every tree it
1044
- // has to reach lives outside the private state directory: that one stays
1045
- // 0700 because it holds conductor.db and the WAL SQLite keeps recreating,
1046
- // and making it searchable to let a slot in would publish fleet history to
1047
- // every local account. `sandbox-exec` and `none` keep the old paths — same
1048
- // uid, nothing to traverse into (#125).
1049
- const runTreeRoot = d.boundary?.probe.mechanism === "uid-pool" ? sharedRoot() : stateDir();
1050
- if (runTreeRoot !== stateDir()) {
1051
- mkdirSync(runTreeRoot, { recursive: true });
1052
- chmodSync(runTreeRoot, 0o711);
1053
- }
918
+ // Per run rather than one shared directory, so one run's transcript cannot
919
+ // be truncated or replaced by the next.
920
+ const runTreeRoot = stateDir();
1054
921
  const sessionDir = join(runTreeRoot, "sessions", `run-${String(runId)}`);
1055
- const envRoot = join(runTreeRoot, "boundaries", `run-${String(runId)}`);
1056
922
  mkdirSync(sessionDir, { recursive: true });
1057
923
 
1058
- // ---- the credential boundary for this run (#125) --------------------
1059
- slotIndex = d.boundary?.slots.acquire();
1060
- boundary = buildSessionBoundary({
1061
- probe: d.boundary?.probe ?? NO_BOUNDARY_PROBE,
1062
- isolation: d.boundary?.isolation ?? "none",
1063
- role: "worker",
1064
- ...(slotIndex === undefined ? {} : { slot: slotIndex }),
1065
- envRoot,
1066
- writeRoots: [worktreePath, sessionDir],
1067
- // The workspace root, denied wholesale and then re-granted for this run's
1068
- // own checkout by the trailing allow in `sandboxProfile`. On `uid-pool`
1069
- // the modes already do this; on macOS the profile is the only thing that
1070
- // does, because sandbox-exec does not change the uid.
1071
- //
1072
- // `mirrorRoot` is deliberately NOT denied. The run repository borrows its
1073
- // objects from the mirror through git alternates, so denying it would
1074
- // break every git command in the checkout — and a run being able to read
1075
- // another run's objects out of that shared store is the residual this
1076
- // design accepts and documents, not one it pretends to have closed. The
1077
- // mirror stays unwritable, because it is not in `writeRoots`.
1078
- denyReadRoots: [...credentialDenyRoots(), project.workspaceRoot],
1079
- denyReadFiles: credentialDenyFiles(),
1080
- ...(credentials.readToken === undefined ? {} : { readToken: credentials.readToken }),
1081
- // Resolved from the operator's own home at startup, because the session's
1082
- // redirected $HOME cannot see it (#152). Admission already refused the whole
1083
- // pass when it is absent, so undefined here is a fleet not dispatching.
1084
- ...(d.model?.credential === undefined ? {} : { model: d.model.credential }),
1085
- });
1086
-
1087
- if (boundary.principal !== undefined) {
1088
- // Ownership handoff, then proof. `applyRunOwnership` makes the tree
1089
- // `<slot>:conductor-daemon 2770`, which is also the point of no return:
1090
- // if this daemon is not an *effective* member of that group it has just
1091
- // locked itself out of push, salvage and reclaim, and the modes look
1092
- // perfectly correct while it does. So it writes, reads and removes a
1093
- // probe file rather than inspecting them (#125).
1094
- // Both gids come from the probe's own `getent`, never from the daemon's
1095
- // `egid`. They are different groups: the shipped unit runs
1096
- // `User=fleet`/`Group=fleet` with `conductor-daemon` merely
1097
- // supplementary, so `egid` is `fleet` — handing that to
1098
- // `applyRunOwnership` would make every run repo `<slot>:fleet`, readable
1099
- // by anyone else in `fleet`, while the probe had validated a group the
1100
- // modes never used. A uid-pool probe always resolves both, so their
1101
- // absence here is a contradiction rather than a default to paper over.
1102
- const { daemonGid, runsGid } = d.boundary?.probe ?? {};
1103
- if (daemonGid === undefined || runsGid === undefined) {
1104
- throw new Error(
1105
- `the credential boundary reported a per-run principal without resolving ` +
1106
- `${CONDUCTOR_GROUPS.daemon}/${CONDUCTOR_GROUPS.runs} gids — refusing to hand the tree to a group ` +
1107
- `nobody verified.`,
1108
- );
1109
- }
1110
- applyRunOwnership(worktreePath, boundary.principal, daemonGid);
1111
- applyRunOwnership(sessionDir, boundary.principal, daemonGid);
1112
- // The session's own HOME, TMPDIR and gh-config tree.
1113
- // `prepareSessionEnvRoot` creates them daemon-owned 0700, so without this
1114
- // the slot principal cannot write its own home and the harness fails at
1115
- // session startup — which reads like a broken install rather than a
1116
- // permissions handoff that was one directory short.
1117
- applyRunOwnership(envRoot, boundary.principal, daemonGid);
1118
- // The claim, checked rather than assumed. On Linux the principal
1119
- // separates by DAC alone, so whether the operator's credentials are
1120
- // actually out of reach depends on modes this package never set. Ask the
1121
- // slot itself, and refuse the dispatch if the answer is yes.
1122
- const leaking = await credentialReadRefusal(
1123
- spawnCaptured,
1124
- boundary.principal,
1125
- { boundingSet: d.boundary?.probe.boundingSetDropped ?? false },
1126
- [...credentialDenyRoots(), ...credentialDenyFiles()],
1127
- );
1128
- if (leaking !== undefined) throw new Error(leaking);
1129
- // The other half of the layout, and it is not optional: the run's git dir
1130
- // borrows the mirror's objects through `alternates`, so a mirror the slot
1131
- // principal cannot read makes the checkout look corrupt rather than
1132
- // forbidden.
1133
- //
1134
- // Both trees are checked first, and the check REFUSES rather than
1135
- // widening the state directory — see `mirrorTraversalRefusal`. A slot
1136
- // that cannot reach its own checkout is just as broken as one that cannot
1137
- // read the mirror, and the shipped defaults put both under the private
1138
- // root.
1139
- const mirrorPath = mirrorPathFor(r.repo, project.mirrorRoot);
1140
- for (const tree of [mirrorPath, worktreePath]) {
1141
- const unreachable = mirrorTraversalRefusal(tree, stateDir());
1142
- if (unreachable !== undefined) throw new Error(unreachable);
1143
- }
1144
- applyMirrorReadAccess(mirrorPath, runsGid);
1145
- const denied = verifyDaemonAccess(worktreePath);
1146
- if (denied !== undefined) throw new Error(denied);
1147
- }
1148
-
1149
- // An MCP server carrying its own PAT re-opens the hole the principal
1150
- // closes, so this is a refusal and not a warning. Audited here, after
1151
- // provisioning, because both roots the harness will discover — the run's
1152
- // own checkout and the agent principal's config root — exist only now.
1153
- const mcp = mcpRefusal(
1154
- auditMcpRoots({ agentHome: boundary.env["HOME"] ?? homedir(), cwd: worktreePath }),
1155
- );
1156
- if (mcp !== undefined) throw new Error(mcp);
1157
-
1158
924
  // ---- the run's mutation channel (#126) -------------------------------
1159
925
  // A shared, daemon-owned 0711 parent with one 0600 socket per run, never a
1160
926
  // per-run *directory*: a directory owned by the run principal would hand
@@ -1179,7 +945,6 @@ async function handleIssue(d: Deps, r: Routed, attempt: number): Promise<void> {
1179
945
  repo: r.repo,
1180
946
  runRepoPath: worktreePath,
1181
947
  branch,
1182
- ...(boundary.principal === undefined ? {} : { principal: boundary.principal }),
1183
948
  },
1184
949
  { ...(d.verbPeerReader === undefined ? {} : { peerReader: d.verbPeerReader }) },
1185
950
  );
@@ -1205,13 +970,9 @@ async function handleIssue(d: Deps, r: Routed, attempt: number): Promise<void> {
1205
970
  caps,
1206
971
  maxTurns: () => turnLimit?.maxTurns() ?? caps.workerMaxTurns,
1207
972
  sessionDir,
1208
- boundary,
1209
- // Inside the run's own boundary root, so a slot principal can reach it
1210
- // by traversal. A socket under the daemon's 0700 home would be
1211
- // unreachable to the very process it exists for (#125).
1212
- // Derived from the hoisted envRoot for the same reason, and because
1213
- // that tree has already been handed to the slot principal.
1214
- socketPath: join(envRoot, "ipc.sock"),
973
+ // The session's control socket, under the daemon's own state directory —
974
+ // a child process of the daemon reaches it directly.
975
+ socketPath: join(sessionDir, "ipc.sock"),
1215
976
  verbSocketPath: verbListener.path,
1216
977
  onChildLog: (line) => {
1217
978
  log(`#${issue} ${line}`);
@@ -1236,14 +997,6 @@ async function handleIssue(d: Deps, r: Routed, attempt: number): Promise<void> {
1236
997
  // PR verification or terminal row writes can leave stale `running` state.
1237
998
  turnLimit?.close();
1238
999
  turnLimit = undefined;
1239
- // The slot IS the principal, so holding it past the session means the
1240
- // next run could be handed the same uid while this one's tree still
1241
- // exists — two live runs able to write each other's checkout, which is
1242
- // the cross-run property this whole change buys.
1243
- if (slotIndex !== undefined) {
1244
- d.boundary?.slots.release(slotIndex);
1245
- slotIndex = undefined;
1246
- }
1247
1000
  }
1248
1001
 
1249
1002
  // A configured model the harness could not honour means this run was done by
@@ -1459,13 +1212,6 @@ async function handleIssue(d: Deps, r: Routed, attempt: number): Promise<void> {
1459
1212
  turnLimit = undefined;
1460
1213
  const detail = errText(err);
1461
1214
  log(`#${issue} errored: ${detail}`);
1462
- // Released here as well: a dispatch that failed before `runWorker` never
1463
- // reached the `finally` above, and a leaked slot permanently shrinks the
1464
- // pool until the daemon restarts.
1465
- if (slotIndex !== undefined) {
1466
- d.boundary?.slots.release(slotIndex);
1467
- slotIndex = undefined;
1468
- }
1469
1215
  // A crash lands anywhere, including mid-edit in a tree holding the only
1470
1216
  // copy of real work. Nothing else on this path so much as looks at it.
1471
1217
  const settlement =
@@ -1948,7 +1694,7 @@ function planUsageEscalation(project: string, plan: PlanUsageStatus): Escalation
1948
1694
  * that grows a field has no business breaking these tests.
1949
1695
  */
1950
1696
  export async function admitCandidates(
1951
- d: Pick<Deps, "project" | "caps" | "tracker" | "store" | "escalate" | "usage" | "boundary" | "model">,
1697
+ d: Pick<Deps, "project" | "caps" | "tracker" | "store" | "escalate" | "usage">,
1952
1698
  routed: Routed[],
1953
1699
  slots: number,
1954
1700
  ): Promise<AdmissionPass> {
@@ -1960,73 +1706,6 @@ export async function admitCandidates(
1960
1706
  holds.push({ issue, reason });
1961
1707
  };
1962
1708
 
1963
- // The fail-closed the issue asks for, and it only ever fires because the
1964
- // operator asked for a boundary by name. Placed above every other gate,
1965
- // including the tracker calls: a fleet that must not dispatch must not spend
1966
- // API budget discovering it, and it must not claim an issue it will refuse.
1967
- //
1968
- // Deliberately NOT a downgrade to group-mode. That is a different, weaker
1969
- // claim, and silently substituting it is how an operator ends up believing
1970
- // they have a boundary they do not have (#125).
1971
- if (boundaryRefusesDispatch(d.boundary) && d.boundary !== undefined) {
1972
- const refusal = boundaryRefusal(d.boundary.probe);
1973
- for (const r of routed) hold(r.issue.number, "credential-boundary");
1974
- log(`credential boundary holding ${String(routed.length)} candidate(s): ${refusal}`);
1975
- if (!d.boundary.paged) {
1976
- d.boundary.paged = true;
1977
- await safeEscalate(d, {
1978
- tier: 2,
1979
- project: project.name,
1980
- issue: NO_ISSUE,
1981
- summary: `${project.name} cannot dispatch: credentials.isolation is "per-run" and this host offers no mechanism`,
1982
- detail: [refusal, "", "Nothing is dispatched until this is resolved. No work has been claimed."].join("\n"),
1983
- });
1984
- }
1985
- return { admitted: [], holds };
1986
- }
1987
-
1988
- // One layer down from the boundary above: that asks whether this host can run a
1989
- // session as its own principal, this asks whether a session can do anything at
1990
- // all once started. Every worker on the reference fleet died at turn 0 with
1991
- // "No model selected" and consumed an issue attempt for it — three issues in
1992
- // eleven minutes, while the fleet looked busy (#152).
1993
- //
1994
- // Above the tracker calls for the same reason as the boundary: a fleet that
1995
- // cannot dispatch must not claim an issue it will fail, and must not spend API
1996
- // budget discovering that.
1997
- if (d.model !== undefined && d.model.credential === undefined) {
1998
- for (const r of routed) hold(r.issue.number, "no-model-credential");
1999
- log(`no model credential holding ${String(routed.length)} candidate(s)`);
2000
- if (!d.model.paged) {
2001
- d.model.paged = true;
2002
- await safeEscalate(d, {
2003
- tier: 2,
2004
- project: project.name,
2005
- issue: NO_ISSUE,
2006
- summary: `${project.name} cannot dispatch: no model credential is reachable from a worker session`,
2007
- detail: [
2008
- "Sessions run with $HOME redirected into a per-run tree, so the harness never reads the",
2009
- "operator's own configuration — that is what keeps a credential-bearing MCP server out of",
2010
- "every session. It also means a login recorded only in ~/.omp/agent/agent.db is invisible to",
2011
- "a worker, which then dies at turn 0 with \"No model selected\".",
2012
- "",
2013
- "Two supported shapes, either of which fixes it:",
2014
- " * a model API key in the daemon's environment (ANTHROPIC_API_KEY, OPENAI_API_KEY, …)",
2015
- " * the harness's auth broker — `omp auth-broker serve` plus `omp auth-broker token`, or",
2016
- " auth.broker.url / auth.broker.token in ~/.omp/agent/config.yml. Refresh tokens stay in",
2017
- " the broker and each session gets a short-lived access token, which is what per-run",
2018
- " isolation wants.",
2019
- "",
2020
- "Copying agent.db into each run is deliberately NOT how this works: the harness rotates the",
2021
- "OAuth credentials in it, so a copy goes stale and can invalidate the original.",
2022
- "",
2023
- "Nothing is dispatched until then. No work has been claimed, and no attempt was spent.",
2024
- ].join("\n"),
2025
- });
2026
- }
2027
- return { admitted: [], holds };
2028
- }
2029
-
2030
1709
  // The plan allowance is a fleet-wide question, so it is asked once per pass
2031
1710
  // and answers for every candidate — unlike every gate below it, which is
2032
1711
  // per-issue. It sits here rather than beside the spend cap in `tick` for one
@@ -2492,20 +2171,6 @@ export interface DaemonHealthSnapshot {
2492
2171
  rssBytes: number;
2493
2172
  dispatch?: DispatchSummary;
2494
2173
  codeGraph?: CodeGraphHealth;
2495
- /**
2496
- * The boundary THIS daemon resolved at startup.
2497
- *
2498
- * Published because capabilities are a property of the process, not the host:
2499
- * `omp-conductor status` runs in an interactive shell with none of the unit's
2500
- * ambient capabilities and no conductor groups, so re-probing there reports
2501
- * `group-mode` on a fleet that is genuinely `uid-pool`. The operator's
2502
- * verification step would say the boundary failed while it was working.
2503
- *
2504
- * It is also the only answer that survives a `daemon-reload` without a
2505
- * restart: the config on disk can say anything, this is what the running
2506
- * process actually got.
2507
- */
2508
- boundary?: { isolation: CredentialIsolation; mechanism: IsolationMechanism; headline: string };
2509
2174
  }
2510
2175
 
2511
2176
  export function daemonHealthSnapshot(
@@ -2514,7 +2179,6 @@ export function daemonHealthSnapshot(
2514
2179
  paused = isPaused(),
2515
2180
  rssBytes = process.memoryUsage().rss,
2516
2181
  codeGraph?: CodeGraphHealth,
2517
- boundary?: DaemonHealthSnapshot["boundary"],
2518
2182
  ): DaemonHealthSnapshot {
2519
2183
  const dispatch = store.latestDispatch(project);
2520
2184
  return {
@@ -2525,7 +2189,6 @@ export function daemonHealthSnapshot(
2525
2189
  rssBytes,
2526
2190
  ...(dispatch === undefined ? {} : { dispatch }),
2527
2191
  ...(codeGraph?.configured === true ? { codeGraph } : {}),
2528
- ...(boundary === undefined ? {} : { boundary }),
2529
2192
  };
2530
2193
  }
2531
2194
 
@@ -3272,47 +2935,12 @@ export async function reconcileOrphanedRuns(
3272
2935
  // ------------------------------------------------------------------- the daemon
3273
2936
 
3274
2937
  export async function runDaemon(o: DaemonOpts = {}): Promise<void> {
3275
- // Before anything reads the config for real: a file written before #125 gains
3276
- // an explicit `credentials.isolation` here, so the answer lives on disk
3277
- // rather than being inherited from a default nobody chose. Non-fatal by
3278
- // design — a config the daemon cannot rewrite still loads as `"none"` and
3279
- // still dispatches, because an outage caused by a security feature is worse
3280
- // than the day before it shipped.
3281
- try {
3282
- const migration = migrateCredentialsOnDisk();
3283
- if (migration.migrated.length > 0) {
3284
- log(
3285
- `migrated ${migration.migrated.join(", ")} to an explicit credentials.isolation: "none" in ` +
3286
- `${migration.path} — this fleet is UNPROTECTED: sessions run as this daemon's user and can reach ` +
3287
- `its GitHub credential. Set credentials.isolation to "per-run" once the host is provisioned (README).`,
3288
- );
3289
- }
3290
- } catch (err) {
3291
- log(`could not persist the credentials migration (${errText(err)}) — continuing with isolation "none"`);
3292
- }
3293
-
3294
2938
  const cfg = loadConfig();
3295
2939
  const project = findProject(cfg, o.project);
3296
2940
  const caps = resolveCaps(project, cfg.defaults);
3297
2941
  const store = openStore(dbPath());
3298
2942
  const tracker = makeTracker(project);
3299
2943
 
3300
- // The host capability probe, once, at startup. It states the mechanism
3301
- // rather than guessing, and it proves the launcher works against a trivial
3302
- // child before reporting one available — a mechanism whose argv the host
3303
- // rejects looks identical here and then fails every dispatch at run time.
3304
- const credentials = resolveCredentials(project);
3305
- const probe = await probeHost({ slots: caps.maxConcurrentWorkers });
3306
- const boundary: FleetBoundary = {
3307
- isolation: credentials.isolation,
3308
- probe,
3309
- slots: createSlotPool(caps.maxConcurrentWorkers),
3310
- paged: false,
3311
- };
3312
- const described = describeBoundary(credentials.isolation, probe);
3313
- log(`credential boundary: ${described.headline}`);
3314
- for (const line of described.detail) log(`credential boundary: ${line}`);
3315
-
3316
2944
  // #126's transport, stated at startup rather than guessed at first use. The
3317
2945
  // banner names what this host can actually enforce — whether the kernel will
3318
2946
  // vouch for a caller's uid, and whether runs get distinct principals at all —
@@ -3321,13 +2949,7 @@ export async function runDaemon(o: DaemonOpts = {}): Promise<void> {
3321
2949
  const verbPeerReader = peerCredentialReader();
3322
2950
  const verbActions = githubVerbActions(project);
3323
2951
  let orchestratorVerbs: VerbListener | undefined;
3324
- log(
3325
- `verb transport: ${transportBanner(
3326
- ensureVerbSocketDir(stateDir()),
3327
- verbPeerReader,
3328
- probe.mechanism === "uid-pool",
3329
- )}`,
3330
- );
2952
+ log(`verb transport: ${transportBanner(ensureVerbSocketDir(stateDir()), verbPeerReader)}`);
3331
2953
 
3332
2954
  // Recorded here, before a single tick runs, so that the deploy an operator
3333
2955
  // *means* to do never trips the tripwire: installing a new build and
@@ -3335,19 +2957,6 @@ export async function runDaemon(o: DaemonOpts = {}): Promise<void> {
3335
2957
  // the other thing — the package changing while the daemon that dispatches
3336
2958
  // work is holding it open, whether that is a worker that wandered out of its
3337
2959
  // worktree or a human editing the live install "just to test something".
3338
- // Resolved once, here, for the same reason the host probe above is: every
3339
- // session of every tick must agree, and a fleet with no way to authenticate a
3340
- // model must refuse at admission rather than burn an attempt per dispatch
3341
- // (#152).
3342
- const model: { credential?: ModelCredential; paged: boolean } = { paged: false };
3343
- const resolvedModel = resolveModelCredential();
3344
- if (resolvedModel !== undefined) model.credential = resolvedModel;
3345
- log(
3346
- resolvedModel === undefined
3347
- ? "model credential: NONE reachable from a session — dispatch is held until an API key or an auth broker is configured"
3348
- : `model credential: ${resolvedModel.kind === "env" ? "from this daemon's environment" : "auth broker, injected per session"}`,
3349
- );
3350
-
3351
2960
  const integrity: IntegrityGate = { baseline: packageManifest(), paged: false };
3352
2961
  log(`package integrity baseline: ${integrity.baseline.size} files under ${import.meta.dir}`);
3353
2962
 
@@ -3427,64 +3036,12 @@ export async function runDaemon(o: DaemonOpts = {}): Promise<void> {
3427
3036
  log("orchestrator: external — tier-1 escalations post as issue comments for the external session's drain duty");
3428
3037
  } else {
3429
3038
  try {
3430
- // Its own principal, distinct from every worker slot. The orchestrator
3431
- // reads the state directory and its briefs and must have no read or
3432
- // write access to any run checkout so `workspaceRoot` and the mirror
3433
- // are denied outright here. This is the ONLY mechanical file gate an
3434
- // orchestrator gets: the tool-layer jail was deleted in 0.4.3 (#143)
3435
- // because it could only be installed in sessions this daemon spawns, and
3436
- // the principal refuses the syscall `bash` would make anyway, which the
3437
- // jail never did. It is also conditional, and honestly so: on a host with
3438
- // no isolating mechanism there is no principal and no gate, which is what
3439
- // `status` reports as `unprotected`.
3440
- // Same rule as a worker's, for the same reason: under `uid-pool` the
3441
- // orchestrator is its own uid, so every tree it must reach lives outside
3442
- // the 0700 private state directory. Its principal is in `conductor-runs`
3443
- // and deliberately NOT in the daemon group, so it could not traverse
3444
- // there even once — and widening the state directory is the one fix that
3445
- // is not available, because that is where conductor.db lives.
3446
- const orchTreeRoot = probe.mechanism === "uid-pool" ? sharedRoot() : stateDir();
3447
- if (orchTreeRoot !== stateDir()) {
3448
- mkdirSync(orchTreeRoot, { recursive: true });
3449
- chmodSync(orchTreeRoot, 0o711);
3450
- }
3451
- const orchEnvRoot = join(orchTreeRoot, "boundaries", "orchestrator");
3039
+ // The orchestrator is a child process of this daemon running as its own
3040
+ // user. Nothing mechanically stops it reading a run checkout what holds
3041
+ // it is its brief and the verb ledger (#143).
3042
+ const orchTreeRoot = stateDir();
3452
3043
  const orchCwd = join(orchTreeRoot, "orchestrator");
3453
3044
  mkdirSync(orchCwd, { recursive: true });
3454
- const orchestratorBoundary = buildSessionBoundary({
3455
- probe,
3456
- isolation: credentials.isolation,
3457
- role: "orchestrator",
3458
- envRoot: orchEnvRoot,
3459
- writeRoots: [orchCwd],
3460
- denyReadRoots: [...credentialDenyRoots(), project.workspaceRoot, project.mirrorRoot],
3461
- denyReadFiles: credentialDenyFiles(),
3462
- ...(credentials.readToken === undefined ? {} : { readToken: credentials.readToken }),
3463
- ...(model.credential === undefined ? {} : { model: model.credential }),
3464
- });
3465
- if (orchestratorBoundary.principal !== undefined) {
3466
- const gid = probe.daemonGid;
3467
- if (gid === undefined) {
3468
- throw new Error(
3469
- `the orchestrator principal was allocated without a resolved ${CONDUCTOR_GROUPS.daemon} gid`,
3470
- );
3471
- }
3472
- applyRunOwnership(orchEnvRoot, orchestratorBoundary.principal, gid);
3473
- applyRunOwnership(orchCwd, orchestratorBoundary.principal, gid);
3474
- // The same empirical gate a worker gets. The orchestrator is the
3475
- // session with merge and release authority, so "it cannot reach the
3476
- // credential" matters more here, not less — and it was the half that
3477
- // was never checked. `sharedGroup: false` because it is launched with
3478
- // no supplementary group at all, so the probe has to run under exactly
3479
- // that identity rather than a worker's.
3480
- const orchLeaking = await credentialReadRefusal(
3481
- spawnCaptured,
3482
- orchestratorBoundary.principal,
3483
- { boundingSet: probe.boundingSetDropped, sharedGroup: false },
3484
- [...credentialDenyRoots(), ...credentialDenyFiles()],
3485
- );
3486
- if (orchLeaking !== undefined) throw new Error(orchLeaking);
3487
- }
3488
3045
  // A third socket, distinct from every run's, in the same daemon-owned
3489
3046
  // 0711 parent. This is what makes "merge authority is the orchestrator's"
3490
3047
  // a property of the channel: the daemon knows which session is speaking
@@ -3497,9 +3054,6 @@ export async function runDaemon(o: DaemonOpts = {}): Promise<void> {
3497
3054
  path: verbSocketPath(ensureVerbSocketDir(orchTreeRoot), "orchestrator"),
3498
3055
  project: project.name,
3499
3056
  role: "orchestrator",
3500
- ...(orchestratorBoundary.principal === undefined
3501
- ? {}
3502
- : { principal: orchestratorBoundary.principal }),
3503
3057
  },
3504
3058
  { ...(verbPeerReader === undefined ? {} : { peerReader: verbPeerReader }) },
3505
3059
  );
@@ -3507,8 +3061,7 @@ export async function runDaemon(o: DaemonOpts = {}): Promise<void> {
3507
3061
  cwd: orchCwd,
3508
3062
  brief,
3509
3063
  releaseGrants,
3510
- boundary: orchestratorBoundary,
3511
- socketPath: join(orchEnvRoot, "ipc.sock"),
3064
+ socketPath: join(orchCwd, "ipc.sock"),
3512
3065
  verbSocketPath: orchestratorVerbs.path,
3513
3066
  onChildLog: (line) => {
3514
3067
  log(`orchestrator ${line}`);
@@ -3561,8 +3114,6 @@ export async function runDaemon(o: DaemonOpts = {}): Promise<void> {
3561
3114
  const d: Deps = {
3562
3115
  project,
3563
3116
  caps,
3564
- boundary,
3565
- model,
3566
3117
  tracker,
3567
3118
  store,
3568
3119
  // Process-wide, so a `status` served off this daemon's own HTTP surface
@@ -3669,12 +3220,7 @@ export async function runDaemon(o: DaemonOpts = {}): Promise<void> {
3669
3220
  project: project.name,
3670
3221
  store,
3671
3222
  turnLimits,
3672
- health: () =>
3673
- daemonHealthSnapshot(store, project.name, undefined, undefined, codeGraph, {
3674
- isolation: boundary.isolation,
3675
- mechanism: boundary.probe.mechanism,
3676
- headline: describeBoundary(boundary.isolation, boundary.probe).headline,
3677
- }),
3223
+ health: () => daemonHealthSnapshot(store, project.name, undefined, undefined, codeGraph),
3678
3224
  }),
3679
3225
  });
3680
3226
  log(`serving /healthz on :${server.port}, project ${project.name}`);