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.
package/README.md CHANGED
@@ -149,6 +149,21 @@ be present. If you run omp, it is.
149
149
  Also required on the host:
150
150
 
151
151
  - `bun`: the CLI and the daemon run on it (`Bun.serve` backs `/healthz`).
152
+ - **A model credential a *session* can reach.** Sessions inherit the daemon's
153
+ environment and `$HOME` unmodified, so a session authenticates with whatever
154
+ the daemon authenticates with, and with nothing else. A login recorded under a
155
+ different account's `~/.omp/agent/agent.db` is invisible to the service, and
156
+ its workers die at turn 0 with `No model selected`. Two supported shapes:
157
+ - a model API key in the **daemon's** environment (`ANTHROPIC_API_KEY`,
158
+ `OPENAI_API_KEY`, …).
159
+ - the harness's **auth broker** — `omp auth-broker serve` plus `omp auth-broker
160
+ token`, or `auth.broker.url` / `auth.broker.token` in
161
+ `~/.omp/agent/config.yml`. The conductor resolves that connection and injects
162
+ it into each session. Refresh tokens stay in the broker and a session receives
163
+ only short-lived access tokens.
164
+
165
+ With neither, `omp-conductor status` reports the hold and **nothing is
166
+ dispatched** — no issue is claimed and no attempt is spent.
152
167
  - `gh`, already authenticated: every tracker operation shells out to it, so the
153
168
  daemon never handles a GitHub token itself.
154
169
  - `git`: mirrors and worktrees.
@@ -870,10 +885,12 @@ bad merge.
870
885
 
871
886
  ## Host sizing and memory
872
887
 
873
- Workers are **in-process** omp sessions inside the daemon's single PID (plus one
874
- long-lived orchestrator session). systemd's Memory peak for `omp-conductor.service`
875
- is therefore daemon + every live worker + the orchestrator + any MCP stdio
876
- children those sessions mount not a separate worker process list.
888
+ Workers are **child processes** of the daemon (plus one long-lived orchestrator
889
+ session), each its own pid, talking back over a unix socket. They are still
890
+ inside the service's cgroup, so systemd's Memory peak for
891
+ `omp-conductor.service` is daemon + every live worker + the orchestrator + any
892
+ MCP stdio children those sessions mount. `MemoryMax=` governs that whole total,
893
+ not one process.
877
894
 
878
895
  On the reference deploy that produced [issue #51](https://github.com/TerrifiedBug/conductor/issues/51):
879
896
 
@@ -910,7 +927,7 @@ rest. `0` is a real value (a hard stop), not "unset".
910
927
 
911
928
  | Cap | Default | What it protects |
912
929
  | --- | --- | --- |
913
- | `maxConcurrentWorkers` | `2` (setup may write `1` on <16 GiB hosts) | Parallel in-process omp sessions inside the daemon PID. Two, because **CI runner slots, not model tokens, are the usual throughput ceiling** — a third worker would starve its own PR checks on a small self-hosted runner pool. On hosts under ~16 GiB RAM, prefer `1` so the unit stays out of swap ([host sizing](#host-sizing-and-memory)). Raise it only if you actually have the runners *and* the RAM. |
930
+ | `maxConcurrentWorkers` | `2` (setup may write `1` on <16 GiB hosts) | Parallel omp sessions, each a child process of the daemon and all inside its cgroup. Two, because **CI runner slots, not model tokens, are the usual throughput ceiling** — a third worker would starve its own PR checks on a small self-hosted runner pool. On hosts under ~16 GiB RAM, prefer `1` so the unit stays out of swap ([host sizing](#host-sizing-and-memory)). Raise it only if you actually have the runners *and* the RAM. |
914
931
  | `dailySpendUsd` | `25` | Rolling-day spend ceiling in USD, or `null` for no spend gate. `0` is a hard stop. Metered from assistant `usage.cost.total`. |
915
932
  | `planUsage` | `null` (unmetered) | Subscription/plan allowance guard: `{ "windowId": "anthropic:7d", "maxUsedFraction": 0.85 }`, or `null` for no plan gate. Independent of `dailySpendUsd` — see [Plan allowance](#plan-allowance-planusage) below. |
916
933
  | `workerMaxTurns` | `120` | Startup ceiling for each new worker. Catches a session looping without converging; use `omp-conductor extend` to raise one live run without changing this default. |
@@ -1376,6 +1393,7 @@ persists the class on the row, and performs the one recovery that class names.
1376
1393
 
1377
1394
  | Class | Signals | Recovery | Budget |
1378
1395
  | --- | --- | --- | --- |
1396
+ | `env-start-failure` | turn 0 plus an explicit harness start error (`No model selected`, a rejected key) | escalate — the session never read the issue | none |
1379
1397
  | `settlement-stuck` | a row carrying a PR that has since merged | settle: release the label, mark the row merged | none |
1380
1398
  | `merge-conflict` | `pushed-green`, PR open, GitHub reports conflicting | requeue for a rebase continuation | continuation |
1381
1399
  | `question` | the worker stopped to ask something (`blocked`) | escalate, carrying the worker's own report as evidence | none |
@@ -1394,9 +1412,10 @@ cause nobody has named — the behaviour this exists to end.
1394
1412
 
1395
1413
  ### The budgets follow the cause
1396
1414
 
1397
- `failuresFor` (implementation attempts) excludes `ci-infra` and
1398
- `settlement-stuck`; `continuationsFor` excludes `admin-kill` and
1399
- `settlement-stuck`. A merge conflict *is* charged as a continuation, because a
1415
+ `failuresFor` (implementation attempts) excludes `ci-infra`, `settlement-stuck`
1416
+ and `env-start-failure`; `continuationsFor` excludes `admin-kill`,
1417
+ `settlement-stuck` and `env-start-failure`. An environment fault charges neither:
1418
+ the session never started, so nothing about the issue was attempted. A merge conflict *is* charged as a continuation, because a
1400
1419
  rebase is real work — just never as a failed implementation attempt.
1401
1420
 
1402
1421
  An **unclassified** row (every row written before 0.4.3) counts exactly as it
@@ -1536,9 +1555,6 @@ A complete, valid config for one project with two target repos:
1536
1555
  "github-release": "human",
1537
1556
  "deploy": "human"
1538
1557
  },
1539
- "credentials": {
1540
- "isolation": "per-run"
1541
- },
1542
1558
  "policy": {
1543
1559
  "merge": {
1544
1560
  "requiredChecks": ["build", "lint"],
@@ -1896,7 +1912,7 @@ omp-conductor help
1896
1912
  | `stop` | Prefer `systemctl stop omp-conductor.service` when that unit's MainPID is the live daemon — systemd then owns the stop and will not schedule a restart for the exit it just requested. Otherwise `SIGTERM`, then `SIGKILL` after a 10-second grace period. Prints `not running` when there is nothing to stop, and tags the confirmation with `(via systemctl)` when the unit path was used. |
1897
1913
  | `restart` | Prefer `systemctl restart` when the unit owns the live pid so the replacement stays supervised; otherwise `stop` then `start`, inheriting the running daemon's port and project unless a flag overrides them. The new process **salvages dirty live worktrees before orphaning** those rows — see [Deploying a new package onto a busy fleet](#deploying-a-new-package-onto-a-busy-fleet). |
1898
1914
  | `upgrade [--to VERSION] [--project NAME]` | Deterministically update the Bun-global CLI, omp plugin, Herdr recovery plugin, and managed brief as one release. Resolves the npm version and exact `gitHead`, pauses only new claims, drains active workers, installs all surfaces, reloads Herdr and the daemon, waits for pane recovery, verifies identities and fleet health twice, then restores the original dispatch state. A no-op when already current. Failure leaves dispatch paused. Must run outside a Herdr-managed session. |
1899
- | `status [--project NAME]` | Layered fleet report first: `dispatch` / `ticks` / next scheduled tick / `pane` / `recovery` / `herdr` / `telegram` / `boundary` / `brief` / `decisions` / optional `failure classes` and `code graph` / `daemon`, then the project body. The project body includes the latest completed dispatch timestamp, ready/routed/admitted counts, and bounded hold groups; API failures are marked `DEGRADED` so queue starvation cannot look idle. The next tick comes from the live heartbeat process, not a guess from log timestamps. Telegram health uses `getMe` to prove API authentication without sending a message and reports inbound bridge configuration separately. Configured graphs report prerequisites, indexed repos, timer state, and refresh freshness without blocking dispatch. A `reports` block lists everything the outbox has not delivered, with its age, and prints `pending` (nobody has it) differently from `SENDING` (outcome unknown, it may already have arrived) — see [Report delivery](#report-delivery-the-outbox). The daemon block includes `rss` from `/healthz`; live workers add a busy-deploy warning. A `.conductor-stalled` marker adds an `orchestrator STALLED since …` line. |
1915
+ | `status [--project NAME]` | Layered fleet report first: `dispatch` / `ticks` / next scheduled tick / `pane` / `recovery` / `herdr` / `telegram` / `brief` / `decisions` / optional `failure classes` and `code graph` / `daemon`, then the project body. The project body includes the latest completed dispatch timestamp, ready/routed/admitted counts, and bounded hold groups; API failures are marked `DEGRADED` so queue starvation cannot look idle. The next tick comes from the live heartbeat process, not a guess from log timestamps. Telegram health uses `getMe` to prove API authentication without sending a message and reports inbound bridge configuration separately. Configured graphs report prerequisites, indexed repos, timer state, and refresh freshness without blocking dispatch. A `reports` block lists everything the outbox has not delivered, with its age, and prints `pending` (nobody has it) differently from `SENDING` (outcome unknown, it may already have arrived) — see [Report delivery](#report-delivery-the-outbox). The daemon block includes `rss` from `/healthz`; live workers add a busy-deploy warning. A `.conductor-stalled` marker adds an `orchestrator STALLED since …` line. |
1900
1916
  | `ledger [--issue N] [--limit N]` | Every [mediated-verb](#the-mediated-verbs-126) call and how the daemon decided it: the verb, the arguments it was given, allow or refuse, the named refusal reason, and any resulting SHA. Sessions cannot push, open a PR, merge, label or release except through those verbs, so this is the record of what they *tried* as well as what they did — the thing an escalation cites. Reads (`conductor_pr_status`) are deliberately absent: a poll every thirty seconds would bury the refusals. `--issue` narrows to one issue's run; `--limit` defaults to 50. The newest few also appear in `status`. |
1901
1917
  | `board [--project NAME]` | Live keyboard-driven kanban over the same SQLite and `/healthz` truth as `status`, plus the tracker's current labels: Queue, Claimed, Running, Green, Blocked, Failed, Orphaned, and the last 24 hours of Merged and History. Columns are mutually exclusive and describe current state, not the newest run row, so a requeued issue is queued rather than failed and a closed issue is neither. Refreshes run/spend/turn values every second, and health plus the label read every ten seconds. `Enter` follows the selected transcript in place; `u` invokes the existing unblock workflow on a Blocked, Failed, or Orphaned card; `i` / `p` open the issue / PR; `r` refreshes health; `?` shows all keys. Requires an interactive terminal of at least 50×20. |
1902
1918
  | `hold [--project NAME]` | Soft stop: pause claiming **and** disarm ticks. Daemon and pane stay up. Prefer this over `pause` when the intent is "stop the conductor" without killing processes. See [Stop the conductor](#stop-the-conductor-hold--halt). |
@@ -2098,29 +2114,14 @@ Unconfined means auditable, not licensed. `orchestratorReadPaths` is retired: it
2098
2114
  is still accepted in a config and ignored, so a fleet carrying it upgrades
2099
2115
  without editing anything.
2100
2116
 
2101
- **One conditional exception, and it is a credential boundary rather than a tool
2102
- gate.** When the *daemon* starts the orchestrator (`escalation.orchestrator:
2103
- "embedded"`) on a fleet whose `credentials.isolation` resolves to an isolating
2104
- mechanism, that session also runs as its own OS principal with
2105
- `workspaceRoot`, `mirrorRoot` and the host credential denied — a syscall-level
2106
- refusal `bash` cannot talk its way around, which is why it is worth keeping
2107
- where it applies. It does not apply to an external orchestrator, and it does not
2108
- apply on a host `status` reports as `unprotected`. The installed package is
2109
- readable either way: reading your own dispatcher's source to file an accurate
2110
- bug is exactly what should never be refused (#143).
2111
-
2112
- The workers are the sessions that are gated — a mechanical worktree confinement
2113
- on structured tools, plus their own OS principal under
2114
- `credentials.isolation: "per-run"`. That asymmetry is the design: a worker is
2115
- dispatched code it did not write and runs unattended, while the orchestrator is
2116
- the session the operator is reading.
2117
-
2118
2117
  ## The mediated verbs (#126)
2119
2118
 
2120
- With the credential boundary in place a session can no longer mutate GitHub by
2121
- accident or by malice and can no longer do its job either. These verbs are the
2122
- only sanctioned way back, and the point of them is *where the checks run*: in
2123
- the daemon, across a process boundary, not in a prompt the model can rewrite.
2119
+ A session can reach `gh`: it inherits the daemon's environment, credentials and
2120
+ all. It is told not to publish with it. These verbs are the sanctioned route
2121
+ instead, because the dispatcher owns the settlement record a push or a PR the
2122
+ daemon did not perform is a run it cannot account for, and the checks that would
2123
+ have refused it never ran. The point of them is *where those checks run*: in the
2124
+ daemon, across a process boundary, not in a prompt the model can rewrite.
2124
2125
 
2125
2126
  ### The verbs
2126
2127
 
@@ -2148,35 +2149,34 @@ on run X asking to merge run Y's PR is not merely wrong, it is unexpressible.
2148
2149
 
2149
2150
  ```
2150
2151
  <state dir>/verbs/ daemon-owned, mode 0711
2151
- run-7-9a783d877d422b9e.sock 0600, owned by run 7's principal
2152
- run-9-1c40e2a5b6d3f018.sock 0600, owned by run 9's principal
2152
+ run-7-9a783d877d422b9e.sock 0600, run 7's and nothing else's
2153
+ run-9-1c40e2a5b6d3f018.sock 0600, run 9's and nothing else's
2153
2154
  orchestrator-4b1f...c2.sock 0600, a third, distinct one
2154
2155
  ```
2155
2156
 
2156
2157
  `0711` is load-bearing and the obvious `0700` does not work: *connecting* needs
2157
- **search** (`+x`) on every path component, not read. Searchable by run
2158
- principals, listable by none, and writable by none but the daemon that last
2159
- one is what stops a run unlinking a sibling's socket or binding an impostor in
2160
- its place. A per-run *directory* owned by the run principal would hand exactly
2161
- that power back, and is deliberately not used.
2158
+ **search** (`+x`) on every path component, not read. So the directory is
2159
+ traversable by a process that already knows a socket's name, listable by none,
2160
+ and writable by none but the daemon that last one is what stops anything else
2161
+ on the host unlinking a socket and binding an impostor in its place. A
2162
+ *directory* per run, owned by the run, would hand exactly that power back, and
2163
+ is deliberately not used.
2162
2164
 
2163
2165
  Before binding, the daemon verifies every component of the path is owned by
2164
2166
  itself (or root), free of symlinks, and unwritable by anyone else; a failed
2165
- check **refuses dispatch** rather than degrading. Where the run has its own
2166
- principal it also verifies the chain is traversable, and says which directory to
2167
- `chmod o+x` when it is not. Paths are unguessably suffixed, and only the daemon
2168
- ever unlinks one.
2167
+ check **refuses dispatch** rather than degrading. Paths are unguessably
2168
+ suffixed, and only the daemon ever unlinks one.
2169
2169
 
2170
2170
  Peer credentials are asserted server-side — `getpeereid` on macOS, `SO_PEERCRED`
2171
- on Linux — against the uid allocated for that run. A mismatch closes the
2172
- connection with no reply and logs an impersonation attempt. Where the host has
2173
- no per-run principals there is no distinct uid to compare, and the daemon says
2174
- so at startup rather than implying a guarantee it is not making:
2171
+ on Linux — and the daemon states at startup exactly what that buys rather than
2172
+ implying more. Sessions are child processes running under the daemon's own uid,
2173
+ so the peer check proves the caller is a local process on this host; it is the
2174
+ socket, not the uid, that says which run is calling. A connection whose peer
2175
+ cannot be read at all is closed with no reply and logged.
2175
2176
 
2176
2177
  ```
2177
2178
  verb transport: verb sockets in ~/.omp/conductor/verbs (mode 711); each socket
2178
- 0600 under the daemon's own uid (no per-run principals on this host); peer uid
2179
- asserted with getpeereid
2179
+ 0600 under the daemon's own uid; peer uid asserted with getpeereid
2180
2180
  ```
2181
2181
 
2182
2182
  **No mutation route exists on the HTTP port**, and none may be added. That
@@ -2184,8 +2184,8 @@ surface is unauthenticated loopback TCP reachable by any local user; a `PUT` or
2184
2184
  `POST` at any verb path answers 404, pinned by a test.
2185
2185
 
2186
2186
  The child-side tool handler is a thin client only. It forwards arguments and
2187
- renders the answer — no policy branch, no credential, no local fallback. With no
2188
- socket it fails closed and says so, rather than reaching for `git push`.
2187
+ renders the answer — no policy branch, no local fallback, no second route. With
2188
+ no socket it fails closed and says so, rather than reaching for `git push`.
2189
2189
 
2190
2190
  ### The ledger
2191
2191
 
@@ -2216,210 +2216,6 @@ leaves a durable record that something tried. It is no longer what *stops* a
2216
2216
  release. Treat a block there as evidence about a session's intentions; the
2217
2217
  daemon is what prevented it.
2218
2218
 
2219
- ## The credential boundary (`credentials`)
2220
-
2221
- Every prose rule about what a worker "may not" do reduces to one fact: until
2222
- this existed, **model-executed code held the operator's own GitHub write
2223
- credential.** Sessions ran inside the daemon process, as the same OS user, with
2224
- the same `$HOME` and the same filesystem view — and the daemon authenticates by
2225
- shelling out to the logged-in `gh`, so a worker's `bash` reached the same
2226
- credential by running the same binary.
2227
-
2228
- Scrubbing `GH_TOKEN` does not fix that, and this package will not describe it as
2229
- if it did. Code running as the same uid defeats an environment variable in one
2230
- line (`GH_CONFIG_DIR=$HOME/.config/gh gh pr merge …`), and the login keychain,
2231
- `~/.ssh` and `~/.git-credentials` need no environment at all. **The boundary is
2232
- an OS principal.** The environment scrubbing that ships alongside it is
2233
- accident-prevention: it makes the common mistakes fail immediately and legibly,
2234
- and it stops nothing that is trying.
2235
-
2236
- ### What actually changed
2237
-
2238
- - Worker and orchestrator sessions are **child processes**, launched under a
2239
- principal scoped to one run. They are still supervised exactly as before —
2240
- turn caps, wall-clock caps, spend accounting, salvage, transcript paths — and
2241
- a session that outlives its daemon is not possible: the child exits when its
2242
- control socket closes.
2243
- - A run's checkout is **its own git repository**, borrowing objects read-only
2244
- from the shared mirror through git alternates. It used to be a linked worktree
2245
- of that mirror, which needs write access to the mirror's refs and therefore
2246
- handed every run write access to every other run's.
2247
- - **Publishing is the daemon's.** The worker commits locally; the daemon fetches
2248
- the run branch out of the run's repository into the mirror and pushes it to
2249
- GitHub, fast-forward only. There is no force path anywhere in this package. A
2250
- rejected push settles the run `failed` with git's stderr verbatim.
2251
- - One function, `credentialedEnv()`, is the only construction site of credential
2252
- material in the daemon. `gh`, the mirror's clone and fetch, the daemon-side
2253
- push and the mediated verbs all go through it, so "what can reach the
2254
- credential" is answered by its call sites rather than by an audit.
2255
-
2256
- ### `credentials.isolation`
2257
-
2258
- | Value | Meaning |
2259
- |---|---|
2260
- | `"per-run"` | Every session runs as its own OS principal. The daemon refuses to dispatch if this host cannot provide one. |
2261
- | `"none"` | Sessions run as the daemon's user. `status` reports the fleet as **unprotected**, every time. |
2262
-
2263
- There is deliberately no third value. The degraded `group-mode` mechanism below
2264
- is something the *host probe* can report, never something a config can ask for,
2265
- so an operator cannot believe they configured the full boundary and get the
2266
- weaker one.
2267
-
2268
- **A config written before this key existed is migrated, not rejected.** On the
2269
- first start it resolves to `"none"`, the key is written into the file
2270
- explicitly, and the daemon logs the migration once by project name. The answer
2271
- then lives on disk rather than being inherited from a default nobody chose — and
2272
- no running fleet stops dispatching on upgrade. A fleet that upgrades and does
2273
- nothing else is unprotected and says so in `status`; turning that into a refusal
2274
- would have been an outage caused by a security feature.
2275
-
2276
- ### `credentials.readToken`
2277
-
2278
- Optional, and absent by default. The boundary removes GitHub *reads* too, so a
2279
- worker with no token works from its dispatch brief — which already carries the
2280
- issue body and the acceptance criteria — and from the mediated verbs. Set this
2281
- to a **read-scoped** token to give sessions `gh` reads back.
2282
-
2283
- This package cannot verify the scope. A write-scoped token here re-opens exactly
2284
- the hole the rest of this section closes.
2285
-
2286
- ### Mechanisms, and what each one does not protect
2287
-
2288
- The daemon probes the host at startup, states the mechanism, and proves it by
2289
- running the composed launcher against a trivial child before reporting it
2290
- available. A mechanism whose argv the host rejects resolves *down* at probe time
2291
- rather than failing every dispatch at run time.
2292
-
2293
- | Mechanism | Where | Contains a determined escape? |
2294
- |---|---|---|
2295
- | `uid-pool` | Linux, with the capabilities and accounts below | **Yes** |
2296
- | `sandbox-exec` | macOS | **Yes**, within a deprecated Apple interface |
2297
- | `group-mode` | Linux, groups but no capabilities | **No** — cross-run separation only |
2298
- | `none` | anywhere else | **No** |
2299
-
2300
- `group-mode` exists so an operator who cannot grant capabilities still gets
2301
- cross-run checkout separation. It runs sessions as the daemon's own uid, so it
2302
- bounds accidents and does not stop a determined same-uid escape. `status` says
2303
- which one is live, and lists what it does not close.
2304
-
2305
- ### Deploying `per-run` on a Linux fleet host
2306
-
2307
- See `systemd/omp-conductor.service.example` for the copy-pasteable version. In
2308
- summary:
2309
-
2310
- 1. **Capabilities on the existing unprivileged account.** The unit grants
2311
- `CAP_SETUID CAP_SETGID CAP_CHOWN CAP_SETPCAP`. They exist to be *dropped
2312
- into* run children, never inherited by them. A capability grant on the
2313
- `fleet` account is a narrower blast radius than running as root or shipping a
2314
- setuid binary.
2315
-
2316
- `CAP_SETPCAP` is there solely so the launcher can empty the child's
2317
- capability **bounding** set, which `PR_CAPBSET_DROP` requires. A host that
2318
- withholds it takes a documented fallback: the launcher omits
2319
- `--bounding-set=-all`, the child still ends with every other set empty behind
2320
- `NoNewPrivs`, the leftover `CapBnd` is inert, and `status` reports it as a
2321
- named residual.
2322
-
2323
- 2. **`setpriv` (util-linux) must be on `PATH`.** Every run child is launched
2324
- through it: group list, then gid, then uid, then empty the
2325
- permitted/effective/inheritable/ambient sets, drop the bounding set, set
2326
- `PR_SET_NO_NEW_PRIVS`, then `exec`. Without `setpriv` the probe reports
2327
- `none`. There is deliberately no `spawn({uid,gid})` fallback — ambient
2328
- capabilities survive `execve` for ordinary binaries, so a child launched that
2329
- way holds `CAP_SETUID` itself and can `setuid()` back to a sibling run or to
2330
- the daemon, voiding the boundary while appearing to work.
2331
-
2332
- 3. **Provision with the shipped command, not by hand.**
2333
- `omp-conductor boundary-setup --slots <n>` prints the exact idempotent root
2334
- commands, generated from the same constants the startup probe checks, so the
2335
- instructions cannot drift from what the daemon then demands.
2336
-
2337
- ```bash
2338
- omp-conductor boundary-setup --slots 2 # read it first
2339
- omp-conductor boundary-setup --slots 2 | sudo bash
2340
- sudo systemctl restart omp-conductor.service
2341
- ```
2342
-
2343
- It needs no config and must run **before** `setup`: setup writes worktree and
2344
- mirror paths into the shared root this creates. What it establishes:
2345
-
2346
- 4. **One account per run slot**, `conductor-agent-<n>` for each concurrent
2347
- worker plus `conductor-agent-orch` for the orchestrator. `maxConcurrentWorkers`
2348
- is 2 by default, so the pool is small. The orchestrator's account is distinct
2349
- precisely so it has no read or write access to any run checkout, and it is
2350
- launched with no supplementary group at all.
2351
-
2352
- 5. **Two groups**, and the split between them is the whole filesystem model:
2353
-
2354
- | Group | Members | Purpose |
2355
- |---|---|---|
2356
- | `conductor-daemon` | the daemon account **only** | lets it reach every run repo — fetch the branch, salvage, reclaim the tree |
2357
- | `conductor-runs` | every slot principal | read-only access to the shared mirror |
2358
-
2359
- A slot principal must **never** be in `conductor-daemon`. That membership is
2360
- the only thing keeping sibling runs out of each other's checkouts, and its
2361
- absence is asserted from a live session by the probe suite.
2362
-
2363
- Supplementary group membership is fixed when a process starts, so this
2364
- **requires a service restart**. Without it the daemon's live credentials lack
2365
- the group even though `getent` shows it, and it would chown every run repo to
2366
- a group it cannot itself use — the probe checks the running process's own
2367
- credentials for exactly this reason, and every run additionally write-tests
2368
- its tree after the ownership handoff.
2369
-
2370
- 6. **Layout.** Run repos are `conductor-agent-<slot>:conductor-daemon`,
2371
- directories `2770` (setgid, so what the worker creates inherits the group)
2372
- and files `0660` with execute bits preserved. The shared mirror is
2373
- daemon-owned, group `conductor-runs`, `0750`/`0640`.
2374
-
2375
- Everything a slot must reach lives under **`/var/lib/omp-conductor`** (mode
2376
- `0711`): worktrees, mirrors, per-run session transcripts and per-run boundary
2377
- homes. It is deliberately outside the state directory, which stays `0700`
2378
- because it holds `conductor.db` and the WAL files SQLite recreates at
2379
- runtime — making that searchable would publish fleet history to every local
2380
- account. Override with `$OMP_CONDUCTOR_SHARED`. Uninstall is therefore two
2381
- paths, not one.
2382
-
2383
- 7. **The daemon's home is `0711`, with its credential leaves closed**
2384
- (`.ssh`, `.config/gh` at `0700`; `.npmrc`, `.git-credentials` at `0600`).
2385
- Searchable because the runtime and the installed package live in it — a
2386
- `0700` home kills every worker before it connects, and no shell-based probe
2387
- notices — and closed at the leaves because that is where the boundary
2388
- actually rests. The daemon re-checks this empirically at dispatch, asking the
2389
- slot principal itself whether it can read any credential path, and refuses
2390
- the run if it can.
2391
-
2392
- Verify with `omp-conductor status`: the `boundary` row names the live mechanism
2393
- and lists its residuals.
2394
-
2395
- ### macOS dev hosts
2396
-
2397
- `sandbox-exec` is Apple-deprecated and is still the only thing on a stock host
2398
- that denies a *read*, which is what this needs. The generated profile makes the
2399
- run's checkout the only writable path and denies `~/.ssh`, `~/.config/gh`,
2400
- `~/.git-credentials`, `~/.npmrc`, the other run checkouts, and mach lookups of
2401
- the keychain services. Verified on darwin 25.5.0 against the full probe list.
2402
-
2403
- A dev host may also simply run `credentials.isolation: "none"`, in which case
2404
- `status` reports it unprotected. That is a legitimate choice for a machine that
2405
- is not a fleet host; it is not a default.
2406
-
2407
- ### The residual, stated plainly
2408
-
2409
- A run can **read** another run's git objects out of the shared mirror. That is
2410
- the price of not cloning the repository per run, and it is bounded: same source,
2411
- no write path, no credential. It is asserted as *permitted* by the probe suite,
2412
- so nobody can quietly "fix" it by weakening the mirror's modes.
2413
-
2414
- ### MCP servers are inside the blast radius
2415
-
2416
- A GitHub MCP server carrying its own PAT re-opens the hole. Sessions discover
2417
- MCP configuration from the **agent principal's** config root — the redirected
2418
- `$HOME` this package creates per session, not the operator's — and dispatch
2419
- **refuses with a named error** when a credential-bearing GitHub server is found
2420
- there or in the run's checkout. Put servers you want sessions to have in the
2421
- agent principal's root, without credentials.
2422
-
2423
2219
 
2424
2220
  ## Limitations
2425
2221
 
@@ -2447,18 +2243,6 @@ Known and deliberate in this version:
2447
2243
  - **No cross-process lock on the mirrors.** Two dispatch loops fetching the same
2448
2244
  repo at the same instant can collide on git's ref locks; the run fails and is
2449
2245
  retried rather than corrupted.
2450
- - **A run can read another run's git objects.** The shared mirror is one object
2451
- store, so the isolation is on writes and credentials, not reads. See
2452
- [The residual, stated plainly](#the-residual-stated-plainly).
2453
- - **`group-mode` does not contain a determined escape.** It separates run
2454
- checkouts by group and mode while sessions still run as the daemon's uid. Only
2455
- `uid-pool` and `sandbox-exec` make the stronger claim, and `status` says which
2456
- one is live.
2457
- - **A fleet that upgrades and changes nothing is unprotected.** The
2458
- `credentials` migration resolves to `"none"` and writes it down rather than
2459
- refusing to dispatch, because an outage caused by a security feature is worse
2460
- than the day before it shipped. `status` reports it every tick until an
2461
- operator provisions the host and sets `"per-run"`.
2462
2246
  - **Uniquely local mirror branches are retained.** Terminal runs are reaped
2463
2247
  automatically only after every commit exists on a remote ref. A failed salvage
2464
2248
  push deliberately leaves its branch and tree for an operator rather than
@@ -2476,7 +2260,7 @@ Known and deliberate in this version:
2476
2260
  "nobody reads it until morning" path the orchestrator exists to avoid. The warning
2477
2261
  is in `daemon.log`; nothing pages you about it.
2478
2262
  - **Workers are not terminal panes, so you cannot watch them there.** Each
2479
- worker is an in-process omp session started by `createSession`. The resident
2263
+ worker is an omp session the daemon starts as a child process. The resident
2480
2264
  daemon tracks workers in a background pool so the five-minute loop keeps
2481
2265
  settling PRs and checking capacity; shutdown waits for that pool. Herdr still
2482
2266
  shows exactly one pane (the orchestrator's) regardless of concurrency.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "omp-conductor",
3
- "version": "0.4.4",
3
+ "version": "0.5.0",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "description": "A 24/7 dispatcher that takes ready GitHub issues to green, mergeable PRs using omp coding sessions, with tiered escalation first to an orchestrator session and then to a human.",
@@ -513,62 +513,6 @@ lose the substituted coordinates and the overwrite confirmation.
513
513
 
514
514
  ---
515
515
 
516
- ## Step 5b — offer the credential boundary, and be honest about its cost
517
-
518
- Ask this once, plainly, because the default is not the safe answer — it is the
519
- *non-breaking* one:
520
-
521
- > Should worker and orchestrator sessions run under their own OS accounts, so a
522
- > session that goes rogue in bash holds no credential capable of merging,
523
- > pushing, releasing or reading your `gh` config?
524
-
525
- `credentials.isolation` is `none` unless someone says otherwise, and `status`
526
- reports such a fleet **unprotected** on every tick. That is deliberate: a fleet
527
- that refuses to dispatch the moment it upgrades is worse than one that is
528
- honestly unprotected.
529
-
530
- Say what it costs before they answer:
531
-
532
- - **Linux only** for the real thing. macOS dev hosts get `sandbox-exec`, which
533
- is Apple-deprecated but is still the only stock mechanism that denies a *read*.
534
- - **Root, once**, to create the accounts and groups.
535
- - **A service restart**, because supplementary group membership is fixed when a
536
- process starts.
537
- - Worktrees and mirrors move to `/var/lib/omp-conductor`. Uninstall becomes two
538
- paths instead of one.
539
-
540
- If they say yes, the order matters and getting it wrong looks like a bug:
541
-
542
- ```bash
543
- omp-conductor boundary-setup --slots <maxConcurrentWorkers> # read it first
544
- omp-conductor boundary-setup --slots <maxConcurrentWorkers> | sudo bash
545
- sudo systemctl restart omp-conductor.service
546
- ```
547
-
548
- **Then open a fresh shell before running the wizard.** The setup probe reads its
549
- *own* process credentials, so a stale login will refuse to offer `per-run` and
550
- that reads like a broken install rather than an un-refreshed session.
551
-
552
- Only then run `/conductor setup` and choose `per-run`. That single amend writes
553
- the isolation, relocates the roots, and regenerates the systemd unit *with* the
554
- capabilities the launcher needs — the unit is not optional, and a fleet
555
- configured `per-run` whose unit lacks them probes down to `group-mode` and
556
- refuses every issue. Install the unit with the commands the wizard prints, then
557
- confirm:
558
-
559
- ```bash
560
- omp-conductor status # boundary: per-run — uid-pool
561
- ```
562
-
563
- `status` reports the **daemon's** boundary, read from `/healthz`, not a probe of
564
- whatever shell you are standing in — capabilities belong to the process, and an
565
- interactive shell has none of the unit's. If no daemon is running it falls back
566
- to a local probe and says so; that line is not the fleet's answer.
567
-
568
- If it says anything else, the reason names the missing capability, group or
569
- path. Do not guess at it, and never "fix" it by widening the state directory —
570
- that is where `conductor.db` lives.
571
-
572
516
  ## Step 6 — check the worker brief's assumptions against reality
573
517
 
574
518
  The worker brief makes concrete claims to a session that has no other context. If
@@ -26,10 +26,10 @@ two-pass verification, and restoration of the previous dispatch state.
26
26
  ## When it fails on the systemd unit
27
27
 
28
28
  Since 0.4.0 the command also verifies the unit systemd actually booted from, not
29
- just the staged copy. A fleet configured `credentials.isolation: "per-run"`
30
- whose installed unit predates the capability grant probes down to `group-mode`
31
- and then refuses every issue and package identities, services, pane and ticks
32
- all look perfectly healthy around it, which is why it needed its own check.
29
+ just the staged copy. A unit installed before a directive the current
30
+ configuration requires keeps running without it, and package identities,
31
+ services, pane and ticks all look perfectly healthy around it, which is why it
32
+ needed its own check.
33
33
 
34
34
  The error names the missing directives. Fix it by re-staging rather than by
35
35
  hand-editing the live unit:
@@ -45,11 +45,6 @@ omp-conductor upgrade # re-run; it now verifies clean
45
45
  Only directives this configuration *requires* are checked, so an operator's own
46
46
  `MemoryMax`, `After=` or extra `Environment=` never fails an upgrade.
47
47
 
48
- What the command deliberately does **not** do is create the accounts and groups
49
- the boundary needs. That is `omp-conductor boundary-setup`, it needs root, and
50
- the daemon runs unprivileged on purpose — so it detects and instructs rather
51
- than escalating.
52
-
53
48
  Report the command's result. Do not reproduce its lifecycle as an AI checklist,
54
49
  edit installed files, publish npm, or substitute separate install/restart steps.
55
50
  If it fails, report the exact error and leave dispatch paused as the command
@@ -237,8 +237,8 @@ Not yours to relax:
237
237
 
238
238
  - **Workers stop at a green PR.** `conductor_pr_merge`, `conductor_label` and
239
239
  `conductor_release` refuse a worker session mechanically, whatever any config
240
- says, and a worker holds no credential to go around them with. This one is
241
- absolute. A worker sees one issue, so it cannot judge whether a release is
240
+ says, and a worker that reaches for `gh` instead has committed a reportable
241
+ breach. This one is absolute. A worker sees one issue, so it cannot judge whether a release is
242
242
  worth cutting, and a session that merges its own work has removed every review
243
243
  the PR existed to get. Release work is never delegated downward: if any of it
244
244
  is delegated at all, it is delegated to **you**.
@@ -279,9 +279,11 @@ Not yours to relax:
279
279
 
280
280
  ## Your verb surface
281
281
 
282
- You hold no GitHub credential. Everything below happens through conductor tools
283
- whose checks run in the dispatcher, and each one records what you asked for and
284
- what it decided — readable with `omp-conductor ledger`, including the refusals.
282
+ You can reach `gh`, and you do not publish with it. Everything below happens
283
+ through conductor tools whose checks run in the dispatcher, and each one records
284
+ what you asked for and what it decided — readable with `omp-conductor ledger`,
285
+ including the refusals. That record is the point: a merge or a release the
286
+ dispatcher did not perform is one nobody can account for afterwards.
285
287
 
286
288
  | Tool | Yours when | What the dispatcher checks before acting |
287
289
  | --- | --- | --- |
@@ -307,20 +309,12 @@ the SHA that produced it. Never work around one: there is no path around it, and
307
309
  the attempt is in the ledger.
308
310
 
309
311
  **Your file tools are not gated — the workers' are.** Every worker session runs
310
- under a mechanical worktree gate and, under `credentials.isolation: "per-run"`,
311
- its own OS principal. Your structured tools are gated by nothing, and that is a
312
- deliberate operator decision rather than an oversight. What holds YOUR
313
- boundaries is this brief, the verb ledger every `conductor_*` call writes, and
314
- the operator reading both. Never read or edit a worker's checkout or the mirror
315
- cache — when you need a run's code, read its PR. Unconfined means auditable, not
316
- licensed.
317
-
318
- One caveat, so you never assume more protection than you have: if this daemon
319
- started you *and* the fleet runs an isolating credentials mechanism, you are
320
- also a separate OS principal that genuinely cannot reach a run checkout, the
321
- mirror or the host credential — `omp-conductor status` names that mechanism on
322
- its `boundary` line. Started by your operator, or on a host that reports
323
- `unprotected`, nothing stops those reads but the paragraph above.
312
+ under a mechanical worktree gate. Your structured tools are gated by nothing,
313
+ and that is a deliberate operator decision rather than an oversight. What holds
314
+ YOUR boundaries is this brief, the verb ledger every `conductor_*` call writes,
315
+ and the operator reading both. Never read or edit a worker's checkout or the
316
+ mirror cache when you need a run's code, read its PR. Unconfined means
317
+ auditable, not licensed.
324
318
 
325
319
  **Your own** merge and release authority is not decided here, and not by this
326
320
  brief either. It is whatever your operator granted at setup time, and the