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/README.md CHANGED
@@ -149,25 +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 run with `$HOME`
153
- redirected into a per-run tree, so the harness never reads the operator's own
154
- configuration that is what keeps a credential-bearing MCP server out of every
155
- session. It also means a login recorded only in `~/.omp/agent/agent.db` is
156
- invisible to a worker, which then dies at turn 0 with `No model selected`. Two
157
- supported shapes:
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:
158
157
  - a model API key in the **daemon's** environment (`ANTHROPIC_API_KEY`,
159
- `OPENAI_API_KEY`, …); these are deliberately not scrubbed, unlike the GitHub
160
- and npm keys.
158
+ `OPENAI_API_KEY`, …).
161
159
  - the harness's **auth broker** — `omp auth-broker serve` plus `omp auth-broker
162
160
  token`, or `auth.broker.url` / `auth.broker.token` in
163
161
  `~/.omp/agent/config.yml`. The conductor resolves that connection and injects
164
162
  it into each session. Refresh tokens stay in the broker and a session receives
165
- only short-lived access tokens, which is exactly what per-run isolation wants.
163
+ only short-lived access tokens.
166
164
 
167
165
  With neither, `omp-conductor status` reports the hold and **nothing is
168
- dispatched** — no issue is claimed and no attempt is spent. Copying `agent.db`
169
- into each run is deliberately not how this works: the harness rotates the OAuth
170
- credentials in it, so a copy goes stale and can invalidate the original.
166
+ dispatched** — no issue is claimed and no attempt is spent.
171
167
  - `gh`, already authenticated: every tracker operation shells out to it, so the
172
168
  daemon never handles a GitHub token itself.
173
169
  - `git`: mirrors and worktrees.
@@ -889,10 +885,12 @@ bad merge.
889
885
 
890
886
  ## Host sizing and memory
891
887
 
892
- Workers are **in-process** omp sessions inside the daemon's single PID (plus one
893
- long-lived orchestrator session). systemd's Memory peak for `omp-conductor.service`
894
- is therefore daemon + every live worker + the orchestrator + any MCP stdio
895
- 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.
896
894
 
897
895
  On the reference deploy that produced [issue #51](https://github.com/TerrifiedBug/conductor/issues/51):
898
896
 
@@ -929,7 +927,7 @@ rest. `0` is a real value (a hard stop), not "unset".
929
927
 
930
928
  | Cap | Default | What it protects |
931
929
  | --- | --- | --- |
932
- | `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. |
933
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`. |
934
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. |
935
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. |
@@ -1557,9 +1555,6 @@ A complete, valid config for one project with two target repos:
1557
1555
  "github-release": "human",
1558
1556
  "deploy": "human"
1559
1557
  },
1560
- "credentials": {
1561
- "isolation": "per-run"
1562
- },
1563
1558
  "policy": {
1564
1559
  "merge": {
1565
1560
  "requiredChecks": ["build", "lint"],
@@ -1917,7 +1912,7 @@ omp-conductor help
1917
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. |
1918
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). |
1919
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. |
1920
- | `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. |
1921
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`. |
1922
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. |
1923
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). |
@@ -2119,29 +2114,14 @@ Unconfined means auditable, not licensed. `orchestratorReadPaths` is retired: it
2119
2114
  is still accepted in a config and ignored, so a fleet carrying it upgrades
2120
2115
  without editing anything.
2121
2116
 
2122
- **One conditional exception, and it is a credential boundary rather than a tool
2123
- gate.** When the *daemon* starts the orchestrator (`escalation.orchestrator:
2124
- "embedded"`) on a fleet whose `credentials.isolation` resolves to an isolating
2125
- mechanism, that session also runs as its own OS principal with
2126
- `workspaceRoot`, `mirrorRoot` and the host credential denied — a syscall-level
2127
- refusal `bash` cannot talk its way around, which is why it is worth keeping
2128
- where it applies. It does not apply to an external orchestrator, and it does not
2129
- apply on a host `status` reports as `unprotected`. The installed package is
2130
- readable either way: reading your own dispatcher's source to file an accurate
2131
- bug is exactly what should never be refused (#143).
2132
-
2133
- The workers are the sessions that are gated — a mechanical worktree confinement
2134
- on structured tools, plus their own OS principal under
2135
- `credentials.isolation: "per-run"`. That asymmetry is the design: a worker is
2136
- dispatched code it did not write and runs unattended, while the orchestrator is
2137
- the session the operator is reading.
2138
-
2139
2117
  ## The mediated verbs (#126)
2140
2118
 
2141
- With the credential boundary in place a session can no longer mutate GitHub by
2142
- accident or by malice and can no longer do its job either. These verbs are the
2143
- only sanctioned way back, and the point of them is *where the checks run*: in
2144
- 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.
2145
2125
 
2146
2126
  ### The verbs
2147
2127
 
@@ -2169,35 +2149,34 @@ on run X asking to merge run Y's PR is not merely wrong, it is unexpressible.
2169
2149
 
2170
2150
  ```
2171
2151
  <state dir>/verbs/ daemon-owned, mode 0711
2172
- run-7-9a783d877d422b9e.sock 0600, owned by run 7's principal
2173
- 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
2174
2154
  orchestrator-4b1f...c2.sock 0600, a third, distinct one
2175
2155
  ```
2176
2156
 
2177
2157
  `0711` is load-bearing and the obvious `0700` does not work: *connecting* needs
2178
- **search** (`+x`) on every path component, not read. Searchable by run
2179
- principals, listable by none, and writable by none but the daemon that last
2180
- one is what stops a run unlinking a sibling's socket or binding an impostor in
2181
- its place. A per-run *directory* owned by the run principal would hand exactly
2182
- 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.
2183
2164
 
2184
2165
  Before binding, the daemon verifies every component of the path is owned by
2185
2166
  itself (or root), free of symlinks, and unwritable by anyone else; a failed
2186
- check **refuses dispatch** rather than degrading. Where the run has its own
2187
- principal it also verifies the chain is traversable, and says which directory to
2188
- `chmod o+x` when it is not. Paths are unguessably suffixed, and only the daemon
2189
- ever unlinks one.
2167
+ check **refuses dispatch** rather than degrading. Paths are unguessably
2168
+ suffixed, and only the daemon ever unlinks one.
2190
2169
 
2191
2170
  Peer credentials are asserted server-side — `getpeereid` on macOS, `SO_PEERCRED`
2192
- on Linux — against the uid allocated for that run. A mismatch closes the
2193
- connection with no reply and logs an impersonation attempt. Where the host has
2194
- no per-run principals there is no distinct uid to compare, and the daemon says
2195
- 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.
2196
2176
 
2197
2177
  ```
2198
2178
  verb transport: verb sockets in ~/.omp/conductor/verbs (mode 711); each socket
2199
- 0600 under the daemon's own uid (no per-run principals on this host); peer uid
2200
- asserted with getpeereid
2179
+ 0600 under the daemon's own uid; peer uid asserted with getpeereid
2201
2180
  ```
2202
2181
 
2203
2182
  **No mutation route exists on the HTTP port**, and none may be added. That
@@ -2205,8 +2184,8 @@ surface is unauthenticated loopback TCP reachable by any local user; a `PUT` or
2205
2184
  `POST` at any verb path answers 404, pinned by a test.
2206
2185
 
2207
2186
  The child-side tool handler is a thin client only. It forwards arguments and
2208
- renders the answer — no policy branch, no credential, no local fallback. With no
2209
- 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`.
2210
2189
 
2211
2190
  ### The ledger
2212
2191
 
@@ -2237,210 +2216,6 @@ leaves a durable record that something tried. It is no longer what *stops* a
2237
2216
  release. Treat a block there as evidence about a session's intentions; the
2238
2217
  daemon is what prevented it.
2239
2218
 
2240
- ## The credential boundary (`credentials`)
2241
-
2242
- Every prose rule about what a worker "may not" do reduces to one fact: until
2243
- this existed, **model-executed code held the operator's own GitHub write
2244
- credential.** Sessions ran inside the daemon process, as the same OS user, with
2245
- the same `$HOME` and the same filesystem view — and the daemon authenticates by
2246
- shelling out to the logged-in `gh`, so a worker's `bash` reached the same
2247
- credential by running the same binary.
2248
-
2249
- Scrubbing `GH_TOKEN` does not fix that, and this package will not describe it as
2250
- if it did. Code running as the same uid defeats an environment variable in one
2251
- line (`GH_CONFIG_DIR=$HOME/.config/gh gh pr merge …`), and the login keychain,
2252
- `~/.ssh` and `~/.git-credentials` need no environment at all. **The boundary is
2253
- an OS principal.** The environment scrubbing that ships alongside it is
2254
- accident-prevention: it makes the common mistakes fail immediately and legibly,
2255
- and it stops nothing that is trying.
2256
-
2257
- ### What actually changed
2258
-
2259
- - Worker and orchestrator sessions are **child processes**, launched under a
2260
- principal scoped to one run. They are still supervised exactly as before —
2261
- turn caps, wall-clock caps, spend accounting, salvage, transcript paths — and
2262
- a session that outlives its daemon is not possible: the child exits when its
2263
- control socket closes.
2264
- - A run's checkout is **its own git repository**, borrowing objects read-only
2265
- from the shared mirror through git alternates. It used to be a linked worktree
2266
- of that mirror, which needs write access to the mirror's refs and therefore
2267
- handed every run write access to every other run's.
2268
- - **Publishing is the daemon's.** The worker commits locally; the daemon fetches
2269
- the run branch out of the run's repository into the mirror and pushes it to
2270
- GitHub, fast-forward only. There is no force path anywhere in this package. A
2271
- rejected push settles the run `failed` with git's stderr verbatim.
2272
- - One function, `credentialedEnv()`, is the only construction site of credential
2273
- material in the daemon. `gh`, the mirror's clone and fetch, the daemon-side
2274
- push and the mediated verbs all go through it, so "what can reach the
2275
- credential" is answered by its call sites rather than by an audit.
2276
-
2277
- ### `credentials.isolation`
2278
-
2279
- | Value | Meaning |
2280
- |---|---|
2281
- | `"per-run"` | Every session runs as its own OS principal. The daemon refuses to dispatch if this host cannot provide one. |
2282
- | `"none"` | Sessions run as the daemon's user. `status` reports the fleet as **unprotected**, every time. |
2283
-
2284
- There is deliberately no third value. The degraded `group-mode` mechanism below
2285
- is something the *host probe* can report, never something a config can ask for,
2286
- so an operator cannot believe they configured the full boundary and get the
2287
- weaker one.
2288
-
2289
- **A config written before this key existed is migrated, not rejected.** On the
2290
- first start it resolves to `"none"`, the key is written into the file
2291
- explicitly, and the daemon logs the migration once by project name. The answer
2292
- then lives on disk rather than being inherited from a default nobody chose — and
2293
- no running fleet stops dispatching on upgrade. A fleet that upgrades and does
2294
- nothing else is unprotected and says so in `status`; turning that into a refusal
2295
- would have been an outage caused by a security feature.
2296
-
2297
- ### `credentials.readToken`
2298
-
2299
- Optional, and absent by default. The boundary removes GitHub *reads* too, so a
2300
- worker with no token works from its dispatch brief — which already carries the
2301
- issue body and the acceptance criteria — and from the mediated verbs. Set this
2302
- to a **read-scoped** token to give sessions `gh` reads back.
2303
-
2304
- This package cannot verify the scope. A write-scoped token here re-opens exactly
2305
- the hole the rest of this section closes.
2306
-
2307
- ### Mechanisms, and what each one does not protect
2308
-
2309
- The daemon probes the host at startup, states the mechanism, and proves it by
2310
- running the composed launcher against a trivial child before reporting it
2311
- available. A mechanism whose argv the host rejects resolves *down* at probe time
2312
- rather than failing every dispatch at run time.
2313
-
2314
- | Mechanism | Where | Contains a determined escape? |
2315
- |---|---|---|
2316
- | `uid-pool` | Linux, with the capabilities and accounts below | **Yes** |
2317
- | `sandbox-exec` | macOS | **Yes**, within a deprecated Apple interface |
2318
- | `group-mode` | Linux, groups but no capabilities | **No** — cross-run separation only |
2319
- | `none` | anywhere else | **No** |
2320
-
2321
- `group-mode` exists so an operator who cannot grant capabilities still gets
2322
- cross-run checkout separation. It runs sessions as the daemon's own uid, so it
2323
- bounds accidents and does not stop a determined same-uid escape. `status` says
2324
- which one is live, and lists what it does not close.
2325
-
2326
- ### Deploying `per-run` on a Linux fleet host
2327
-
2328
- See `systemd/omp-conductor.service.example` for the copy-pasteable version. In
2329
- summary:
2330
-
2331
- 1. **Capabilities on the existing unprivileged account.** The unit grants
2332
- `CAP_SETUID CAP_SETGID CAP_CHOWN CAP_SETPCAP`. They exist to be *dropped
2333
- into* run children, never inherited by them. A capability grant on the
2334
- `fleet` account is a narrower blast radius than running as root or shipping a
2335
- setuid binary.
2336
-
2337
- `CAP_SETPCAP` is there solely so the launcher can empty the child's
2338
- capability **bounding** set, which `PR_CAPBSET_DROP` requires. A host that
2339
- withholds it takes a documented fallback: the launcher omits
2340
- `--bounding-set=-all`, the child still ends with every other set empty behind
2341
- `NoNewPrivs`, the leftover `CapBnd` is inert, and `status` reports it as a
2342
- named residual.
2343
-
2344
- 2. **`setpriv` (util-linux) must be on `PATH`.** Every run child is launched
2345
- through it: group list, then gid, then uid, then empty the
2346
- permitted/effective/inheritable/ambient sets, drop the bounding set, set
2347
- `PR_SET_NO_NEW_PRIVS`, then `exec`. Without `setpriv` the probe reports
2348
- `none`. There is deliberately no `spawn({uid,gid})` fallback — ambient
2349
- capabilities survive `execve` for ordinary binaries, so a child launched that
2350
- way holds `CAP_SETUID` itself and can `setuid()` back to a sibling run or to
2351
- the daemon, voiding the boundary while appearing to work.
2352
-
2353
- 3. **Provision with the shipped command, not by hand.**
2354
- `omp-conductor boundary-setup --slots <n>` prints the exact idempotent root
2355
- commands, generated from the same constants the startup probe checks, so the
2356
- instructions cannot drift from what the daemon then demands.
2357
-
2358
- ```bash
2359
- omp-conductor boundary-setup --slots 2 # read it first
2360
- omp-conductor boundary-setup --slots 2 | sudo bash
2361
- sudo systemctl restart omp-conductor.service
2362
- ```
2363
-
2364
- It needs no config and must run **before** `setup`: setup writes worktree and
2365
- mirror paths into the shared root this creates. What it establishes:
2366
-
2367
- 4. **One account per run slot**, `conductor-agent-<n>` for each concurrent
2368
- worker plus `conductor-agent-orch` for the orchestrator. `maxConcurrentWorkers`
2369
- is 2 by default, so the pool is small. The orchestrator's account is distinct
2370
- precisely so it has no read or write access to any run checkout, and it is
2371
- launched with no supplementary group at all.
2372
-
2373
- 5. **Two groups**, and the split between them is the whole filesystem model:
2374
-
2375
- | Group | Members | Purpose |
2376
- |---|---|---|
2377
- | `conductor-daemon` | the daemon account **only** | lets it reach every run repo — fetch the branch, salvage, reclaim the tree |
2378
- | `conductor-runs` | every slot principal | read-only access to the shared mirror |
2379
-
2380
- A slot principal must **never** be in `conductor-daemon`. That membership is
2381
- the only thing keeping sibling runs out of each other's checkouts, and its
2382
- absence is asserted from a live session by the probe suite.
2383
-
2384
- Supplementary group membership is fixed when a process starts, so this
2385
- **requires a service restart**. Without it the daemon's live credentials lack
2386
- the group even though `getent` shows it, and it would chown every run repo to
2387
- a group it cannot itself use — the probe checks the running process's own
2388
- credentials for exactly this reason, and every run additionally write-tests
2389
- its tree after the ownership handoff.
2390
-
2391
- 6. **Layout.** Run repos are `conductor-agent-<slot>:conductor-daemon`,
2392
- directories `2770` (setgid, so what the worker creates inherits the group)
2393
- and files `0660` with execute bits preserved. The shared mirror is
2394
- daemon-owned, group `conductor-runs`, `0750`/`0640`.
2395
-
2396
- Everything a slot must reach lives under **`/var/lib/omp-conductor`** (mode
2397
- `0711`): worktrees, mirrors, per-run session transcripts and per-run boundary
2398
- homes. It is deliberately outside the state directory, which stays `0700`
2399
- because it holds `conductor.db` and the WAL files SQLite recreates at
2400
- runtime — making that searchable would publish fleet history to every local
2401
- account. Override with `$OMP_CONDUCTOR_SHARED`. Uninstall is therefore two
2402
- paths, not one.
2403
-
2404
- 7. **The daemon's home is `0711`, with its credential leaves closed**
2405
- (`.ssh`, `.config/gh` at `0700`; `.npmrc`, `.git-credentials` at `0600`).
2406
- Searchable because the runtime and the installed package live in it — a
2407
- `0700` home kills every worker before it connects, and no shell-based probe
2408
- notices — and closed at the leaves because that is where the boundary
2409
- actually rests. The daemon re-checks this empirically at dispatch, asking the
2410
- slot principal itself whether it can read any credential path, and refuses
2411
- the run if it can.
2412
-
2413
- Verify with `omp-conductor status`: the `boundary` row names the live mechanism
2414
- and lists its residuals.
2415
-
2416
- ### macOS dev hosts
2417
-
2418
- `sandbox-exec` is Apple-deprecated and is still the only thing on a stock host
2419
- that denies a *read*, which is what this needs. The generated profile makes the
2420
- run's checkout the only writable path and denies `~/.ssh`, `~/.config/gh`,
2421
- `~/.git-credentials`, `~/.npmrc`, the other run checkouts, and mach lookups of
2422
- the keychain services. Verified on darwin 25.5.0 against the full probe list.
2423
-
2424
- A dev host may also simply run `credentials.isolation: "none"`, in which case
2425
- `status` reports it unprotected. That is a legitimate choice for a machine that
2426
- is not a fleet host; it is not a default.
2427
-
2428
- ### The residual, stated plainly
2429
-
2430
- A run can **read** another run's git objects out of the shared mirror. That is
2431
- the price of not cloning the repository per run, and it is bounded: same source,
2432
- no write path, no credential. It is asserted as *permitted* by the probe suite,
2433
- so nobody can quietly "fix" it by weakening the mirror's modes.
2434
-
2435
- ### MCP servers are inside the blast radius
2436
-
2437
- A GitHub MCP server carrying its own PAT re-opens the hole. Sessions discover
2438
- MCP configuration from the **agent principal's** config root — the redirected
2439
- `$HOME` this package creates per session, not the operator's — and dispatch
2440
- **refuses with a named error** when a credential-bearing GitHub server is found
2441
- there or in the run's checkout. Put servers you want sessions to have in the
2442
- agent principal's root, without credentials.
2443
-
2444
2219
 
2445
2220
  ## Limitations
2446
2221
 
@@ -2468,18 +2243,6 @@ Known and deliberate in this version:
2468
2243
  - **No cross-process lock on the mirrors.** Two dispatch loops fetching the same
2469
2244
  repo at the same instant can collide on git's ref locks; the run fails and is
2470
2245
  retried rather than corrupted.
2471
- - **A run can read another run's git objects.** The shared mirror is one object
2472
- store, so the isolation is on writes and credentials, not reads. See
2473
- [The residual, stated plainly](#the-residual-stated-plainly).
2474
- - **`group-mode` does not contain a determined escape.** It separates run
2475
- checkouts by group and mode while sessions still run as the daemon's uid. Only
2476
- `uid-pool` and `sandbox-exec` make the stronger claim, and `status` says which
2477
- one is live.
2478
- - **A fleet that upgrades and changes nothing is unprotected.** The
2479
- `credentials` migration resolves to `"none"` and writes it down rather than
2480
- refusing to dispatch, because an outage caused by a security feature is worse
2481
- than the day before it shipped. `status` reports it every tick until an
2482
- operator provisions the host and sets `"per-run"`.
2483
2246
  - **Uniquely local mirror branches are retained.** Terminal runs are reaped
2484
2247
  automatically only after every commit exists on a remote ref. A failed salvage
2485
2248
  push deliberately leaves its branch and tree for an operator rather than
@@ -2497,7 +2260,7 @@ Known and deliberate in this version:
2497
2260
  "nobody reads it until morning" path the orchestrator exists to avoid. The warning
2498
2261
  is in `daemon.log`; nothing pages you about it.
2499
2262
  - **Workers are not terminal panes, so you cannot watch them there.** Each
2500
- 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
2501
2264
  daemon tracks workers in a background pool so the five-minute loop keeps
2502
2265
  settling PRs and checking capacity; shutdown waits for that pool. Herdr still
2503
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.5",
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