omp-conductor 0.18.0 → 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.
Files changed (61) hide show
  1. package/README.md +34 -0
  2. package/REFERENCE.md +60 -10
  3. package/agents/to-spec.md +90 -0
  4. package/package.json +2 -1
  5. package/schema/config.schema.json +29 -0
  6. package/src/admission.ts +204 -75
  7. package/src/ask.ts +268 -7
  8. package/src/board.ts +17 -3
  9. package/src/briefs/orchestrator.md +42 -14
  10. package/src/briefs/to-spec.md +84 -0
  11. package/src/briefs/worker.md +2 -1
  12. package/src/cli.ts +2 -0
  13. package/src/command-help.ts +11 -0
  14. package/src/command-manifest.ts +22 -0
  15. package/src/commands/context.ts +1 -0
  16. package/src/commands/drain.ts +176 -0
  17. package/src/commands/extend.ts +6 -10
  18. package/src/commands/status.ts +5 -1
  19. package/src/commands/watch.ts +50 -2
  20. package/src/commands/worker.ts +9 -10
  21. package/src/config-schema.ts +24 -0
  22. package/src/config.ts +42 -1
  23. package/src/daemon.ts +965 -36
  24. package/src/dashboard/app.js +4 -1
  25. package/src/dashboard/server.ts +5 -2
  26. package/src/decisions.ts +235 -17
  27. package/src/diff-flags.ts +75 -1
  28. package/src/doctor.ts +52 -0
  29. package/src/escalate.ts +9 -3
  30. package/src/failure-class.ts +28 -2
  31. package/src/fleet.ts +146 -22
  32. package/src/gitops.ts +188 -81
  33. package/src/graph-health.ts +35 -1
  34. package/src/graph.ts +66 -1
  35. package/src/harness-loader.ts +59 -0
  36. package/src/host.ts +567 -2
  37. package/src/lifecycle.ts +122 -1
  38. package/src/omp.ts +227 -20
  39. package/src/orchestrator-tick.ts +1386 -15
  40. package/src/orchestrator.ts +12 -0
  41. package/src/privileged.ts +1 -4
  42. package/src/release-policy.ts +503 -9
  43. package/src/session-host.ts +99 -5
  44. package/src/settlement.ts +69 -17
  45. package/src/setup-host.ts +1205 -6
  46. package/src/setup-install.ts +28 -0
  47. package/src/setup-wizard.ts +13 -2
  48. package/src/setup.ts +29 -13
  49. package/src/shell.ts +15 -0
  50. package/src/status-render.ts +78 -11
  51. package/src/store.ts +443 -42
  52. package/src/to-spec.ts +387 -0
  53. package/src/tracker/github.ts +104 -14
  54. package/src/types.ts +343 -13
  55. package/src/upgrade-verify.ts +209 -2
  56. package/src/upgrade.ts +175 -1
  57. package/src/verbs/protocol.ts +39 -0
  58. package/src/verbs/server.ts +730 -56
  59. package/src/verbs/socket.ts +24 -5
  60. package/src/worker.ts +25 -2
  61. package/src/worktree.ts +29 -12
@@ -215,21 +215,40 @@ export function unlinkStaleSocket(path: string): void {
215
215
  }
216
216
 
217
217
  /**
218
- * Restrict a bound socket to the daemon's own uid.
218
+ * Restrict a bound socket to the daemon's own uid, or to the worker identity
219
+ * that owns the channel when `owner` is given.
219
220
  *
220
221
  * Returns the guarantee in force, because #126 asks the daemon to *state* its
221
222
  * mechanism at startup rather than guess: the socket is `0600` under a `0711`
222
223
  * daemon-owned parent, so it is one-user, and the unguessable suffix plus that
223
- * parent are the whole story. Saying so is the difference between an audited
224
- * boundary and a hopeful one.
224
+ * parent are the whole story. Under the worker identity the one user is the
225
+ * worker account the socket is chowned to it, because the child that must
226
+ * connect to it runs as that uid and nothing else does. Saying so is the
227
+ * difference between an audited boundary and a hopeful one.
225
228
  */
226
- export type SocketOwnership = "daemon-user";
229
+ export type SocketOwnership = "daemon-user" | "worker-user";
230
+
231
+ export interface SecureBoundSocketOptions {
232
+ /** The chmod call; injected so tests pin the mode without a real socket. */
233
+ chmod?: (p: string, mode: number) => void;
234
+ /**
235
+ * The uid/gid the socket belongs to (the worker identity for a run
236
+ * channel). Absent, the socket stays daemon-owned and only the daemon's
237
+ * uid can connect.
238
+ */
239
+ owner?: { uid: number; gid: number };
240
+ }
227
241
 
228
242
  export function secureBoundSocket(
229
243
  path: string,
230
- chmod: (p: string, mode: number) => void = chmodSync,
244
+ options: SecureBoundSocketOptions = {},
231
245
  ): SocketOwnership {
246
+ const chmod = options.chmod ?? chmodSync;
232
247
  chmod(path, VERB_SOCKET_MODE);
248
+ if (options.owner !== undefined) {
249
+ chownSync(path, options.owner.uid, options.owner.gid);
250
+ return "worker-user";
251
+ }
233
252
  return "daemon-user";
234
253
  }
235
254
 
package/src/worker.ts CHANGED
@@ -12,9 +12,10 @@
12
12
 
13
13
  import { mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
14
14
  import { join } from "node:path";
15
+ import type { WorkerIdentity } from "./host.ts";
15
16
  import { createSession, disposeSession, SessionAdmissionClosedError, type AgentSessionLike } from "./omp.ts";
16
17
  import type { GateShape, ReleaseBlockContext } from "./release-policy.ts";
17
- import type { Caps, ResolvedGrants, RunState } from "./types.ts";
18
+ import type { Caps, GraphToolsObservation, ResolvedGrants, RunState } from "./types.ts";
18
19
 
19
20
  /** Structured evidence fields from the worker's final report. */
20
21
  const PR_URL_PATTERN = /^pr:\s*(https:\/\/github\.com\/\S+\/pull\/\d+)\s*$/im;
@@ -217,6 +218,17 @@ export interface WorkerOpts {
217
218
  onReleaseBlocked?: (shape: GateShape, context: ReleaseBlockContext) => void;
218
219
  /** The child's pid, the instant it exists. See {@link VerbListener.bindPid}. */
219
220
  onSpawn?: (pid: number) => void;
221
+ /**
222
+ * The worker identity this session runs under (#798): the dedicated
223
+ * least-privilege account the daemon resolved for worker launch. Forwarded
224
+ * to `createSession`, which launches the child through the identity
225
+ * transition (setpriv), re-points its environment at the worker's own home,
226
+ * grants it the control socket, and refuses the session unless its kernel
227
+ * uid/gid match. The daemon supplies it for every worker launch and refuses
228
+ * to dispatch a worker without it; absent, the session runs as the
229
+ * caller's own identity (orchestrator, tests).
230
+ */
231
+ workerIdentity?: WorkerIdentity;
220
232
  /** Control socket for that child, beside the run's own session directory. */
221
233
  socketPath?: string;
222
234
  /**
@@ -330,6 +342,15 @@ export interface WorkerResult {
330
342
  * is otherwise indistinguishable from a run that was merely unlucky.
331
343
  */
332
344
  modelFallbackMessage?: string;
345
+ /**
346
+ * The code-graph session observation (#726): what the session's own registry
347
+ * held at start, read off the session exactly like `modelFallbackMessage`.
348
+ * Absent means the session surface did not record one — never "graph tools
349
+ * absent", which is the `present: false` truth value. A dispatched run that
350
+ * records it lets the doctor tell "the model ignored a tool it had" from
351
+ * "the tool was missing" apart.
352
+ */
353
+ graphTools?: GraphToolsObservation;
333
354
  }
334
355
 
335
356
  /**
@@ -550,6 +571,7 @@ export async function runWorker(
550
571
  role: "worker",
551
572
  ...(o.releaseGrants === undefined ? {} : { releaseGrants: o.releaseGrants }),
552
573
  ...(o.onReleaseBlocked === undefined ? {} : { onReleaseBlocked: o.onReleaseBlocked }),
574
+ ...(o.workerIdentity === undefined ? {} : { identity: o.workerIdentity }),
553
575
  ...(o.onSpawn === undefined ? {} : { onSpawn: o.onSpawn }),
554
576
  ...(o.socketPath === undefined ? {} : { socketPath: o.socketPath }),
555
577
  ...(o.verbSocketPath === undefined ? {} : { verbSocketPath: o.verbSocketPath }),
@@ -613,7 +635,7 @@ export async function runWorker(
613
635
  const withSessionFacts = (
614
636
  result: Omit<WorkerResult, ReliabilityKeys>,
615
637
  ): Omit<WorkerResult, ReliabilityKeys> => {
616
- const { sessionFile, modelFallbackMessage } = session;
638
+ const { sessionFile, modelFallbackMessage, graphTools } = session;
617
639
  if (!advisorSpendFolded) {
618
640
  advisorSpendFolded = true;
619
641
  // Advisor turns live in their own `__advisor*.jsonl` and never surface
@@ -634,6 +656,7 @@ export async function runWorker(
634
656
  spendUsd,
635
657
  ...(sessionFile === undefined ? {} : { sessionFile }),
636
658
  ...(modelFallbackMessage === undefined ? {} : { modelFallbackMessage }),
659
+ ...(graphTools === undefined ? {} : { graphTools }),
637
660
  };
638
661
  };
639
662
 
package/src/worktree.ts CHANGED
@@ -27,7 +27,7 @@
27
27
  import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
28
28
  import { dirname, join } from "node:path";
29
29
 
30
- import { credentialedEnv, scrubUserinfo } from "./gitops.ts";
30
+ import { credentialedEnv, runRepoSafeDirectoryExemption, scrubUserinfo } from "./gitops.ts";
31
31
  import type { RepoTarget } from "./types.ts";
32
32
 
33
33
  /**
@@ -255,7 +255,9 @@ function refreshManagedExclude(worktree: string): void {
255
255
  try {
256
256
  // `--git-common-dir` is relative for linked worktrees; resolve against the
257
257
  // tree so bare-mirror layouts and plain clones both land on info/exclude.
258
- const common = Bun.spawnSync(["git", "rev-parse", "--git-common-dir"], {
258
+ // The tree is worker-owned by the time salvage runs (#798), so the
259
+ // exact-path exemption rides on the argv (#816).
260
+ const common = Bun.spawnSync(["git", ...runRepoSafeDirectoryExemption(worktree), "rev-parse", "--git-common-dir"], {
259
261
  cwd: worktree,
260
262
  stdin: "ignore",
261
263
  stdout: "pipe",
@@ -640,11 +642,16 @@ export async function salvageWip(
640
642
  // and dies with the next `worktree remove --force` (#44).
641
643
  refreshManagedExclude(worktree);
642
644
 
643
- if ((await git(["status", "--porcelain"], worktree)) === "") return { kind: "nothing" };
645
+ // The tree is worker-owned by the time salvage runs (#798): every daemon
646
+ // git call against it carries the exact per-run ownership exemption —
647
+ // never a wildcard, never a global config entry (#816).
648
+ const exempt = runRepoSafeDirectoryExemption(worktree);
649
+
650
+ if ((await git([...exempt, "status", "--porcelain"], worktree)) === "") return { kind: "nothing" };
644
651
 
645
652
  // The tree's own branch, not one the caller believes it should be on: this
646
653
  // string ends up in an escalation as the place to go looking.
647
- const branch = await git(["rev-parse", "--abbrev-ref", "HEAD"], worktree);
654
+ const branch = await git([...exempt, "rev-parse", "--abbrev-ref", "HEAD"], worktree);
648
655
 
649
656
  // `-A` on purpose: the losses this exists for were mostly *new* files.
650
657
  //
@@ -660,13 +667,13 @@ export async function salvageWip(
660
667
  // A literal `:(exclude)<path>` was also an outright bug: git counts it as
661
668
  // naming the path, so an already-ignored file made `add` exit 1 and every
662
669
  // cap-kill would have reported a salvage *failure*.
663
- await git(["add", "-A"], worktree);
670
+ await git([...exempt, "add", "-A"], worktree);
664
671
 
665
672
  // The dirty check above ran before git applied its ignores, so a tree whose
666
673
  // only changes were ignored scratch had work by that test and none by this.
667
674
  // Without this, `commit` exits non-zero on an empty index and a tree
668
675
  // holding nothing worth keeping gets reported as a salvage *failure*.
669
- const cached = await git(["diff", "--cached", "--name-status"], worktree);
676
+ const cached = await git([...exempt, "diff", "--cached", "--name-status"], worktree);
670
677
  if (cached === "") {
671
678
  return { kind: "nothing" };
672
679
  }
@@ -674,6 +681,7 @@ export async function salvageWip(
674
681
  const msg = salvageCommitMessage(issue, attempt, ending, files, newPaths);
675
682
  await git(
676
683
  [
684
+ ...exempt,
677
685
  ...SALVAGE_COMMIT_CONFIG,
678
686
  "commit",
679
687
  "--no-verify",
@@ -684,7 +692,7 @@ export async function salvageWip(
684
692
  ],
685
693
  worktree,
686
694
  );
687
- const sha = await git(["rev-parse", "HEAD"], worktree);
695
+ const sha = await git([...exempt, "rev-parse", "HEAD"], worktree);
688
696
  const salvaged = { kind: "salvaged" as const, sha, branch, files, newPaths };
689
697
 
690
698
  if (branch === "HEAD") {
@@ -784,7 +792,12 @@ async function repairAlternates(
784
792
  worktreePath: string,
785
793
  mirrorPath: string,
786
794
  ): Promise<{ kind: "ok" } | { kind: "quarantine"; detail: string }> {
787
- const common = await runGit(["rev-parse", "--git-common-dir"], worktreePath);
795
+ // The tree is worker-owned by cleanup time (#798); the exact-path exemption
796
+ // makes this read the daemon's own (#816).
797
+ const common = await runGit(
798
+ [...runRepoSafeDirectoryExemption(worktreePath), "rev-parse", "--git-common-dir"],
799
+ worktreePath,
800
+ );
788
801
  if (common.code !== 0) {
789
802
  return {
790
803
  kind: "quarantine",
@@ -868,11 +881,15 @@ export async function cleanupRetainedWorktree(
868
881
  };
869
882
  }
870
883
 
871
- const dirty = await git(["status", "--porcelain"], worktreePath);
884
+ // The tree is worker-owned by cleanup time (#798): every daemon git call
885
+ // against it carries the exact per-run exemption (#816).
886
+ const exempt = runRepoSafeDirectoryExemption(worktreePath);
887
+
888
+ const dirty = await git([...exempt, "status", "--porcelain"], worktreePath);
872
889
  if (dirty !== "") {
873
890
  return { kind: "retained", reason: "dirty", detail: "worktree has uncommitted changes" };
874
891
  }
875
- const actual = await git(["rev-parse", "--abbrev-ref", "HEAD"], worktreePath);
892
+ const actual = await git([...exempt, "rev-parse", "--abbrev-ref", "HEAD"], worktreePath);
876
893
  if (actual !== branch) {
877
894
  return {
878
895
  kind: "retained",
@@ -907,7 +924,7 @@ export async function cleanupRetainedWorktree(
907
924
  if (existsSync(worktreePath)) {
908
925
  try {
909
926
  await git(
910
- ["fetch", "--no-tags", mirrorPath, "+refs/remotes/origin/*:refs/remotes/origin/*"],
927
+ [...runRepoSafeDirectoryExemption(worktreePath), "fetch", "--no-tags", mirrorPath, "+refs/remotes/origin/*:refs/remotes/origin/*"],
911
928
  worktreePath,
912
929
  );
913
930
  } catch (err) {
@@ -917,7 +934,7 @@ export async function cleanupRetainedWorktree(
917
934
  detail: `the run repository's remote refs could not be read: ${err instanceof Error ? err.message : String(err)}`,
918
935
  };
919
936
  }
920
- const runUnique = await git(["rev-list", ref, "--not", "--remotes"], worktreePath);
937
+ const runUnique = await git([...runRepoSafeDirectoryExemption(worktreePath), "rev-list", ref, "--not", "--remotes"], worktreePath);
921
938
  if (runUnique !== "") {
922
939
  return {
923
940
  kind: "retained",