omp-conductor 0.5.0 → 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 +41 -25
- package/package.json +1 -1
- package/src/omp.ts +5 -3
- package/src/verbs/server.ts +0 -8
- package/src/verbs/socket.ts +27 -68
- package/src/worktree.ts +5 -3
package/README.md
CHANGED
|
@@ -149,21 +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
|
-
environment and `$HOME` unmodified, so a
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
-
|
|
158
|
-
`
|
|
159
|
-
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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.
|
|
167
172
|
- `gh`, already authenticated: every tracker operation shells out to it, so the
|
|
168
173
|
daemon never handles a GitHub token itself.
|
|
169
174
|
- `git`: mirrors and worktrees.
|
|
@@ -2149,18 +2154,29 @@ on run X asking to merge run Y's PR is not merely wrong, it is unexpressible.
|
|
|
2149
2154
|
|
|
2150
2155
|
```
|
|
2151
2156
|
<state dir>/verbs/ daemon-owned, mode 0711
|
|
2152
|
-
run-7-9a783d877d422b9e.sock 0600, run 7
|
|
2153
|
-
run-9-1c40e2a5b6d3f018.sock 0600, run 9
|
|
2154
|
-
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
|
|
2155
2160
|
```
|
|
2156
2161
|
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
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.
|
|
2164
2180
|
|
|
2165
2181
|
Before binding, the daemon verifies every component of the path is owned by
|
|
2166
2182
|
itself (or root), free of symlinks, and unwritable by anyone else; a failed
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "omp-conductor",
|
|
3
|
-
"version": "0.5.
|
|
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.",
|
package/src/omp.ts
CHANGED
|
@@ -428,9 +428,11 @@ export interface CreateSessionOptions {
|
|
|
428
428
|
*/
|
|
429
429
|
export async function createSession(opts: CreateSessionOptions): Promise<AgentSessionLike> {
|
|
430
430
|
const owned = opts.socketPath === undefined;
|
|
431
|
-
// 0711, not 0700: searchable
|
|
432
|
-
//
|
|
433
|
-
//
|
|
431
|
+
// 0711, not 0700: searchable but not listable, which keeps *other local
|
|
432
|
+
// accounts* from enumerating this directory. It is not a restraint on the
|
|
433
|
+
// session — that runs as this process's own uid, matches the owner class and
|
|
434
|
+
// therefore has `rwx` here regardless. Same reasoning, and the same limit, as
|
|
435
|
+
// the run workspace parent.
|
|
434
436
|
const socketDir = owned ? mkdtempSync(join(tmpdir(), "omp-session-")) : dirname(opts.socketPath ?? "");
|
|
435
437
|
const socketPath = opts.socketPath ?? join(socketDir, "s");
|
|
436
438
|
mkdirSync(socketDir, { recursive: true, mode: 0o711 });
|
package/src/verbs/server.ts
CHANGED
|
@@ -51,7 +51,6 @@ import {
|
|
|
51
51
|
secureBoundSocket,
|
|
52
52
|
socketFd,
|
|
53
53
|
unlinkStaleSocket,
|
|
54
|
-
traversalProblem,
|
|
55
54
|
validateSocketPath,
|
|
56
55
|
type PeerReader,
|
|
57
56
|
type SocketOwnership,
|
|
@@ -935,13 +934,6 @@ export interface ListenOptions {
|
|
|
935
934
|
daemonUid?: number;
|
|
936
935
|
/** Injected in tests; the real one chowns, which needs privilege. */
|
|
937
936
|
secure?: typeof secureBoundSocket;
|
|
938
|
-
/**
|
|
939
|
-
* Injected in tests for the same reason `secure` is: the traversal rule is
|
|
940
|
-
* about the *deployment's* directory chain, and a test that needs a bound
|
|
941
|
-
* socket in a developer's `0750` home is not the case it is asserting. The
|
|
942
|
-
* rule itself is pinned directly in `socket.test.ts`.
|
|
943
|
-
*/
|
|
944
|
-
traversal?: typeof traversalProblem;
|
|
945
937
|
}
|
|
946
938
|
|
|
947
939
|
/**
|
package/src/verbs/socket.ts
CHANGED
|
@@ -1,28 +1,34 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The authenticated local transport the mutation verbs ride on (#126).
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* *
|
|
7
|
-
*
|
|
4
|
+
* **What the permission layout does and does not buy, stated plainly.** Every
|
|
5
|
+
* session is a child process running as the daemon's own uid. So the modes here
|
|
6
|
+
* keep *other local accounts* out; they do not restrain a session, because a
|
|
7
|
+
* session matches the owner class and owner bits are `rwx`. A session that
|
|
8
|
+
* wanted to list the socket directory, unlink a sibling's socket and bind an
|
|
9
|
+
* impostor in its place could do so, and nothing in this file would stop it.
|
|
8
10
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* - the parent being non-writable is what stops a run unlinking a sibling's
|
|
14
|
-
* socket or binding an impostor listener in its place. A per-run directory
|
|
15
|
-
* owned by the run principal would hand back exactly that power, and is why
|
|
16
|
-
* the obvious layout is not used here.
|
|
11
|
+
* That is not a hole this file is hiding — it is the consequence of running
|
|
12
|
+
* sessions as the daemon. What restrains a session is the mechanical worktree
|
|
13
|
+
* gate on its structured tools plus the fact that publication only happens
|
|
14
|
+
* through the daemon, and what makes a bad push *visible* is the verb ledger.
|
|
17
15
|
*
|
|
18
|
-
*
|
|
19
|
-
* {@link validateSocketPath} refuses to bind under a path anyone else could
|
|
20
|
-
* have tampered with, and {@link peerVerdict} compares the connecting uid the
|
|
21
|
-
* kernel reports against the uid the daemon allocated for that run.
|
|
16
|
+
* So the layout is:
|
|
22
17
|
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
18
|
+
* - the parent directory is daemon-owned and mode **`0711`** — searchable so a
|
|
19
|
+
* caller reaches a socket it can already name, not listable and not writable
|
|
20
|
+
* by any account other than the daemon's;
|
|
21
|
+
* - each socket is `0600`, so only the daemon's uid can connect at all;
|
|
22
|
+
* - the per-socket suffix is random, so the path is not guessable by a foreign
|
|
23
|
+
* account that cannot list the directory.
|
|
24
|
+
*
|
|
25
|
+
* Two further rules make that hold rather than merely describe it:
|
|
26
|
+
* {@link validateSocketPath} refuses to bind under a path anyone else could have
|
|
27
|
+
* tampered with, and {@link peerVerdict} compares the connecting uid the kernel
|
|
28
|
+
* reports against the daemon's own.
|
|
29
|
+
*
|
|
30
|
+
* Every decision in this file is a pure function of `lstat` results, so the
|
|
31
|
+
* adversarial cases are testable without root and without a second uid.
|
|
26
32
|
*/
|
|
27
33
|
|
|
28
34
|
import { dlopen, FFIType, ptr, suffix } from "bun:ffi";
|
|
@@ -31,10 +37,10 @@ import { chmodSync, chownSync, lstatSync, mkdirSync, rmSync } from "node:fs";
|
|
|
31
37
|
import type { Stats } from "node:fs";
|
|
32
38
|
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
33
39
|
|
|
34
|
-
/** Searchable
|
|
40
|
+
/** Searchable, but not listable or writable by any account but the daemon's. */
|
|
35
41
|
export const VERB_DIR_MODE = 0o711;
|
|
36
42
|
|
|
37
|
-
/**
|
|
43
|
+
/** Only the daemon's uid may connect. The parent's `0711` keeps that reachable. */
|
|
38
44
|
export const VERB_SOCKET_MODE = 0o600;
|
|
39
45
|
|
|
40
46
|
/**
|
|
@@ -196,53 +202,6 @@ export function validateSocketPath(
|
|
|
196
202
|
return undefined;
|
|
197
203
|
}
|
|
198
204
|
|
|
199
|
-
/**
|
|
200
|
-
* The first path component a *foreign* uid could not search through, or
|
|
201
|
-
* `undefined` when the whole chain is traversable.
|
|
202
|
-
*
|
|
203
|
-
* Checked rather than assumed, because this is the failure the layout in this
|
|
204
|
-
* file is most likely to hit in the field and the least likely to be noticed:
|
|
205
|
-
* connecting to a socket needs `+x` on **every** component, and the daemon's
|
|
206
|
-
* own state directory is `0700`. A run principal would then be refused at the
|
|
207
|
-
* state directory rather than at the socket, with an `EACCES` that names a
|
|
208
|
-
* directory nobody was thinking about.
|
|
209
|
-
*
|
|
210
|
-
* Only meaningful when the run has a distinct principal. Under one uid the
|
|
211
|
-
* daemon is the caller, so `0700` is traversable by definition — which is
|
|
212
|
-
* exactly why this cannot be left to be discovered on the first fleet that
|
|
213
|
-
* turns per-run principals on.
|
|
214
|
-
*
|
|
215
|
-
* Deliberately reported rather than repaired: the fix is `chmod o+x` on a
|
|
216
|
-
* directory that may be the operator's home, and a daemon that silently
|
|
217
|
-
* widened `$HOME` would be trading a legible refusal for a surprise.
|
|
218
|
-
*/
|
|
219
|
-
export function traversalProblem(
|
|
220
|
-
path: string,
|
|
221
|
-
lstat: (p: string) => Stats = lstatSync,
|
|
222
|
-
): SocketPathProblem | undefined {
|
|
223
|
-
for (const component of ancestors(path)) {
|
|
224
|
-
let stat: Stats;
|
|
225
|
-
try {
|
|
226
|
-
stat = lstat(component);
|
|
227
|
-
} catch {
|
|
228
|
-
return {
|
|
229
|
-
component,
|
|
230
|
-
fault: "missing",
|
|
231
|
-
message: `verb socket parent ${component} does not exist or cannot be read`,
|
|
232
|
-
};
|
|
233
|
-
}
|
|
234
|
-
if ((stat.mode & 0o001) === 0) {
|
|
235
|
-
return {
|
|
236
|
-
component,
|
|
237
|
-
fault: "not-searchable",
|
|
238
|
-
message:
|
|
239
|
-
`verb socket parent ${component} is mode ${(stat.mode & 0o7777).toString(8)}, which a run principal ` +
|
|
240
|
-
`cannot search through. Connecting needs +x on every component. Run: chmod o+x ${component}`,
|
|
241
|
-
};
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
return undefined;
|
|
245
|
-
}
|
|
246
205
|
|
|
247
206
|
/**
|
|
248
207
|
* Remove a stale socket at `path`, and only ever one the daemon itself placed.
|
package/src/worktree.ts
CHANGED
|
@@ -312,9 +312,11 @@ export async function addRunRepo(
|
|
|
312
312
|
branch: string,
|
|
313
313
|
): Promise<{ path: string; reattached: boolean }> {
|
|
314
314
|
const mirrorPath = await ensureMirror(repo, mirrorRoot);
|
|
315
|
-
// 0711: searchable
|
|
316
|
-
//
|
|
317
|
-
//
|
|
315
|
+
// 0711: searchable but not listable, so no *other local account* can
|
|
316
|
+
// enumerate the fleet's checkouts. It does not stop a session reaching a
|
|
317
|
+
// sibling's tree: sessions run as the daemon's own uid, so the owner bits are
|
|
318
|
+
// theirs. What confines a worker to its own checkout is the mechanical
|
|
319
|
+
// worktree gate on its tools, not this mode.
|
|
318
320
|
mkdirSync(workspaceRoot, { recursive: true, mode: 0o711 });
|
|
319
321
|
|
|
320
322
|
const runRepo = worktreePathFor(workspaceRoot, issue);
|