omp-conductor 0.17.1 → 0.18.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 +34 -0
- package/REFERENCE.md +71 -17
- package/agents/to-spec.md +90 -0
- package/package.json +2 -1
- package/schema/config.schema.json +53 -1
- package/src/admission.ts +308 -76
- package/src/ask.ts +307 -10
- package/src/backups.ts +2 -2
- package/src/board.ts +17 -3
- package/src/briefs/orchestrator.md +43 -14
- package/src/briefs/to-spec.md +84 -0
- package/src/briefs/worker.md +37 -19
- package/src/cli.ts +2 -0
- package/src/command-help.ts +19 -1
- package/src/command-manifest.ts +27 -2
- package/src/commands/context.ts +1 -0
- package/src/commands/drain.ts +176 -0
- package/src/commands/extend.ts +6 -10
- package/src/commands/status.ts +5 -1
- package/src/commands/watch.ts +110 -3
- package/src/commands/worker.ts +9 -10
- package/src/config-schema.ts +57 -0
- package/src/config.ts +102 -2
- package/src/daemon.ts +1220 -1517
- package/src/dashboard/app.js +4 -1
- package/src/dashboard/server.ts +5 -2
- package/src/decisions.ts +279 -16
- package/src/depends-on.ts +261 -1
- package/src/diff-flags.ts +425 -1
- package/src/digest-schedule.ts +37 -0
- package/src/doctor.ts +52 -0
- package/src/escalate.ts +9 -3
- package/src/failure-class.ts +43 -4
- package/src/fleet.ts +166 -24
- package/src/gitops.ts +188 -81
- package/src/graph-health.ts +55 -8
- package/src/graph.ts +379 -69
- package/src/harness-loader.ts +59 -0
- package/src/host.ts +567 -2
- package/src/lifecycle.ts +158 -6
- package/src/omp.ts +269 -20
- package/src/orchestrator-tick.ts +1489 -26
- package/src/orchestrator.ts +12 -0
- package/src/privileged.ts +1 -4
- package/src/release-policy.ts +503 -9
- package/src/routing.ts +11 -3
- package/src/session-host.ts +115 -5
- package/src/settlement.ts +1780 -0
- package/src/setup-host.ts +1205 -6
- package/src/setup-install.ts +119 -30
- package/src/setup-wizard.ts +88 -2
- package/src/setup.ts +119 -13
- package/src/shell.ts +15 -0
- package/src/status-render.ts +100 -11
- package/src/store.ts +519 -45
- package/src/to-spec.ts +387 -0
- package/src/tracker/github.ts +150 -14
- package/src/types.ts +470 -16
- package/src/upgrade-verify.ts +209 -2
- package/src/upgrade.ts +175 -1
- package/src/verbs/protocol.ts +39 -0
- package/src/verbs/server.ts +770 -40
- package/src/verbs/socket.ts +24 -5
- package/src/worker.ts +239 -9
- package/src/worktree.ts +142 -18
package/src/session-host.ts
CHANGED
|
@@ -21,7 +21,9 @@
|
|
|
21
21
|
|
|
22
22
|
import { connect } from "node:net";
|
|
23
23
|
|
|
24
|
+
import { identityMismatch } from "./host.ts";
|
|
24
25
|
import { createLocalSession, disposeSession, type AgentSessionLike } from "./omp.ts";
|
|
26
|
+
import type { GraphToolsObservation } from "./types.ts";
|
|
25
27
|
import type { GateShape, ReleaseBlockContext } from "./release-policy.ts";
|
|
26
28
|
import type { ResolvedGrants, SessionRole } from "./types.ts";
|
|
27
29
|
|
|
@@ -34,6 +36,16 @@ export interface SessionHostSpec {
|
|
|
34
36
|
socket: string;
|
|
35
37
|
cwd: string;
|
|
36
38
|
role: SessionRole;
|
|
39
|
+
/**
|
|
40
|
+
* The kernel identity this session was launched under (#798): the uid/gid
|
|
41
|
+
* setpriv dropped to before this process started. The child asserts
|
|
42
|
+
* `process.getuid()/getgid()` against it as its first act, and refuses to
|
|
43
|
+
* build a session when they differ — a worker session whose kernel identity
|
|
44
|
+
* is not the one its parent established must not run worker code. Absent,
|
|
45
|
+
* the session runs as the daemon's own uid, which is what every
|
|
46
|
+
* non-worker surface (orchestrator, tests) wants.
|
|
47
|
+
*/
|
|
48
|
+
identity?: { uid: number; gid: number };
|
|
37
49
|
sessionDir?: string;
|
|
38
50
|
model?: string;
|
|
39
51
|
resume?: boolean;
|
|
@@ -55,6 +67,19 @@ export interface SessionHostSpec {
|
|
|
55
67
|
* Carried as a plain scalar like the other fields; absent, nothing is staged
|
|
56
68
|
* on the far side either. */
|
|
57
69
|
ompSettingsFile?: string;
|
|
70
|
+
/**
|
|
71
|
+
* The structured-settlement contract (#540): a JSON Schema the session's
|
|
72
|
+
* `yield` tool validates its `data` payload against. Carried as plain JSON
|
|
73
|
+
* through the spec, like everything else; absent, the far side passes nothing
|
|
74
|
+
* to the harness.
|
|
75
|
+
*/
|
|
76
|
+
outputSchema?: unknown;
|
|
77
|
+
/** Enforcement policy for {@link outputSchema} — the worker contract is
|
|
78
|
+
* `"permissive"`: a schema violation settles with whatever the worker
|
|
79
|
+
* produced, never a lost report. */
|
|
80
|
+
outputSchemaMode?: "permissive" | "strict";
|
|
81
|
+
/** Force the hidden `yield` tool into this session's toolset (#540). */
|
|
82
|
+
requireYieldTool?: boolean;
|
|
58
83
|
}
|
|
59
84
|
|
|
60
85
|
/** Parent → child. */
|
|
@@ -66,7 +91,35 @@ export type ParentToHost =
|
|
|
66
91
|
|
|
67
92
|
/** Child → parent. */
|
|
68
93
|
export type HostToParent =
|
|
69
|
-
| {
|
|
94
|
+
| {
|
|
95
|
+
t: "ready";
|
|
96
|
+
sessionFile?: string;
|
|
97
|
+
modelFallbackMessage?: string;
|
|
98
|
+
/**
|
|
99
|
+
* The omp-conductor version this child process loaded, attested at the
|
|
100
|
+
* ready handshake (#832): the session host's `ready` is the exact-session
|
|
101
|
+
* handoff the upgrade verifies against, so the live process's own loaded
|
|
102
|
+
* module version travels with it instead of being read off disk later.
|
|
103
|
+
* Absent only when the version cannot be read — absence is "not
|
|
104
|
+
* attested", never "the newest".
|
|
105
|
+
*/
|
|
106
|
+
extensionVersion?: string;
|
|
107
|
+
/** Exact peer entry and package version loaded by this child (#828). */
|
|
108
|
+
harnessPath?: string;
|
|
109
|
+
harnessVersion?: string;
|
|
110
|
+
}
|
|
111
|
+
| {
|
|
112
|
+
t: "graph-tools";
|
|
113
|
+
/**
|
|
114
|
+
* The code-graph session observation (#726): whether the graph MCP tools
|
|
115
|
+
* were in this session's registry. Sent once the session's tool registry
|
|
116
|
+
* settles (it finalises after creation, so the ready handshake never
|
|
117
|
+
* waits for it). Absent value never travels — no message means no
|
|
118
|
+
* observation was recorded, never "graph tools absent", which is the
|
|
119
|
+
* `present: false` truth value.
|
|
120
|
+
*/
|
|
121
|
+
graphTools: GraphToolsObservation;
|
|
122
|
+
}
|
|
70
123
|
| { t: "start-error"; message: string }
|
|
71
124
|
| { t: "event"; event: unknown }
|
|
72
125
|
| { t: "session-file"; path: string }
|
|
@@ -159,11 +212,17 @@ export interface SessionHostDeps {
|
|
|
159
212
|
* into the same `Error` the in-process path would have raised, so a missing
|
|
160
213
|
* peer dependency still reads as a deployment mistake rather than as an
|
|
161
214
|
* unexplained child exit.
|
|
215
|
+
*
|
|
216
|
+
* Returns `false` when the session refused to start because the identity it
|
|
217
|
+
* was launched for does not match the kernel identity this process actually
|
|
218
|
+
* runs under — that refusal is the fail-closed arm of the worker boundary,
|
|
219
|
+
* and the caller turns it into a non-zero exit so nothing downstream can read
|
|
220
|
+
* it as a clean session.
|
|
162
221
|
*/
|
|
163
222
|
export async function runSessionHost(
|
|
164
223
|
spec: SessionHostSpec,
|
|
165
224
|
deps: SessionHostDeps = { createSession: createLocalSession },
|
|
166
|
-
): Promise<
|
|
225
|
+
): Promise<boolean> {
|
|
167
226
|
|
|
168
227
|
const socket = connect(spec.socket);
|
|
169
228
|
socket.setNoDelay(true);
|
|
@@ -171,6 +230,26 @@ export async function runSessionHost(
|
|
|
171
230
|
if (!socket.writableEnded) socket.write(encodeFrame(message));
|
|
172
231
|
};
|
|
173
232
|
|
|
233
|
+
// The kernel identity check comes as early as there is a socket to report
|
|
234
|
+
// over and before the harness (`createSession`) has run a single byte of
|
|
235
|
+
// worker-controlled code. A child whose uid/gid are not the ones its parent
|
|
236
|
+
// launched it for proves the boundary did not hold — the daemon would be
|
|
237
|
+
// supervising a session that is not what it thinks it is.
|
|
238
|
+
const violation = identityMismatch(spec.identity);
|
|
239
|
+
if (violation !== undefined) {
|
|
240
|
+
send({ t: "start-error", message: violation });
|
|
241
|
+
// The frame must reach the parent before this process exits: `process.exit`
|
|
242
|
+
// does not flush the socket, and a parent that only observes the exit used
|
|
243
|
+
// to report a bare exit code instead of the child's own words. The parent
|
|
244
|
+
// resolves the run as failed with no live child — fail closed.
|
|
245
|
+
await new Promise<void>((resolve) => {
|
|
246
|
+
socket.once("finish", () => resolve());
|
|
247
|
+
socket.once("error", () => resolve());
|
|
248
|
+
socket.end();
|
|
249
|
+
});
|
|
250
|
+
return false;
|
|
251
|
+
}
|
|
252
|
+
|
|
174
253
|
const { promise: connected, resolve: onConnect, reject: onConnectFail } = Promise.withResolvers<void>();
|
|
175
254
|
socket.once("connect", () => {
|
|
176
255
|
onConnect();
|
|
@@ -192,6 +271,9 @@ export async function runSessionHost(
|
|
|
192
271
|
...(spec.verbSocketPath === undefined ? {} : { verbSocketPath: spec.verbSocketPath }),
|
|
193
272
|
...(spec.readOnly === undefined ? {} : { readOnly: spec.readOnly }),
|
|
194
273
|
...(spec.ompSettingsFile === undefined ? {} : { ompSettingsFile: spec.ompSettingsFile }),
|
|
274
|
+
...(spec.outputSchema === undefined ? {} : { outputSchema: spec.outputSchema }),
|
|
275
|
+
...(spec.outputSchemaMode === undefined ? {} : { outputSchemaMode: spec.outputSchemaMode }),
|
|
276
|
+
...(spec.requireYieldTool === undefined ? {} : { requireYieldTool: spec.requireYieldTool }),
|
|
195
277
|
// The release audit lives in the daemon's state directory, which this
|
|
196
278
|
// process may not be able to write and must not be trusted to. It
|
|
197
279
|
// becomes a message; the parent performs the durable write.
|
|
@@ -204,12 +286,15 @@ export async function runSessionHost(
|
|
|
204
286
|
// The frame must reach the parent before this process exits: `process.exit`
|
|
205
287
|
// does not flush the socket, and a parent that only observes the exit used
|
|
206
288
|
// to report a bare exit code instead of the child's own words.
|
|
289
|
+
// This is a failed start, not an identity refusal: the process still walks
|
|
290
|
+
// away with a clean exit code, because the parent's verdict comes from the
|
|
291
|
+
// frame, not the status — 91 is reserved for the boundary refusal above.
|
|
207
292
|
await new Promise<void>((resolve) => {
|
|
208
293
|
socket.once("finish", () => resolve());
|
|
209
294
|
socket.once("error", () => resolve());
|
|
210
295
|
socket.end();
|
|
211
296
|
});
|
|
212
|
-
return;
|
|
297
|
+
return true;
|
|
213
298
|
}
|
|
214
299
|
|
|
215
300
|
const live = session;
|
|
@@ -232,8 +317,28 @@ export async function runSessionHost(
|
|
|
232
317
|
...(live.modelFallbackMessage === undefined
|
|
233
318
|
? {}
|
|
234
319
|
: { modelFallbackMessage: live.modelFallbackMessage }),
|
|
320
|
+
// The loaded-module attestation (#832): what THIS process's session
|
|
321
|
+
// machinery actually executes, carried in the exact-session handoff the
|
|
322
|
+
// upgrade verifies against.
|
|
323
|
+
...(live.extensionVersion === undefined ? {} : { extensionVersion: live.extensionVersion }),
|
|
324
|
+
...(live.harnessPath === undefined ? {} : { harnessPath: live.harnessPath }),
|
|
325
|
+
...(live.harnessVersion === undefined ? {} : { harnessVersion: live.harnessVersion }),
|
|
235
326
|
});
|
|
236
327
|
|
|
328
|
+
// The code-graph session observation (#726) rides its own frame: the factory
|
|
329
|
+
// reads the session's tool registry asynchronously (the harness finalises
|
|
330
|
+
// MCP wiring after creation), so the ready handshake never waits for it. The
|
|
331
|
+
// child is the session's own process — this is the runtime observation the
|
|
332
|
+
// config check in `resolvePrereqs` deliberately is not, and an observation
|
|
333
|
+
// that never resolves simply never travels: no message is "not recorded",
|
|
334
|
+
// never "graph tools absent".
|
|
335
|
+
const graphToolsReady = live.graphToolsReady;
|
|
336
|
+
if (graphToolsReady !== undefined) {
|
|
337
|
+
void graphToolsReady.then((graphTools) => {
|
|
338
|
+
if (graphTools !== undefined) send({ t: "graph-tools", graphTools });
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
|
|
237
342
|
const { promise: finished, resolve: finish } = Promise.withResolvers<void>();
|
|
238
343
|
let buffer = "";
|
|
239
344
|
socket.on("data", (chunk: Buffer) => {
|
|
@@ -300,6 +405,7 @@ export async function runSessionHost(
|
|
|
300
405
|
// Teardown noise must not become the run's outcome.
|
|
301
406
|
}
|
|
302
407
|
socket.end();
|
|
408
|
+
return true;
|
|
303
409
|
}
|
|
304
410
|
|
|
305
411
|
/** Reads the spec the parent handed over. Argv, not stdin: stdin is the harness's. */
|
|
@@ -315,6 +421,10 @@ if (import.meta.main) {
|
|
|
315
421
|
// Exit code is the parent's only signal when the socket never opened, so a
|
|
316
422
|
// spec that will not parse fails loudly here rather than as a silent hang.
|
|
317
423
|
const spec = specFromArgv(process.argv);
|
|
318
|
-
await runSessionHost(spec);
|
|
319
|
-
process
|
|
424
|
+
const started = await runSessionHost(spec);
|
|
425
|
+
// A refused start — the launched identity did not match this process's
|
|
426
|
+
// kernel identity — exits 91 (the same code the rejected cgroup-boundary
|
|
427
|
+
// launcher used) so an operator reading `ps` or the exit status sees a
|
|
428
|
+
// boundary refusal, never a clean exit.
|
|
429
|
+
process.exit(started ? 0 : 91);
|
|
320
430
|
}
|