openmausbot 0.1.73 → 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 (33) 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-BYbK7ber.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/index.js +592 -453
  7. package/dist-server/mcp-gate.js +58 -16
  8. package/dist-server/openmausbot.js +186 -64
  9. package/dist-server/pair-cli.js +186 -64
  10. package/dist-server/server/auto-approve.js +30 -206
  11. package/dist-server/server/channel-queue.js +20 -2
  12. package/dist-server/server/config.js +36 -1
  13. package/dist-server/server/drivers/acp/core.js +56 -3
  14. package/dist-server/server/drivers/acp/grok.js +3 -1
  15. package/dist-server/server/drivers/antigravity-acp.js +5 -4
  16. package/dist-server/server/drivers/antigravity.js +9 -4
  17. package/dist-server/server/drivers/claude.js +54 -11
  18. package/dist-server/server/drivers/codex.js +22 -11
  19. package/dist-server/server/engine-install.js +9 -5
  20. package/dist-server/server/index.js +185 -223
  21. package/dist-server/server/message-db.js +70 -3
  22. package/dist-server/server/procs.js +82 -23
  23. package/dist-server/server/routines.js +67 -8
  24. package/dist-server/server/steer-queue.js +27 -7
  25. package/dist-server/server/team-manifest.js +1 -1
  26. package/dist-server/server/webhooks.js +4 -1
  27. package/dist-server/server/workspace-backup.js +24 -9
  28. package/dist-server/shared/approval-mode.js +11 -10
  29. package/dist-server/vps-container-mcp.js +9 -0
  30. package/package.json +1 -1
  31. package/dist/assets/index-BLJ7byG3.js +0 -296
  32. package/dist/assets/index-De2TRLxu.css +0 -1
  33. 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 {
@@ -582,10 +585,18 @@ export const CodexDriver = {
582
585
  send({ jsonrpc: "2.0", id, method, params });
583
586
  });
584
587
  let stopping;
585
- const terminate = () => stopping ??= killCliTree(child);
586
- 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 () => {
587
595
  stopRequested = true;
588
- return terminate();
596
+ const stopped = await terminate();
597
+ if (stopped)
598
+ completeStoppedTurn?.();
599
+ return stopped;
589
600
  };
590
601
  const settle = async (ok, stopReason) => {
591
602
  if (state.settled)
@@ -602,15 +613,9 @@ export const CodexDriver = {
602
613
  active.delete(threadId);
603
614
  emit({ ...base(threadId, turnId), type: "turn.completed", ok, stopReason, cost: null, ...(state.usage ? { usage: state.usage } : {}) });
604
615
  };
605
- if (await stop()) {
606
- complete();
607
- }
608
- else {
616
+ completeStoppedTurn = complete;
617
+ if (!(await stop())) {
609
618
  emit({ ...base(threadId, turnId), type: "runtime.error", message: "codex did not shut down after termination was requested" });
610
- if (child.exitCode !== null || child.signalCode !== null)
611
- complete();
612
- else
613
- child.once("close", complete);
614
619
  }
615
620
  };
616
621
  // server→client approval request → canonical request.opened
@@ -943,6 +948,12 @@ export const CodexDriver = {
943
948
  child.on("close", (code) => {
944
949
  if (abandoned)
945
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
+ }
946
957
  if (!state.settled) {
947
958
  emit({
948
959
  ...base(threadId, turnId),
@@ -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
  });