omp-conductor 0.4.5 → 0.5.1
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 +66 -287
- package/package.json +1 -1
- package/skills/conductor-onboarding/SKILL.md +0 -56
- package/skills/conductor-update/SKILL.md +4 -9
- package/src/briefs/orchestrator.md +13 -19
- package/src/briefs/worker.md +18 -16
- package/src/cli.ts +1 -48
- package/src/config.ts +7 -224
- package/src/confinement.ts +2 -3
- package/src/daemon.ts +23 -477
- package/src/failure-class.ts +6 -1
- package/src/fleet.ts +1 -85
- package/src/gitops.ts +189 -0
- package/src/omp.ts +13 -35
- package/src/orchestrator.ts +0 -4
- package/src/plugin.ts +0 -64
- package/src/session-host.ts +12 -19
- package/src/setup-host.ts +2 -192
- package/src/setup.ts +6 -81
- package/src/store.ts +34 -0
- package/src/tracker/github.ts +1 -1
- package/src/types.ts +0 -90
- package/src/upgrade.ts +0 -90
- package/src/verbs/actions.ts +1 -1
- package/src/verbs/server.ts +3 -36
- package/src/verbs/socket.ts +42 -91
- package/src/worker.ts +1 -13
- package/src/worktree.ts +8 -6
- package/systemd/omp-conductor.service.example +8 -102
- package/src/credentials.ts +0 -2203
package/README.md
CHANGED
|
@@ -149,25 +149,26 @@ 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
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
152
|
+
- **A model credential the *daemon's own account* can reach.** Sessions are child
|
|
153
|
+
processes of the daemon and inherit its environment and `$HOME` unmodified, so a
|
|
154
|
+
session authenticates with exactly what the daemon authenticates with — nothing
|
|
155
|
+
is injected and nothing is scrubbed. Any shape the harness itself understands
|
|
156
|
+
works, including the ordinary one:
|
|
157
|
+
- an **OAuth login** already recorded for that account (`omp` login state under
|
|
158
|
+
its `~/.omp/agent`). This is the common case and needs no configuration.
|
|
159
|
+
- a model **API key** in the daemon's environment (`ANTHROPIC_API_KEY`,
|
|
160
|
+
`OPENAI_API_KEY`, …). Note systemd starts the service with a clean
|
|
161
|
+
environment, so it has to be an `Environment=` line on the unit, not something
|
|
162
|
+
exported in your shell.
|
|
163
|
+
- the harness's **auth broker**, configured in that account's
|
|
164
|
+
`~/.omp/agent/config.yml`.
|
|
165
|
+
|
|
166
|
+
The account matters more than the shape: a login recorded under a *different*
|
|
167
|
+
account is invisible to the service. A unit running `User=fleet` cannot see
|
|
168
|
+
`root`'s login, and workers then die at turn 0 with `No model selected`. Such a
|
|
169
|
+
run is classified `env-start-failure` and charges **neither** the failure budget
|
|
170
|
+
nor a continuation — an environment fault is not a failed implementation — but
|
|
171
|
+
nothing dispatches successfully until the credential is reachable.
|
|
171
172
|
- `gh`, already authenticated: every tracker operation shells out to it, so the
|
|
172
173
|
daemon never handles a GitHub token itself.
|
|
173
174
|
- `git`: mirrors and worktrees.
|
|
@@ -889,10 +890,12 @@ bad merge.
|
|
|
889
890
|
|
|
890
891
|
## Host sizing and memory
|
|
891
892
|
|
|
892
|
-
Workers are **
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
893
|
+
Workers are **child processes** of the daemon (plus one long-lived orchestrator
|
|
894
|
+
session), each its own pid, talking back over a unix socket. They are still
|
|
895
|
+
inside the service's cgroup, so systemd's Memory peak for
|
|
896
|
+
`omp-conductor.service` is daemon + every live worker + the orchestrator + any
|
|
897
|
+
MCP stdio children those sessions mount. `MemoryMax=` governs that whole total,
|
|
898
|
+
not one process.
|
|
896
899
|
|
|
897
900
|
On the reference deploy that produced [issue #51](https://github.com/TerrifiedBug/conductor/issues/51):
|
|
898
901
|
|
|
@@ -929,7 +932,7 @@ rest. `0` is a real value (a hard stop), not "unset".
|
|
|
929
932
|
|
|
930
933
|
| Cap | Default | What it protects |
|
|
931
934
|
| --- | --- | --- |
|
|
932
|
-
| `maxConcurrentWorkers` | `2` (setup may write `1` on <16 GiB hosts) | Parallel
|
|
935
|
+
| `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
936
|
| `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
937
|
| `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
938
|
| `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 +1560,6 @@ A complete, valid config for one project with two target repos:
|
|
|
1557
1560
|
"github-release": "human",
|
|
1558
1561
|
"deploy": "human"
|
|
1559
1562
|
},
|
|
1560
|
-
"credentials": {
|
|
1561
|
-
"isolation": "per-run"
|
|
1562
|
-
},
|
|
1563
1563
|
"policy": {
|
|
1564
1564
|
"merge": {
|
|
1565
1565
|
"requiredChecks": ["build", "lint"],
|
|
@@ -1917,7 +1917,7 @@ omp-conductor help
|
|
|
1917
1917
|
| `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
1918
|
| `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
1919
|
| `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` / `
|
|
1920
|
+
| `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
1921
|
| `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
1922
|
| `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
1923
|
| `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 +2119,14 @@ Unconfined means auditable, not licensed. `orchestratorReadPaths` is retired: it
|
|
|
2119
2119
|
is still accepted in a config and ignored, so a fleet carrying it upgrades
|
|
2120
2120
|
without editing anything.
|
|
2121
2121
|
|
|
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
2122
|
## The mediated verbs (#126)
|
|
2140
2123
|
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2124
|
+
A session can reach `gh`: it inherits the daemon's environment, credentials and
|
|
2125
|
+
all. It is told not to publish with it. These verbs are the sanctioned route
|
|
2126
|
+
instead, because the dispatcher owns the settlement record — a push or a PR the
|
|
2127
|
+
daemon did not perform is a run it cannot account for, and the checks that would
|
|
2128
|
+
have refused it never ran. The point of them is *where those checks run*: in the
|
|
2129
|
+
daemon, across a process boundary, not in a prompt the model can rewrite.
|
|
2145
2130
|
|
|
2146
2131
|
### The verbs
|
|
2147
2132
|
|
|
@@ -2169,35 +2154,45 @@ on run X asking to merge run Y's PR is not merely wrong, it is unexpressible.
|
|
|
2169
2154
|
|
|
2170
2155
|
```
|
|
2171
2156
|
<state dir>/verbs/ daemon-owned, mode 0711
|
|
2172
|
-
run-7-9a783d877d422b9e.sock 0600,
|
|
2173
|
-
run-9-1c40e2a5b6d3f018.sock 0600,
|
|
2174
|
-
orchestrator-4b1f...c2.sock 0600,
|
|
2157
|
+
run-7-9a783d877d422b9e.sock 0600, bound for run 7
|
|
2158
|
+
run-9-1c40e2a5b6d3f018.sock 0600, bound for run 9
|
|
2159
|
+
orchestrator-4b1f...c2.sock 0600, the orchestrator's
|
|
2175
2160
|
```
|
|
2176
2161
|
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2162
|
+
**What these modes buy, and what they do not.** They keep every *other local
|
|
2163
|
+
account* out: `0711` on the parent is traversable but not listable, so no other
|
|
2164
|
+
user can enumerate the fleet's sockets, the suffixes are unguessable, and only
|
|
2165
|
+
the daemon's uid can connect to a `0600` socket at all.
|
|
2166
|
+
|
|
2167
|
+
They are **not** a boundary between runs. Sessions are child processes of the
|
|
2168
|
+
daemon running as its own uid, so a session matches the owner class on all of
|
|
2169
|
+
these: it could list the directory and connect to a sibling's socket. Each run is
|
|
2170
|
+
*handed* its own path and nothing else, which is a convention the run has no
|
|
2171
|
+
reason to break — not an enforcement. What makes breaking it visible is the
|
|
2172
|
+
[ledger](#the-ledger): every call is recorded with the channel it
|
|
2173
|
+
arrived on, so a worker calling on another run's socket is in the record.
|
|
2174
|
+
|
|
2175
|
+
Closing that properly needs the sessions to be different OS principals. A
|
|
2176
|
+
per-run credential boundary that did exactly this shipped and was removed in
|
|
2177
|
+
0.5.0 — it worked, and the cost was that it also hid the operator's own model
|
|
2178
|
+
credential from every session, so nothing could start. It is not worth
|
|
2179
|
+
re-litigating without solving that first.
|
|
2183
2180
|
|
|
2184
2181
|
Before binding, the daemon verifies every component of the path is owned by
|
|
2185
2182
|
itself (or root), free of symlinks, and unwritable by anyone else; a failed
|
|
2186
|
-
check **refuses dispatch** rather than degrading.
|
|
2187
|
-
|
|
2188
|
-
`chmod o+x` when it is not. Paths are unguessably suffixed, and only the daemon
|
|
2189
|
-
ever unlinks one.
|
|
2183
|
+
check **refuses dispatch** rather than degrading. Paths are unguessably
|
|
2184
|
+
suffixed, and only the daemon ever unlinks one.
|
|
2190
2185
|
|
|
2191
2186
|
Peer credentials are asserted server-side — `getpeereid` on macOS, `SO_PEERCRED`
|
|
2192
|
-
on Linux —
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2187
|
+
on Linux — and the daemon states at startup exactly what that buys rather than
|
|
2188
|
+
implying more. Sessions are child processes running under the daemon's own uid,
|
|
2189
|
+
so the peer check proves the caller is a local process on this host; it is the
|
|
2190
|
+
socket, not the uid, that says which run is calling. A connection whose peer
|
|
2191
|
+
cannot be read at all is closed with no reply and logged.
|
|
2196
2192
|
|
|
2197
2193
|
```
|
|
2198
2194
|
verb transport: verb sockets in ~/.omp/conductor/verbs (mode 711); each socket
|
|
2199
|
-
0600 under the daemon's own uid
|
|
2200
|
-
asserted with getpeereid
|
|
2195
|
+
0600 under the daemon's own uid; peer uid asserted with getpeereid
|
|
2201
2196
|
```
|
|
2202
2197
|
|
|
2203
2198
|
**No mutation route exists on the HTTP port**, and none may be added. That
|
|
@@ -2205,8 +2200,8 @@ surface is unauthenticated loopback TCP reachable by any local user; a `PUT` or
|
|
|
2205
2200
|
`POST` at any verb path answers 404, pinned by a test.
|
|
2206
2201
|
|
|
2207
2202
|
The child-side tool handler is a thin client only. It forwards arguments and
|
|
2208
|
-
renders the answer — no policy branch, no
|
|
2209
|
-
socket it fails closed and says so, rather than reaching for `git push`.
|
|
2203
|
+
renders the answer — no policy branch, no local fallback, no second route. With
|
|
2204
|
+
no socket it fails closed and says so, rather than reaching for `git push`.
|
|
2210
2205
|
|
|
2211
2206
|
### The ledger
|
|
2212
2207
|
|
|
@@ -2237,210 +2232,6 @@ leaves a durable record that something tried. It is no longer what *stops* a
|
|
|
2237
2232
|
release. Treat a block there as evidence about a session's intentions; the
|
|
2238
2233
|
daemon is what prevented it.
|
|
2239
2234
|
|
|
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
2235
|
|
|
2445
2236
|
## Limitations
|
|
2446
2237
|
|
|
@@ -2468,18 +2259,6 @@ Known and deliberate in this version:
|
|
|
2468
2259
|
- **No cross-process lock on the mirrors.** Two dispatch loops fetching the same
|
|
2469
2260
|
repo at the same instant can collide on git's ref locks; the run fails and is
|
|
2470
2261
|
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
2262
|
- **Uniquely local mirror branches are retained.** Terminal runs are reaped
|
|
2484
2263
|
automatically only after every commit exists on a remote ref. A failed salvage
|
|
2485
2264
|
push deliberately leaves its branch and tree for an operator rather than
|
|
@@ -2497,7 +2276,7 @@ Known and deliberate in this version:
|
|
|
2497
2276
|
"nobody reads it until morning" path the orchestrator exists to avoid. The warning
|
|
2498
2277
|
is in `daemon.log`; nothing pages you about it.
|
|
2499
2278
|
- **Workers are not terminal panes, so you cannot watch them there.** Each
|
|
2500
|
-
worker is an
|
|
2279
|
+
worker is an omp session the daemon starts as a child process. The resident
|
|
2501
2280
|
daemon tracks workers in a background pool so the five-minute loop keeps
|
|
2502
2281
|
settling PRs and checking capacity; shutdown waits for that pool. Herdr still
|
|
2503
2282
|
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.
|
|
3
|
+
"version": "0.5.1",
|
|
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
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
|
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
|
|
283
|
-
whose checks run in the dispatcher, and each one records
|
|
284
|
-
what it decided — readable with `omp-conductor ledger`,
|
|
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
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
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
|