openmausbot 0.1.72 → 0.1.74

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 (36) hide show
  1. package/dist/assets/index-BRLHsQ5g.js +298 -0
  2. package/dist/assets/index-ByLQ7a3F.css +1 -0
  3. package/dist/assets/{index-Bpai86ua.js → index-CwoQdM0z.js} +1 -1
  4. package/dist/index.html +2 -2
  5. package/dist-server/container-mcp.js +9 -0
  6. package/dist-server/drivers/agents-proxy.js +2 -2
  7. package/dist-server/index.js +721 -484
  8. package/dist-server/mcp-gate.js +58 -16
  9. package/dist-server/openmausbot.js +236 -80
  10. package/dist-server/pair-cli.js +236 -80
  11. package/dist-server/server/auto-approve.js +30 -206
  12. package/dist-server/server/channel-queue.js +20 -2
  13. package/dist-server/server/config.js +36 -1
  14. package/dist-server/server/drivers/acp/core.js +56 -3
  15. package/dist-server/server/drivers/acp/grok.js +3 -1
  16. package/dist-server/server/drivers/agents-proxy.js +2 -2
  17. package/dist-server/server/drivers/antigravity-acp.js +5 -4
  18. package/dist-server/server/drivers/antigravity.js +9 -4
  19. package/dist-server/server/drivers/claude.js +54 -11
  20. package/dist-server/server/drivers/codex.js +86 -27
  21. package/dist-server/server/engine-install.js +9 -5
  22. package/dist-server/server/index.js +210 -231
  23. package/dist-server/server/message-db.js +70 -3
  24. package/dist-server/server/procs.js +82 -23
  25. package/dist-server/server/routines.js +67 -8
  26. package/dist-server/server/steer-queue.js +27 -7
  27. package/dist-server/server/team-manifest.js +1 -1
  28. package/dist-server/server/vps-computer.js +81 -12
  29. package/dist-server/server/webhooks.js +4 -1
  30. package/dist-server/server/workspace-backup.js +24 -9
  31. package/dist-server/shared/approval-mode.js +11 -10
  32. package/dist-server/vps-container-mcp.js +9 -0
  33. package/package.json +1 -1
  34. package/dist/assets/index-BFPYw6P-.js +0 -296
  35. package/dist/assets/index-D1odUdTi.css +0 -1
  36. package/dist-server/server/auto-review.js +0 -79
@@ -482,12 +482,15 @@ export async function createPermissionBroker(opts) {
482
482
  continue;
483
483
  }
484
484
  const ask = { id: askId, kind, tool: msg.tool ?? "tool", input: msg.input ?? {}, at: Date.now() };
485
- const finish = (behavior, message, source) => {
485
+ const finish = (behavior, message, source, always) => {
486
486
  if (!pending.delete(askId))
487
487
  return;
488
488
  clearTimeout(timer);
489
489
  try {
490
- conn.write(JSON.stringify({ t: "answer", id: askId, behavior, message }) + "\n");
490
+ // `always` rides to the proxy, which hands the CLI's own suggested
491
+ // permission rules back as updatedPermissions: Claude remembers
492
+ // the allow for the session, the harness remembers nothing.
493
+ conn.write(JSON.stringify({ t: "answer", id: askId, behavior, message, ...(always ? { always: true } : {}) }) + "\n");
491
494
  }
492
495
  catch { }
493
496
  opts.onResolve({ ...ask, behavior, source });
@@ -570,13 +573,13 @@ export async function createPermissionBroker(opts) {
570
573
  }
571
574
  };
572
575
  return {
573
- answer(askId, behavior, message) {
576
+ answer(askId, behavior, message, always) {
574
577
  const p = pending.get(askId);
575
578
  if (!p)
576
579
  return false;
577
580
  if (p.ask.kind === "question" ? behavior !== "answer" : behavior === "answer")
578
581
  return false;
579
- p.finish(behavior, message, "user");
582
+ p.finish(behavior, message, "user", always && behavior === "allow");
580
583
  return true;
581
584
  },
582
585
  pause() {
@@ -752,6 +755,12 @@ export const ClaudeDriver = {
752
755
  ? configuredIdleMinimum
753
756
  : 10_000;
754
757
  const SESSION_IDLE_MS = Math.max(sessionIdleMinimum, Number(process.env.OMB_CLAUDE_SESSION_IDLE_MS) || 10 * 60_000);
758
+ const stopSession = (session) => {
759
+ void killCliTree(session.child).then((stopped) => {
760
+ if (stopped)
761
+ void session.finishClose?.();
762
+ });
763
+ };
755
764
  const closeSession = (threadId, why) => {
756
765
  const s = sessions.get(threadId);
757
766
  if (!s || s.closing)
@@ -772,8 +781,7 @@ export const ClaudeDriver = {
772
781
  }
773
782
  catch { }
774
783
  const kill = setTimeout(() => {
775
- if (s.child.exitCode === null)
776
- killCliTree(s.child);
784
+ stopSession(s);
777
785
  }, 5_000);
778
786
  kill.unref?.();
779
787
  };
@@ -833,7 +841,8 @@ export const ClaudeDriver = {
833
841
  const permissionMode = turn.approvalMode === undefined
834
842
  ? config.permissionMode
835
843
  : turn.approvalMode === "full" ? "bypassPermissions"
836
- : turn.approvalMode === "auto" ? "auto" : "default";
844
+ : turn.approvalMode === "auto" ? "auto"
845
+ : turn.approvalMode === "edits" ? "acceptEdits" : "default";
837
846
  const controlsHost = turn.integrations?.localComputer?.scope === "local-computer";
838
847
  if (controlsHost && permissionMode === "bypassPermissions" && turn.approvalMode !== "full") {
839
848
  throw new Error("local computer control requires the interactive approval broker");
@@ -1049,7 +1058,7 @@ export const ClaudeDriver = {
1049
1058
  live.turn = { turnId, settled: false, sawStreamDelta: false };
1050
1059
  active.set(threadId, { stop: () => {
1051
1060
  closeSession(threadId, "interrupted");
1052
- killCliTree(live.child);
1061
+ stopSession(live);
1053
1062
  }, turnId, broker: live.broker });
1054
1063
  emit({ ...base(threadId, turnId), type: "turn.started" });
1055
1064
  const volatile = turn.systemVolatile ?? "";
@@ -1121,6 +1130,13 @@ export const ClaudeDriver = {
1121
1130
  onAsk: (ask) => {
1122
1131
  const eventTurnId = sessions.get(threadId)?.turn?.turnId ?? turnId;
1123
1132
  askTools.set(ask.id, typeof ask.tool === "string" ? ask.tool : undefined);
1133
+ // Auto was requested: say whether the CLI's reviewer is actually
1134
+ // running, from init, so the harness can tell a classifier's
1135
+ // verdict from a Manual session asking about everything.
1136
+ const nativeMode = sessions.get(threadId)?.nativePermissionMode ?? null;
1137
+ const nativeReview = permissionMode === "auto" && nativeMode !== null
1138
+ ? nativeMode === "auto" ? "active" : "inactive"
1139
+ : undefined;
1124
1140
  emit({
1125
1141
  ...base(threadId, eventTurnId),
1126
1142
  type: "request.opened",
@@ -1128,6 +1144,10 @@ export const ClaudeDriver = {
1128
1144
  requestType: ask.kind,
1129
1145
  tool: ask.tool,
1130
1146
  summary: askSummary(ask),
1147
+ nativeReview,
1148
+ // the proxy hands Claude its own suggested rules on `always`;
1149
+ // host control stays one action at a time
1150
+ allowSession: ask.kind === "permission" && !(controlsHost && typeof ask.tool === "string" && ask.tool.startsWith("mcp__computer")) ? true : undefined,
1131
1151
  approvalScope: typeof ask.tool === "string" && controlsHost && ask.tool.startsWith("mcp__computer")
1132
1152
  ? "local-computer"
1133
1153
  : undefined,
@@ -1190,6 +1210,7 @@ export const ClaudeDriver = {
1190
1210
  volatile: turn.systemVolatile ?? "",
1191
1211
  sessionId: sessionId ?? newSessionId,
1192
1212
  sawInit: false,
1213
+ nativePermissionMode: null,
1193
1214
  turn: { turnId, settled: false, sawStreamDelta: false },
1194
1215
  idleTimer: null,
1195
1216
  closing: false,
@@ -1231,6 +1252,8 @@ export const ClaudeDriver = {
1231
1252
  };
1232
1253
  const currentTurnId = () => session.turn?.turnId ?? turnId;
1233
1254
  const handleLine = (line) => {
1255
+ if (session.closing)
1256
+ return;
1234
1257
  let o;
1235
1258
  try {
1236
1259
  o = JSON.parse(line);
@@ -1243,6 +1266,7 @@ export const ClaudeDriver = {
1243
1266
  case "system":
1244
1267
  if (o.subtype === "init") {
1245
1268
  session.sawInit = true;
1269
+ session.nativePermissionMode = typeof o.permissionMode === "string" ? o.permissionMode : null;
1246
1270
  if (typeof o.session_id === "string")
1247
1271
  session.sessionId = o.session_id;
1248
1272
  emit({ ...base(threadId, currentTurnId()), type: "session.started", sessionId: o.session_id, model: o.model });
@@ -1371,7 +1395,22 @@ export const ClaudeDriver = {
1371
1395
  emit({ ...base(threadId, currentTurnId()), type: "runtime.error", ...describeSpawnFailure(e, config.cli) });
1372
1396
  settle(false, "spawn_error");
1373
1397
  });
1374
- child.on("close", (code) => {
1398
+ let closeFinalized = false;
1399
+ const finalizeClose = async (code) => {
1400
+ if (closeFinalized)
1401
+ return;
1402
+ // The root can close while its MCP helpers are still running. Join
1403
+ // an in-flight stop (or reap its remaining group) before releasing
1404
+ // the turn so a replacement cannot overlap the old helpers.
1405
+ if (!(await killCliTree(child, 0))) {
1406
+ session.broker?.close();
1407
+ session.broker = undefined;
1408
+ emit({ ...base(threadId, currentTurnId()), type: "runtime.error", message: "Claude could not be confirmed stopped; its helper processes may still be running." });
1409
+ return;
1410
+ }
1411
+ if (closeFinalized)
1412
+ return;
1413
+ closeFinalized = true;
1375
1414
  // a turn still running when the process died is a failed turn; a
1376
1415
  // process that exited between turns (idle close, contract change)
1377
1416
  // is just a session ending
@@ -1536,6 +1575,10 @@ export const ClaudeDriver = {
1536
1575
  removePrivateTempDir(session.systemPromptPath);
1537
1576
  if (sessions.get(threadId) === session)
1538
1577
  sessions.delete(threadId);
1578
+ };
1579
+ child.on("close", (code) => {
1580
+ session.finishClose = () => finalizeClose(code);
1581
+ void session.finishClose();
1539
1582
  });
1540
1583
  const stop = () => {
1541
1584
  // taskkill is asynchronous on Windows. Retire steering and approvals
@@ -1543,7 +1586,7 @@ export const ClaudeDriver = {
1543
1586
  closeSession(threadId, "interrupted");
1544
1587
  retry.cancelled = true;
1545
1588
  retryAbort.abort();
1546
- killCliTree(child);
1589
+ stopSession(session);
1547
1590
  };
1548
1591
  active.set(threadId, { stop, turnId, broker });
1549
1592
  emit({ ...base(threadId, turnId), type: "turn.started" });
@@ -1683,7 +1726,7 @@ export const ClaudeDriver = {
1683
1726
  if (!broker)
1684
1727
  return "unavailable";
1685
1728
  const behavior = decision.behavior === "answer" ? "answer" : decision.behavior;
1686
- if (!broker.answer(requestId, behavior, decision.message))
1729
+ if (!broker.answer(requestId, behavior, decision.message, decision.always))
1687
1730
  return "unavailable";
1688
1731
  return behavior === "allow" ? "allowed-once" : behavior === "answer" ? "answered" : "rejected";
1689
1732
  },
@@ -181,6 +181,9 @@ function mcpAppApprovalForm(params) {
181
181
  /** Codex persists these values on its native thread. Keep them explicit on
182
182
  * start, resume, and every turn so switching modes cannot leave a more
183
183
  * permissive sandbox/reviewer stuck to the next request. */
184
+ /** Ask and Edits both run Codex's workspace-write sandbox with the person as
185
+ * reviewer: Codex has no narrower "edits only" mode, so the selector never
186
+ * offers Edits for it (supportsApprovalMode) and a stray value asks. */
184
187
  function namedApprovalParams(mode) {
185
188
  if (mode === "full") {
186
189
  return {
@@ -515,6 +518,10 @@ export const CodexDriver = {
515
518
  stdio: ["pipe", "pipe", "pipe"],
516
519
  });
517
520
  let abandoned = false;
521
+ let codexThreadId = null;
522
+ let codexTurnId = null;
523
+ let startingNativeTurn = false;
524
+ const earlyNotifications = [];
518
525
  const state = {
519
526
  settled: false,
520
527
  lastText: "",
@@ -553,6 +560,21 @@ export const CodexDriver = {
553
560
  rpcPending.set(id, {
554
561
  resolve: (v) => {
555
562
  clearTimeout(timer);
563
+ if (method === "turn/start") {
564
+ if (typeof v?.turn?.id !== "string" || !v.turn.id) {
565
+ reject(new Error("Codex did not return a native turn id"));
566
+ return;
567
+ }
568
+ // Bind synchronously: a single stdout chunk can contain the
569
+ // response, streamed events, completion and a late request.
570
+ codexTurnId = v.turn.id;
571
+ startingNativeTurn = false;
572
+ for (const notification of earlyNotifications.splice(0)) {
573
+ if (state.settled)
574
+ break;
575
+ handleNotification(notification);
576
+ }
577
+ }
556
578
  resolve(v);
557
579
  },
558
580
  reject: (e) => {
@@ -563,10 +585,18 @@ export const CodexDriver = {
563
585
  send({ jsonrpc: "2.0", id, method, params });
564
586
  });
565
587
  let stopping;
566
- const terminate = () => stopping ??= killCliTree(child);
567
- const stop = () => {
588
+ const terminate = () => stopping ??= killCliTree(child).then((stopped) => {
589
+ if (!stopped)
590
+ stopping = undefined;
591
+ return stopped;
592
+ });
593
+ let completeStoppedTurn;
594
+ const stop = async () => {
568
595
  stopRequested = true;
569
- return terminate();
596
+ const stopped = await terminate();
597
+ if (stopped)
598
+ completeStoppedTurn?.();
599
+ return stopped;
570
600
  };
571
601
  const settle = async (ok, stopReason) => {
572
602
  if (state.settled)
@@ -583,15 +613,9 @@ export const CodexDriver = {
583
613
  active.delete(threadId);
584
614
  emit({ ...base(threadId, turnId), type: "turn.completed", ok, stopReason, cost: null, ...(state.usage ? { usage: state.usage } : {}) });
585
615
  };
586
- if (await stop()) {
587
- complete();
588
- }
589
- else {
616
+ completeStoppedTurn = complete;
617
+ if (!(await stop())) {
590
618
  emit({ ...base(threadId, turnId), type: "runtime.error", message: "codex did not shut down after termination was requested" });
591
- if (child.exitCode !== null || child.signalCode !== null)
592
- complete();
593
- else
594
- child.once("close", complete);
595
619
  }
596
620
  };
597
621
  // server→client approval request → canonical request.opened
@@ -710,6 +734,33 @@ export const CodexDriver = {
710
734
  };
711
735
  const handleNotification = (msg) => {
712
736
  const p = msg.params ?? {};
737
+ // An app-server also emits notifications for native helper threads.
738
+ // Only this request's parent may write its transcript/usage or settle
739
+ // its run. Requests still use the approval broker above, including
740
+ // helper requests; ignoring child *notifications* must not grant tools.
741
+ const connectionError = msg.method === "error" &&
742
+ !("threadId" in p) && !("turnId" in p);
743
+ if (!connectionError) {
744
+ if (!codexThreadId || p.threadId !== codexThreadId)
745
+ return;
746
+ if (!codexTurnId) {
747
+ // Some servers stream before acknowledging turn/start. Retain a
748
+ // bounded prefix, then filter against the authoritative response.
749
+ if (startingNativeTurn) {
750
+ if (earlyNotifications.length >= 1024) {
751
+ void settle(false, "too_many_events_before_turn_start");
752
+ }
753
+ else {
754
+ earlyNotifications.push(msg);
755
+ }
756
+ }
757
+ return;
758
+ }
759
+ const eventTurnId = msg.method === "turn/started" || msg.method === "turn/completed"
760
+ ? p.turn?.id : p.turnId;
761
+ if (eventTurnId !== codexTurnId)
762
+ return;
763
+ }
713
764
  switch (msg.method) {
714
765
  // token-level chat text; the item/completed frame follows with the
715
766
  // whole message, so its delta is only a fallback when none streamed
@@ -897,6 +948,12 @@ export const CodexDriver = {
897
948
  child.on("close", (code) => {
898
949
  if (abandoned)
899
950
  return;
951
+ if (state.settled) {
952
+ // Root exit alone cannot release a turn after an uncertain stop.
953
+ // Recheck its group; an explicit later Stop can also retry this.
954
+ void stop();
955
+ return;
956
+ }
900
957
  if (!state.settled) {
901
958
  emit({
902
959
  ...base(threadId, turnId),
@@ -959,7 +1016,6 @@ export const CodexDriver = {
959
1016
  // on start AND resume so Codex owns their lifetime through compaction.
960
1017
  // Removed bot rules are cleared without dropping native configured rules.
961
1018
  const cursor = typeof turn.resumeCursor === "string" ? turn.resumeCursor : null;
962
- let codexThreadId = null;
963
1019
  let startedModel = null;
964
1020
  if (cursor) {
965
1021
  try {
@@ -1020,21 +1076,24 @@ export const CodexDriver = {
1020
1076
  ...(promptText ? [{ type: "text", text: promptText }] : []),
1021
1077
  ...(turn.images ?? []).map((image) => ({ type: "localImage", path: image.path })),
1022
1078
  ];
1023
- const startTurn = () => request("turn/start", {
1024
- threadId: codexThreadId,
1025
- input: turnInput,
1026
- ...approvalParams.turn,
1027
- // Spread, not `effort: turn.effort ?? null`. Probed against
1028
- // codex-cli 0.146.0: null is indistinguishable from an absent key
1029
- // — both leave the thread's current effort alone, emitting no
1030
- // thread/settings/updated, and thread/resume reads the old value
1031
- // back. The app-server offers no way to clear a level either:
1032
- // "" is rejected outright and thread/start takes no effort at
1033
- // all. So a thread keeps the last level it was sent until it is
1034
- // sent another, and choosing Default lands on the bot's next new
1035
- // thread rather than the current one.
1036
- ...(turn.effort ? { effort: turn.effort } : {}),
1037
- });
1079
+ const startTurn = () => {
1080
+ startingNativeTurn = true;
1081
+ return request("turn/start", {
1082
+ threadId: codexThreadId,
1083
+ input: turnInput,
1084
+ ...approvalParams.turn,
1085
+ // Spread, not `effort: turn.effort ?? null`. Probed against
1086
+ // codex-cli 0.146.0: null is indistinguishable from an absent key
1087
+ // — both leave the thread's current effort alone, emitting no
1088
+ // thread/settings/updated, and thread/resume reads the old value
1089
+ // back. The app-server offers no way to clear a level either:
1090
+ // "" is rejected outright and thread/start takes no effort at
1091
+ // all. So a thread keeps the last level it was sent until it is
1092
+ // sent another, and choosing Default lands on the bot's next new
1093
+ // thread rather than the current one.
1094
+ ...(turn.effort ? { effort: turn.effort } : {}),
1095
+ });
1096
+ };
1038
1097
  try {
1039
1098
  await startTurn();
1040
1099
  }
@@ -112,10 +112,16 @@ function runNpm(args, env, cwd, timeoutMs) {
112
112
  let timedOut = false;
113
113
  const timer = setTimeout(() => {
114
114
  timedOut = true;
115
- void killCliTree(child);
115
+ void killCliTree(child).then((stopped) => {
116
+ rejectRun(new Error(stopped
117
+ ? "The install took too long and was stopped. Check the server's network connection and try again."
118
+ : "The install took too long, but npm could not be confirmed stopped. Ask the server administrator to stop the install process before trying again."));
119
+ });
116
120
  }, timeoutMs);
117
121
  timer.unref();
118
122
  child.once("error", (error) => {
123
+ if (timedOut)
124
+ return; // A failed kill is not a failed npm launch.
119
125
  clearTimeout(timer);
120
126
  rejectRun(new Error(error.code === "ENOENT"
121
127
  ? "npm is not installed on this server. Install Node.js with npm for the user running OpenMausBot, then try again."
@@ -123,10 +129,8 @@ function runNpm(args, env, cwd, timeoutMs) {
123
129
  });
124
130
  child.once("close", (code) => {
125
131
  clearTimeout(timer);
126
- if (timedOut) {
127
- rejectRun(new Error("The install took too long and was stopped. Check the server's network connection and try again."));
128
- return;
129
- }
132
+ if (timedOut)
133
+ return; // The whole group must stop, not just npm's root.
130
134
  resolveRun({ code, output });
131
135
  });
132
136
  });