openmausbot 0.1.80 → 0.1.82

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 (58) hide show
  1. package/README.md +6 -5
  2. package/dist/assets/{index-D6RZKWks.js → index-5HVwp5m2.js} +1 -1
  3. package/dist/assets/index-BLLsDK2F.js +309 -0
  4. package/dist/assets/index-CKysBq-V.css +1 -0
  5. package/dist/index.html +2 -2
  6. package/dist-server/container-mcp.js +41 -10
  7. package/dist-server/drivers/agents-proxy.js +50 -11
  8. package/dist-server/index.js +3525 -1296
  9. package/dist-server/openmausbot.js +412 -84
  10. package/dist-server/pair-cli.js +412 -84
  11. package/dist-server/server/box-delete-journal.js +471 -0
  12. package/dist-server/server/box.js +345 -75
  13. package/dist-server/server/cli-phone-setup.js +2 -2
  14. package/dist-server/server/cli.js +84 -12
  15. package/dist-server/server/config.js +48 -18
  16. package/dist-server/server/container-computer.js +7 -0
  17. package/dist-server/server/contracts.js +5 -0
  18. package/dist-server/server/default-model-selection.js +1 -0
  19. package/dist-server/server/drivers/acp/core.js +113 -19
  20. package/dist-server/server/drivers/acp/opencode-go.js +19 -0
  21. package/dist-server/server/drivers/agents-proxy.js +49 -11
  22. package/dist-server/server/drivers/chat-mcp-tools.js +6 -1
  23. package/dist-server/server/drivers/claude.js +33 -2
  24. package/dist-server/server/drivers/codex.js +93 -8
  25. package/dist-server/server/drivers/native.js +6 -1
  26. package/dist-server/server/harness/bus.js +4 -0
  27. package/dist-server/server/harness/registry.js +1 -0
  28. package/dist-server/server/index.js +869 -226
  29. package/dist-server/server/local-vm-lease.js +4 -0
  30. package/dist-server/server/mcp-http.js +337 -0
  31. package/dist-server/server/mcp-probe.js +67 -18
  32. package/dist-server/server/mcp-registry.js +153 -58
  33. package/dist-server/server/member-turn.js +13 -0
  34. package/dist-server/server/message-db.js +74 -8
  35. package/dist-server/server/notify.js +8 -3
  36. package/dist-server/server/recall-disclosure.js +4 -0
  37. package/dist-server/server/recent-work.js +142 -0
  38. package/dist-server/server/request-auth.js +3 -1
  39. package/dist-server/server/routines.js +36 -1
  40. package/dist-server/server/screen-frame-gate.js +4 -94
  41. package/dist-server/server/sessions.js +22 -3
  42. package/dist-server/server/store.js +16 -1
  43. package/dist-server/server/surface.js +41 -19
  44. package/dist-server/server/team-setup-requests.js +1 -1
  45. package/dist-server/server/thread-log-rotation.js +104 -0
  46. package/dist-server/server/tts/fish.js +155 -0
  47. package/dist-server/server/tts/index.js +25 -3
  48. package/dist-server/server/turn-resources.js +7 -2
  49. package/dist-server/server/vps-computer.js +26 -4
  50. package/dist-server/server/vps-ssh.js +88 -0
  51. package/dist-server/server/workspace-backup.js +11 -0
  52. package/dist-server/server/workspace.js +2 -1
  53. package/dist-server/shared/credential-request.js +10 -0
  54. package/dist-server/shared/tool-surface.js +62 -0
  55. package/dist-server/vps-container-mcp.js +41 -10
  56. package/package.json +1 -1
  57. package/dist/assets/index-Cu8BkIfo.js +0 -305
  58. package/dist/assets/index-tfpeJAIG.css +0 -1
@@ -17569,6 +17569,9 @@ var init_image_generation = __esm({
17569
17569
  });
17570
17570
 
17571
17571
  // server/contracts.ts
17572
+ function isModelVariant(value) {
17573
+ return typeof value === "string" && value.length > 0 && value.length <= 256 && value.trim() === value && !new RegExp("\\p{Cc}", "u").test(value);
17574
+ }
17572
17575
  var EFFORT_LEVELS, eventCounter, newEventId, newId;
17573
17576
  var init_contracts = __esm({
17574
17577
  "server/contracts.ts"() {
@@ -17584,21 +17587,31 @@ var init_contracts = __esm({
17584
17587
  function isHarnessOwnedMcpEnvName(name) {
17585
17588
  return name === "ELECTRON_RUN_AS_NODE" || name.startsWith("OMB_") || name.startsWith("OGB_");
17586
17589
  }
17587
- var MAX_ARGS, storedEntrySchema, mutationEntrySchema;
17590
+ var MAX_ARGS, stdioEntrySchema, stdioMutationSchema, remoteEntrySchema, remoteMutationSchema;
17588
17591
  var init_mcp_registry = __esm({
17589
17592
  "server/mcp-registry.ts"() {
17590
17593
  "use strict";
17591
17594
  init_zod();
17592
17595
  MAX_ARGS = 64;
17593
- storedEntrySchema = external_exports.object({
17596
+ stdioEntrySchema = external_exports.object({
17594
17597
  command: external_exports.string().trim().min(1).max(1024),
17595
17598
  args: external_exports.array(external_exports.string().max(4096)).max(MAX_ARGS).optional(),
17596
17599
  env: external_exports.record(external_exports.string(), external_exports.string().max(16384)).optional(),
17597
17600
  enabled: external_exports.boolean().optional()
17598
17601
  }).strict();
17599
- mutationEntrySchema = storedEntrySchema.extend({
17602
+ stdioMutationSchema = stdioEntrySchema.extend({
17600
17603
  env: external_exports.record(external_exports.string(), external_exports.union([external_exports.string().max(16384), external_exports.literal(true)])).optional()
17601
17604
  });
17605
+ remoteEntrySchema = external_exports.object({
17606
+ /** Streamable HTTP unless the entry says the older SSE transport. */
17607
+ type: external_exports.enum(["http", "sse"]).optional(),
17608
+ url: external_exports.string().trim().min(1).max(2048),
17609
+ headers: external_exports.record(external_exports.string(), external_exports.string().max(16384)).optional(),
17610
+ enabled: external_exports.boolean().optional()
17611
+ }).strict();
17612
+ remoteMutationSchema = remoteEntrySchema.extend({
17613
+ headers: external_exports.record(external_exports.string(), external_exports.union([external_exports.string().max(16384), external_exports.literal(true)])).optional()
17614
+ });
17602
17615
  }
17603
17616
  });
17604
17617
 
@@ -17775,6 +17788,7 @@ function loadConfig() {
17775
17788
  if (process.env.OPENCODE_API_KEY !== void 0) cfg.opencodeGo.apiKey = process.env.OPENCODE_API_KEY;
17776
17789
  cfg.tts = { ...cfg.tts };
17777
17790
  if (process.env.OMB_TTS_KEY !== void 0) cfg.tts.key = process.env.OMB_TTS_KEY;
17791
+ if (process.env.OMB_FISH_AUDIO_API_KEY !== void 0) cfg.tts.fishKey = process.env.OMB_FISH_AUDIO_API_KEY;
17778
17792
  cfg.imageGen = { ...cfg.imageGen };
17779
17793
  if (process.env.OMB_OPENAI_IMAGE_KEY !== void 0) cfg.imageGen.key = process.env.OMB_OPENAI_IMAGE_KEY;
17780
17794
  if (process.env.OMB_CUSTOM_IMAGE_KEY !== void 0) cfg.imageGen.customApiKey = process.env.OMB_CUSTOM_IMAGE_KEY;
@@ -17923,7 +17937,7 @@ function instanceConfigs(cfg) {
17923
17937
  }
17924
17938
  return map3;
17925
17939
  }
17926
- var optionalText, SSH_ALIAS, LEGACY_BROWSER_PROFILE_ID, BROWSER_PROFILE_ID, MIN_ROOM_TURN_TIMEOUT_MINUTES, MAX_ROOM_TURN_TIMEOUT_MINUTES, MAX_CONCURRENT_BOT_THREADS, MIN_LOCAL_VM_MAX_INSTANCES, MAX_LOCAL_VM_MAX_INSTANCES, vpsConfigSchema, roomConfigSchema, localVmConfigSchema, browserProfileSchema, legacyBrowserProfileSchema, legacyBrowserProfilesSchema, storedBrowserProfilesSchema, browserProfilesSchema, featureConfigSchema, onboardingConfigSchema, instanceConfigSchema, instanceConfigMapSchema, defaultModelSelectionSchema, appConfigSchema, storedAppConfigSchema, appConfigPatchSchema, jsonObjectSchema, DATA_DIR, LEGACY_DATA_DIR, EVENTS_DIR, NATIVE_DIR, WORKSPACE_CREDENTIAL_ENV, PROVIDER_CREDENTIAL_ENV;
17940
+ var optionalText, SSH_ALIAS, LEGACY_BROWSER_PROFILE_ID, BROWSER_PROFILE_ID, MIN_ROOM_TURN_TIMEOUT_MINUTES, MAX_ROOM_TURN_TIMEOUT_MINUTES, MAX_CONCURRENT_BOT_THREADS, MIN_THREAD_EVENT_LOG_BYTES, MAX_THREAD_EVENT_LOG_BYTES, MIN_LOCAL_VM_MAX_INSTANCES, MAX_LOCAL_VM_MAX_INSTANCES, vpsConfigSchema, roomConfigSchema, localVmConfigSchema, browserProfileSchema, legacyBrowserProfileSchema, legacyBrowserProfilesSchema, storedBrowserProfilesSchema, browserProfilesSchema, featureConfigSchema, onboardingConfigSchema, instanceConfigSchema, instanceConfigMapSchema, defaultModelSelectionSchema, appConfigSchema, storedAppConfigSchema, appConfigPatchSchema, jsonObjectSchema, DATA_DIR, LEGACY_DATA_DIR, EVENTS_DIR, NATIVE_DIR, WORKSPACE_CREDENTIAL_ENV, PROVIDER_CREDENTIAL_ENV;
17927
17941
  var init_config = __esm({
17928
17942
  "server/config.ts"() {
17929
17943
  "use strict";
@@ -17940,6 +17954,8 @@ var init_config = __esm({
17940
17954
  MIN_ROOM_TURN_TIMEOUT_MINUTES = 1;
17941
17955
  MAX_ROOM_TURN_TIMEOUT_MINUTES = 1440;
17942
17956
  MAX_CONCURRENT_BOT_THREADS = 10;
17957
+ MIN_THREAD_EVENT_LOG_BYTES = 256 * 1024;
17958
+ MAX_THREAD_EVENT_LOG_BYTES = 4 * 1024 * 1024 * 1024;
17943
17959
  MIN_LOCAL_VM_MAX_INSTANCES = 1;
17944
17960
  MAX_LOCAL_VM_MAX_INSTANCES = 4;
17945
17961
  vpsConfigSchema = external_exports.object({
@@ -17999,7 +18015,11 @@ var init_config = __esm({
17999
18015
  /** Opt-in computer sharing (a desktop lending folders, a terminal or
18000
18016
  * computer control to a workspace). Off until explicitly enabled; there is
18001
18017
  * no Settings toggle — see sharedComputersEnabled. */
18002
- sharedComputers: external_exports.boolean().optional()
18018
+ sharedComputers: external_exports.boolean().optional(),
18019
+ /** Claude bots also load the MCP servers and connectors from this
18020
+ * machine's own Claude Code setup (Plugins → MCP servers switch). Off by
18021
+ * default: each extra tool costs tokens on every model call. */
18022
+ claudeUserMcp: external_exports.boolean().optional()
18003
18023
  });
18004
18024
  onboardingConfigSchema = external_exports.object({
18005
18025
  /** ISO timestamp of finishing (or skipping to the end of) the welcome flow. */
@@ -18021,8 +18041,12 @@ var init_config = __esm({
18021
18041
  defaultModelSelectionSchema = external_exports.object({
18022
18042
  instanceId: external_exports.string().trim().min(1),
18023
18043
  model: external_exports.string().trim().min(1),
18024
- effort: external_exports.enum(EFFORT_LEVELS).optional()
18025
- });
18044
+ effort: external_exports.enum(EFFORT_LEVELS).optional(),
18045
+ variant: external_exports.string().refine(isModelVariant, "invalid model variant").optional()
18046
+ }).refine(
18047
+ (selection) => selection.variant === void 0 || selection.effort === void 0,
18048
+ "choose either a model variant or an effort level"
18049
+ );
18026
18050
  appConfigSchema = external_exports.object({
18027
18051
  /** Verified by the dedicated domain endpoint, never a generic config patch. */
18028
18052
  customDomain: external_exports.string().optional(),
@@ -18071,13 +18095,16 @@ var init_config = __esm({
18071
18095
  /** Optional OpenCode key; persisted write-only and passed only to its child. */
18072
18096
  opencodeGo: external_exports.object({ apiKey: optionalText }).optional(),
18073
18097
  /** Voice settings and the selected voice id. `provider` picks the
18074
- * engine: "elevenlabs" (default; needs a key), "system" (the Mac's
18075
- * built-in voices, no key), or "chatterbox" (a local OpenAI-compatible
18076
- * Chatterbox server; `baseUrl` and `model` are settings, not secrets). */
18098
+ * engine: "elevenlabs" (default; needs `key`), "fish" (needs its own
18099
+ * `fishKey`), "system" (the Mac's built-in voices, no key), or
18100
+ * "chatterbox" (a local OpenAI-compatible Chatterbox server; `baseUrl`
18101
+ * and `model` are settings, not secrets). Cloud keys stay separate so
18102
+ * switching providers never overwrites or misuses the other key. */
18077
18103
  tts: external_exports.object({
18078
18104
  key: optionalText,
18105
+ fishKey: optionalText,
18079
18106
  voice: optionalText,
18080
- provider: external_exports.enum(["elevenlabs", "system", "chatterbox"]).optional(),
18107
+ provider: external_exports.enum(["elevenlabs", "fish", "system", "chatterbox"]).optional(),
18081
18108
  baseUrl: external_exports.string().trim().max(2048).refine((value) => !value || /^https?:\/\//i.test(value), "the Chatterbox server address must start with http:// or https://").optional(),
18082
18109
  model: optionalText
18083
18110
  }).optional(),
@@ -18106,7 +18133,12 @@ var init_config = __esm({
18106
18133
  * system language. Unknown tags degrade to English in the renderer. */
18107
18134
  language: optionalText,
18108
18135
  rooms: roomConfigSchema.optional(),
18109
- threads: external_exports.object({ maxConcurrentPerBot: external_exports.number().int().min(1).max(MAX_CONCURRENT_BOT_THREADS) }).strict().optional(),
18136
+ threads: external_exports.object({
18137
+ maxConcurrentPerBot: external_exports.number().int().min(1).max(MAX_CONCURRENT_BOT_THREADS),
18138
+ /** Cap each per-thread events/ and native/ NDJSON log at this many
18139
+ * bytes; absent (the default) keeps today's unbounded growth (#1280). */
18140
+ eventLogMaxBytes: external_exports.number().int().min(MIN_THREAD_EVENT_LOG_BYTES).max(MAX_THREAD_EVENT_LOG_BYTES).optional()
18141
+ }).strict().optional(),
18110
18142
  localVm: localVmConfigSchema.optional(),
18111
18143
  features: featureConfigSchema.optional(),
18112
18144
  onboarding: onboardingConfigSchema.optional(),
@@ -18136,6 +18168,7 @@ var init_config = __esm({
18136
18168
  "BOX_TOKEN",
18137
18169
  "OPENCODE_API_KEY",
18138
18170
  "OMB_TTS_KEY",
18171
+ "OMB_FISH_AUDIO_API_KEY",
18139
18172
  "OMB_OPENAI_IMAGE_KEY",
18140
18173
  "OMB_CUSTOM_IMAGE_KEY",
18141
18174
  "COMPOSIO_API_KEY",
@@ -19212,6 +19245,7 @@ var init_registry = __esm({
19212
19245
  browserMcp: inst.adapter.capabilities.browserMcp === true,
19213
19246
  images: inst.adapter.capabilities.images === true,
19214
19247
  effortLevels: inst.adapter.capabilities.effortLevels,
19248
+ modelVariants: inst.adapter.capabilities.modelVariants === true,
19215
19249
  queueing: inst.adapter.capabilities.queueing === true,
19216
19250
  localComputerMcp: inst.adapter.capabilities.localComputerMcp === true,
19217
19251
  approvalReview: inst.reviewPermission !== void 0
@@ -19634,16 +19668,84 @@ var init_redact = __esm({
19634
19668
  }
19635
19669
  });
19636
19670
 
19671
+ // server/thread-log-rotation.ts
19672
+ import { closeSync as closeSync4, fstatSync, openSync as openSync4, readSync as readSync2, renameSync as renameSync5, rmSync as rmSync4, statSync as statSync3, writeSync } from "node:fs";
19673
+ function currentThreadLogCap() {
19674
+ try {
19675
+ return capProvider();
19676
+ } catch {
19677
+ return null;
19678
+ }
19679
+ }
19680
+ function capThreadLog(file2, maxBytes) {
19681
+ if (typeof maxBytes !== "number" || !Number.isFinite(maxBytes) || maxBytes <= 0) return;
19682
+ let size;
19683
+ try {
19684
+ size = statSync3(file2).size;
19685
+ } catch {
19686
+ return;
19687
+ }
19688
+ if (size <= maxBytes) return;
19689
+ trimToTail(file2, Math.max(1, Math.floor(maxBytes / 2)));
19690
+ }
19691
+ function trimToTail(file2, keepBytes) {
19692
+ const staging = `${file2}.trim`;
19693
+ try {
19694
+ const source = openSync4(file2, "r");
19695
+ try {
19696
+ const size = fstatSync(source).size;
19697
+ const start = Math.max(0, size - keepBytes);
19698
+ const window = Buffer.allocUnsafe(size - start);
19699
+ let filled = 0;
19700
+ while (filled < window.length) {
19701
+ const count = readSync2(source, window, filled, window.length - filled, start + filled);
19702
+ if (count === 0) return;
19703
+ filled += count;
19704
+ }
19705
+ const firstNewline = window.indexOf(10);
19706
+ if (firstNewline === -1 || firstNewline + 1 >= window.length) return;
19707
+ const target = openSync4(staging, "w", 384);
19708
+ try {
19709
+ let written = firstNewline + 1;
19710
+ while (written < window.length) {
19711
+ const count = writeSync(target, window, written, window.length - written);
19712
+ if (count === 0) throw new Error(`no progress writing ${staging}`);
19713
+ written += count;
19714
+ }
19715
+ } finally {
19716
+ closeSync4(target);
19717
+ }
19718
+ } finally {
19719
+ closeSync4(source);
19720
+ }
19721
+ renameSync5(staging, file2);
19722
+ } catch {
19723
+ try {
19724
+ rmSync4(staging, { force: true });
19725
+ } catch {
19726
+ }
19727
+ }
19728
+ }
19729
+ var capProvider;
19730
+ var init_thread_log_rotation = __esm({
19731
+ "server/thread-log-rotation.ts"() {
19732
+ "use strict";
19733
+ capProvider = () => null;
19734
+ }
19735
+ });
19736
+
19637
19737
  // server/drivers/native.ts
19638
19738
  import { appendFileSync as appendFileSync3 } from "node:fs";
19639
19739
  import { join as join12 } from "node:path";
19640
19740
  function appendNative(threadId, entry) {
19741
+ const file2 = join12(NATIVE_DIR, `${threadId}.ndjson`);
19641
19742
  try {
19642
19743
  appendFileSync3(
19643
- join12(NATIVE_DIR, `${threadId}.ndjson`),
19744
+ file2,
19644
19745
  JSON.stringify({ at: (/* @__PURE__ */ new Date()).toISOString(), ...entry, msg: redactSecrets(entry.msg) }) + "\n",
19645
19746
  { mode: 384 }
19646
19747
  );
19748
+ capThreadLog(file2, currentThreadLogCap());
19647
19749
  } catch {
19648
19750
  }
19649
19751
  }
@@ -19652,6 +19754,7 @@ var init_native = __esm({
19652
19754
  "use strict";
19653
19755
  init_config();
19654
19756
  init_redact();
19757
+ init_thread_log_rotation();
19655
19758
  }
19656
19759
  });
19657
19760
 
@@ -19727,6 +19830,29 @@ import { basename as basename3, dirname as dirname5, isAbsolute, relative, resol
19727
19830
  function skipSubscriptionAuthForLocalInject(model) {
19728
19831
  return Boolean(decodeInjectId(model));
19729
19832
  }
19833
+ function acpVariantOption(result) {
19834
+ const option = (Array.isArray(result?.configOptions) ? result.configOptions : []).find(
19835
+ (entry) => entry?.type === "select" && typeof entry.id === "string" && (entry.id === "effort" || entry.category === "thought_level")
19836
+ );
19837
+ if (!option) return;
19838
+ const options = [];
19839
+ const seen = /* @__PURE__ */ new Set();
19840
+ const collect = (entries) => {
19841
+ if (!Array.isArray(entries)) return;
19842
+ for (const entry of entries) {
19843
+ if (typeof entry?.value === "string" && !seen.has(entry.value)) {
19844
+ seen.add(entry.value);
19845
+ options.push({ id: entry.value, label: typeof entry.name === "string" ? entry.name : entry.value });
19846
+ } else if (Array.isArray(entry?.options)) collect(entry.options);
19847
+ }
19848
+ };
19849
+ collect(option.options);
19850
+ return {
19851
+ configId: option.id,
19852
+ options,
19853
+ ...typeof option.currentValue === "string" ? { currentValue: option.currentValue } : {}
19854
+ };
19855
+ }
19730
19856
  async function readAcpImageBlocks(images) {
19731
19857
  return Promise.all(images.map(async (image) => ({
19732
19858
  type: "image",
@@ -19902,6 +20028,10 @@ function createAcpDriver(support11) {
19902
20028
  }
19903
20029
  for (const [name, server] of Object.entries(turn.integrations?.custom ?? {})) {
19904
20030
  if (servers.some((existing) => existing.name === name)) continue;
20031
+ if ("url" in server) {
20032
+ servers.push({ type: server.type, name, url: server.url, headers: acpEnv(server.headers) });
20033
+ continue;
20034
+ }
19905
20035
  servers.push({ name, command: server.command, args: server.args, env: acpEnv(server.env) });
19906
20036
  }
19907
20037
  return servers;
@@ -19924,7 +20054,7 @@ function createAcpDriver(support11) {
19924
20054
  return { turnId };
19925
20055
  }
19926
20056
  const resolvedModel = support11.resolveTurnModel?.(turn.model, env);
19927
- support11.applyTurnEnv?.(env, { model: resolvedModel, requestedModel: turn.model });
20057
+ support11.applyTurnEnv?.(env, { model: resolvedModel, requestedModel: turn.model, fullAuto: turnConfig.fullAuto === true });
19928
20058
  const cliTurn = resolvedModel !== void 0 && resolvedModel !== turn.model ? { ...turn, model: resolvedModel } : turn;
19929
20059
  const mcpServers = acpMcpServers(turn);
19930
20060
  let launch;
@@ -19950,6 +20080,37 @@ function createAcpDriver(support11) {
19950
20080
  const asks = /* @__PURE__ */ new Map();
19951
20081
  let nextId = 1;
19952
20082
  let sessionId = null;
20083
+ let sessionConfigResult = null;
20084
+ const modelOf = (result) => {
20085
+ const option = (Array.isArray(result?.configOptions) ? result.configOptions : []).find(
20086
+ (entry) => entry?.id === (support11.selectModel?.configId ?? "model")
20087
+ );
20088
+ return typeof option?.currentValue === "string" ? option.currentValue : null;
20089
+ };
20090
+ const receiveModelVariants = (result) => {
20091
+ sessionConfigResult = result;
20092
+ if (!support11.modelVariants) return;
20093
+ const nativeModel = modelOf(result) ?? cliTurn.model;
20094
+ if (!nativeModel) return;
20095
+ const option = acpVariantOption(result);
20096
+ emit({
20097
+ ...base2(threadId, turnId),
20098
+ type: "session.model-variants",
20099
+ model: nativeModel === cliTurn.model ? turn.model ?? nativeModel : nativeModel,
20100
+ variants: {
20101
+ options: option?.options ?? [],
20102
+ ...option?.currentValue !== void 0 ? { currentValue: option.currentValue } : {}
20103
+ }
20104
+ });
20105
+ };
20106
+ const requestedVariantOption = () => {
20107
+ if (!support11.modelVariants) throw new Error(`${support11.displayName} does not support model variants`);
20108
+ const option = acpVariantOption(sessionConfigResult);
20109
+ if (!option || !option.options.some((entry) => entry.id === turn.variant)) {
20110
+ throw new Error(`${support11.displayName} does not advertise variant ${turn.variant} for this model`);
20111
+ }
20112
+ return option;
20113
+ };
19953
20114
  let interruptTimer = null;
19954
20115
  const rpcPending = /* @__PURE__ */ new Map();
19955
20116
  const send = (obj) => {
@@ -19959,7 +20120,7 @@ function createAcpDriver(support11) {
19959
20120
  }
19960
20121
  appendNative(threadId, { dir: "out", source: SOURCE, msg: nativeLogMessage(obj) });
19961
20122
  };
19962
- const request = (method, params, timeoutMs) => new Promise((resolve12, reject) => {
20123
+ const request = (method, params, timeoutMs, receive) => new Promise((resolve12, reject) => {
19963
20124
  const id = nextId++;
19964
20125
  let timer = null;
19965
20126
  if (timeoutMs) {
@@ -19969,7 +20130,16 @@ function createAcpDriver(support11) {
19969
20130
  }, timeoutMs);
19970
20131
  timer.unref?.();
19971
20132
  }
19972
- rpcPending.set(id, { resolve: resolve12, reject, timer });
20133
+ rpcPending.set(id, {
20134
+ // Consume configuration in wire order: an update following this
20135
+ // response may arrive before the awaiting continuation resumes.
20136
+ resolve: (result) => {
20137
+ receive?.(result);
20138
+ resolve12(result);
20139
+ },
20140
+ reject,
20141
+ timer
20142
+ });
19973
20143
  send({ jsonrpc: "2.0", id, method, params });
19974
20144
  });
19975
20145
  const stop = () => killCliTree(child);
@@ -20152,7 +20322,12 @@ function createAcpDriver(support11) {
20152
20322
  const handleNotification = (msg) => {
20153
20323
  if (msg.method !== "session/update") return;
20154
20324
  const p = msg.params ?? {};
20155
- if (!state.promptSent || p._meta?.isReplay === true) return;
20325
+ if (p._meta?.isReplay === true) return;
20326
+ if (support11.modelVariants && p.update?.sessionUpdate === "config_option_update") {
20327
+ if (!state.settled && sessionId && p.sessionId === sessionId) receiveModelVariants(p.update);
20328
+ return;
20329
+ }
20330
+ if (!state.promptSent) return;
20156
20331
  const u4 = p.update ?? {};
20157
20332
  switch (u4.sessionUpdate) {
20158
20333
  case "agent_message_chunk": {
@@ -20309,6 +20484,7 @@ function createAcpDriver(support11) {
20309
20484
  `${support11.displayName} is configured for image attachments, but this installed runtime does not advertise ACP image input. Update the ${support11.displayName} CLI or send the message without an image.`
20310
20485
  );
20311
20486
  }
20487
+ const sessionServers = mcpServers.filter((server) => !("type" in server) || init?.agentCapabilities?.mcpCapabilities?.[server.type] === true);
20312
20488
  const cursor3 = typeof turn.resumeCursor === "string" ? turn.resumeCursor : null;
20313
20489
  if (!cursor3) sessionAllows.delete(threadId);
20314
20490
  let sessionResult = null;
@@ -20316,16 +20492,23 @@ function createAcpDriver(support11) {
20316
20492
  try {
20317
20493
  sessionResult = await request(
20318
20494
  support11.resumeMethod === "resume" ? "session/resume" : "session/load",
20319
- { sessionId: cursor3, cwd, mcpServers },
20320
- LOAD_SESSION_TIMEOUT
20495
+ { sessionId: cursor3, cwd, mcpServers: sessionServers },
20496
+ LOAD_SESSION_TIMEOUT,
20497
+ (result2) => {
20498
+ if (result2) {
20499
+ sessionId = cursor3;
20500
+ receiveModelVariants(result2);
20501
+ }
20502
+ }
20321
20503
  );
20322
- if (sessionResult) sessionId = cursor3;
20323
20504
  } catch {
20324
20505
  }
20325
20506
  }
20326
20507
  if (!sessionId) {
20327
- sessionResult = await request("session/new", { cwd, mcpServers }, NEW_SESSION_TIMEOUT);
20328
- sessionId = typeof sessionResult?.sessionId === "string" ? sessionResult.sessionId : null;
20508
+ sessionResult = await request("session/new", { cwd, mcpServers: sessionServers }, NEW_SESSION_TIMEOUT, (result2) => {
20509
+ sessionId = typeof result2?.sessionId === "string" ? result2.sessionId : null;
20510
+ receiveModelVariants(result2);
20511
+ });
20329
20512
  if (!sessionId) throw new Error("session/new returned no sessionId");
20330
20513
  }
20331
20514
  let selectedModel = null;
@@ -20343,16 +20526,15 @@ function createAcpDriver(support11) {
20343
20526
  try {
20344
20527
  if (support11.selectModel) {
20345
20528
  const { configId } = support11.selectModel;
20346
- const currentOf = (r2) => (Array.isArray(r2?.configOptions) ? r2.configOptions : []).find((o) => o?.id === configId)?.currentValue ?? null;
20347
- selectedModel = currentOf(sessionResult);
20529
+ selectedModel = modelOf(sessionConfigResult);
20348
20530
  if (cliTurn.model && cliTurn.model !== selectedModel) {
20349
- selectedModel = currentOf(
20350
- await request(
20351
- "session/set_config_option",
20352
- { sessionId, configId, value: cliTurn.model },
20353
- INIT_TIMEOUT
20354
- )
20531
+ sessionResult = await request(
20532
+ "session/set_config_option",
20533
+ { sessionId, configId, value: cliTurn.model },
20534
+ INIT_TIMEOUT,
20535
+ receiveModelVariants
20355
20536
  );
20537
+ selectedModel = modelOf(sessionConfigResult);
20356
20538
  if (selectedModel !== cliTurn.model) {
20357
20539
  throw new Error(
20358
20540
  `${DRIVER_KIND8} did not switch to ${cliTurn.model} (still ${selectedModel ?? "unknown"})`
@@ -20370,16 +20552,34 @@ function createAcpDriver(support11) {
20370
20552
  });
20371
20553
  if (!selectedModel && cliTurn.model) selectedModel = cliTurn.model;
20372
20554
  }
20555
+ if (turn.variant !== void 0) {
20556
+ const option = requestedVariantOption();
20557
+ await request(
20558
+ "session/set_config_option",
20559
+ { sessionId, configId: option.configId, value: turn.variant },
20560
+ SESSION_CONFIG_TIMEOUT,
20561
+ receiveModelVariants
20562
+ );
20563
+ if (requestedVariantOption().currentValue !== turn.variant) {
20564
+ throw new Error(`${support11.displayName} did not apply variant ${turn.variant}`);
20565
+ }
20566
+ }
20373
20567
  } catch (error51) {
20374
20568
  emitSessionStarted();
20375
20569
  throw error51;
20376
20570
  }
20377
20571
  emitSessionStarted();
20378
- state.promptSent = true;
20379
20572
  const text4 = support11.buildPromptText ? support11.buildPromptText(turn) : turn.system ? `${turn.system}
20380
20573
 
20381
20574
  ${turn.text}` : turn.text;
20382
20575
  const imageBlocks = support11.images === true && runtimeAcceptsImages ? await readAcpImageBlocks(images) : [];
20576
+ if (support11.modelVariants && cliTurn.model && modelOf(sessionConfigResult) !== cliTurn.model) {
20577
+ throw new Error(`${support11.displayName} changed model before the prompt`);
20578
+ }
20579
+ if (turn.variant !== void 0 && requestedVariantOption().currentValue !== turn.variant) {
20580
+ throw new Error(`${support11.displayName} changed variant before the prompt`);
20581
+ }
20582
+ state.promptSent = true;
20383
20583
  const result = await request("session/prompt", {
20384
20584
  sessionId,
20385
20585
  prompt: [{ type: "text", text: text4 }, ...imageBlocks]
@@ -20458,6 +20658,7 @@ ${turn.text}` : turn.text;
20458
20658
  images: support11.images !== false,
20459
20659
  nativeImageInput: support11.images === true,
20460
20660
  effortLevels: support11.effortLevels,
20661
+ modelVariants: support11.modelVariants === true,
20461
20662
  // OpenMausBot supplies a per-bot approvalMode on every harness
20462
20663
  // turn, which safely overrides a legacy instance fullAuto value.
20463
20664
  // Direct adapter calls that omit it still fail closed in sendTurn.
@@ -22515,7 +22716,7 @@ var init_ask_question = __esm({
22515
22716
 
22516
22717
  // server/drivers/claude.ts
22517
22718
  import { createHash as createHash5, randomBytes as randomBytes2 } from "node:crypto";
22518
- import { chmodSync as chmodSync4, mkdtempSync as mkdtempSync2, readFileSync as readFileSync7, rmSync as rmSync4, unlinkSync as unlinkSync3, writeFileSync as writeFileSync5 } from "node:fs";
22719
+ import { chmodSync as chmodSync4, mkdtempSync as mkdtempSync2, readFileSync as readFileSync7, rmSync as rmSync5, unlinkSync as unlinkSync3, writeFileSync as writeFileSync5 } from "node:fs";
22519
22720
  import { createServer as createNetServer } from "node:net";
22520
22721
  import { homedir as homedir9, tmpdir as tmpdir3 } from "node:os";
22521
22722
  import { join as join18, dirname as dirname9, isAbsolute as isAbsolute4, normalize } from "node:path";
@@ -22579,6 +22780,13 @@ function claudeEnvironment(model, source = process.env, configDir, instanceEnvir
22579
22780
  function inheritsUserConfig(env) {
22580
22781
  return env.OMB_CLAUDE_INHERIT_USER_CONFIG === "1";
22581
22782
  }
22783
+ function claudeInheritWarning(env) {
22784
+ if (!inheritsUserConfig(env)) return void 0;
22785
+ return {
22786
+ title: "Bots inherit this machine's Claude Code setup",
22787
+ message: "OMB_CLAUDE_INHERIT_USER_CONFIG=1 is set on the OpenMausBot process, so every Claude bot also loads this computer's own MCP servers, connectors, skills, hooks and personal CLAUDE.md on every turn \u2014 often thousands of extra tokens per model call, and tools nobody gave the bot. Unless a bot genuinely needs a server from your user-scope Claude config, remove the variable and restart; add the server under Settings \u2192 MCP servers or the bot project's .mcp.json instead."
22788
+ };
22789
+ }
22582
22790
  function readClaudeAuthSettings(env, instanceEnvironment = {}) {
22583
22791
  if (CLAUDE_ACCOUNT_ENV_KEYS.some((key) => instanceEnvironment[key])) return {};
22584
22792
  try {
@@ -22697,7 +22905,7 @@ function readClaudeModelCatalog(env = process.env) {
22697
22905
  function removePrivateTempDir(filePath) {
22698
22906
  if (!filePath) return true;
22699
22907
  try {
22700
- rmSync4(dirname9(filePath), { recursive: true, force: true, maxRetries: 3, retryDelay: 50 });
22908
+ rmSync5(dirname9(filePath), { recursive: true, force: true, maxRetries: 3, retryDelay: 50 });
22701
22909
  return true;
22702
22910
  } catch {
22703
22911
  return false;
@@ -23048,6 +23256,9 @@ var init_claude = __esm({
23048
23256
  const { instanceId, config: config2 } = input;
23049
23257
  const environment = (model) => claudeEnvironment(config2.managed ? void 0 : model, { ...process.env, ...input.environment }, config2.configDir, input.environment);
23050
23258
  const catalogEnv = environment();
23259
+ if (inheritsUserConfig(catalogEnv)) {
23260
+ console.error(`claude (${instanceId}): OMB_CLAUDE_INHERIT_USER_CONFIG=1 \u2014 bots inherit this machine's Claude Code MCP servers, skills, hooks and CLAUDE.md on every turn; remove it unless a bot needs a user-scope server`);
23261
+ }
23051
23262
  let models2 = STATIC_CLAUDE_MODELS;
23052
23263
  const refreshModels = async () => {
23053
23264
  if (config2.managed) return;
@@ -23182,7 +23393,7 @@ var init_claude = __esm({
23182
23393
  }
23183
23394
  const isolated = !inheritsUserConfig(turnEnvironment);
23184
23395
  if (isolated) {
23185
- if (claudeCliSupports(cliVersion, "--strict-mcp-config")) args.push("--strict-mcp-config");
23396
+ if (!turn.mcpFromUserConfig && claudeCliSupports(cliVersion, "--strict-mcp-config")) args.push("--strict-mcp-config");
23186
23397
  if (claudeCliSupports(cliVersion, "--setting-sources")) args.push("--setting-sources", "project");
23187
23398
  }
23188
23399
  const compactWindow = autoCompactWindow(turnEnvironment);
@@ -23312,7 +23523,7 @@ var init_claude = __esm({
23312
23523
  retryState.delete(threadId);
23313
23524
  if (mcpConfigPath) {
23314
23525
  try {
23315
- rmSync4(dirname9(mcpConfigPath), { recursive: true, force: true });
23526
+ rmSync5(dirname9(mcpConfigPath), { recursive: true, force: true });
23316
23527
  } catch {
23317
23528
  }
23318
23529
  }
@@ -23320,7 +23531,7 @@ var init_claude = __esm({
23320
23531
  }
23321
23532
  if (mcpConfigPath) {
23322
23533
  try {
23323
- rmSync4(dirname9(mcpConfigPath), { recursive: true, force: true });
23534
+ rmSync5(dirname9(mcpConfigPath), { recursive: true, force: true });
23324
23535
  } catch {
23325
23536
  }
23326
23537
  }
@@ -23332,7 +23543,7 @@ var init_claude = __esm({
23332
23543
  broker = void 0;
23333
23544
  if (mcpConfigPath) {
23334
23545
  try {
23335
- rmSync4(dirname9(mcpConfigPath), { recursive: true, force: true });
23546
+ rmSync5(dirname9(mcpConfigPath), { recursive: true, force: true });
23336
23547
  } catch {
23337
23548
  }
23338
23549
  mcpConfigPath = null;
@@ -23447,7 +23658,7 @@ var init_claude = __esm({
23447
23658
  session.broker?.pause();
23448
23659
  if (session.mcpConfigPath) {
23449
23660
  try {
23450
- rmSync4(dirname9(session.mcpConfigPath), { recursive: true, force: true });
23661
+ rmSync5(dirname9(session.mcpConfigPath), { recursive: true, force: true });
23451
23662
  } catch {
23452
23663
  }
23453
23664
  session.mcpConfigPath = null;
@@ -23601,7 +23812,7 @@ var init_claude = __esm({
23601
23812
  failedBroker?.close();
23602
23813
  if (session.mcpConfigPath) {
23603
23814
  try {
23604
- rmSync4(dirname9(session.mcpConfigPath), { recursive: true, force: true });
23815
+ rmSync5(dirname9(session.mcpConfigPath), { recursive: true, force: true });
23605
23816
  } catch {
23606
23817
  }
23607
23818
  session.mcpConfigPath = null;
@@ -23679,7 +23890,7 @@ var init_claude = __esm({
23679
23890
  session.broker = void 0;
23680
23891
  if (session.mcpConfigPath) {
23681
23892
  try {
23682
- rmSync4(dirname9(session.mcpConfigPath), { recursive: true, force: true });
23893
+ rmSync5(dirname9(session.mcpConfigPath), { recursive: true, force: true });
23683
23894
  } catch {
23684
23895
  }
23685
23896
  session.mcpConfigPath = null;
@@ -23730,7 +23941,7 @@ var init_claude = __esm({
23730
23941
  session.broker?.close();
23731
23942
  if (session.mcpConfigPath) {
23732
23943
  try {
23733
- rmSync4(dirname9(session.mcpConfigPath), { recursive: true, force: true });
23944
+ rmSync5(dirname9(session.mcpConfigPath), { recursive: true, force: true });
23734
23945
  } catch {
23735
23946
  }
23736
23947
  }
@@ -23771,7 +23982,8 @@ var init_claude = __esm({
23771
23982
  cliVersionChecked = true;
23772
23983
  const auth = await claudeAuthStatus(config2.cli, env);
23773
23984
  const update = claudeCliUpdate(version2, config2.cli);
23774
- return { state: "available", version: version2, ...auth, ...update ? { update } : {}, billing: "subscription" };
23985
+ const warning = claudeInheritWarning(env);
23986
+ return { state: "available", version: version2, ...auth, ...update ? { update } : {}, ...warning ? { warning } : {}, billing: "subscription" };
23775
23987
  };
23776
23988
  const generateReview = (prompt, signal) => new Promise((resolve12, reject) => {
23777
23989
  const child = spawnCli(
@@ -24757,7 +24969,8 @@ function decodeManagedCodex(raw) {
24757
24969
  throw new Error("Invalid Company Codex configuration.");
24758
24970
  }
24759
24971
  const url2 = new URL(value.url);
24760
- if (url2.username || url2.password || url2.search || url2.hash || !["https:", "http:"].includes(url2.protocol)) throw new Error("Invalid Company Codex endpoint.");
24972
+ const loopback = url2.hostname === "localhost" || url2.hostname === "[::1]" || /^127(?:\.\d{1,3}){3}$/.test(url2.hostname);
24973
+ if (url2.username || url2.password || url2.search || url2.hash || url2.protocol !== "https:" && !(url2.protocol === "http:" && loopback)) throw new Error("Invalid Company Codex endpoint.");
24761
24974
  return { url: url2.href.replace(/\/$/, ""), models: value.models };
24762
24975
  }
24763
24976
  function managedCodexArgs(config2) {
@@ -24780,6 +24993,14 @@ function managedCodexArgs(config2) {
24780
24993
  "shell_environment_policy.ignore_default_excludes=false"
24781
24994
  ];
24782
24995
  }
24996
+ function noteSkippedSseServer(name) {
24997
+ if (skippedSseServers.has(name)) return;
24998
+ skippedSseServers.add(name);
24999
+ console.error(`codex: MCP server ${JSON.stringify(name)} uses the SSE transport, which codex does not speak \u2014 it is available to Claude bots only`);
25000
+ }
25001
+ function tomlInlineTable(entries) {
25002
+ return `{ ${Object.entries(entries).map(([key, value]) => `${JSON.stringify(key)} = ${JSON.stringify(value)}`).join(", ")} }`;
25003
+ }
24783
25004
  function grantedPermissions(raw) {
24784
25005
  if (!raw || typeof raw !== "object" || Array.isArray(raw)) return {};
24785
25006
  return Object.fromEntries(
@@ -24979,23 +25200,43 @@ function codexNativeIncomingLogMessage(message2, sensitiveResponseIds) {
24979
25200
  return message2;
24980
25201
  }
24981
25202
  function mountMcpServer(appServerArgs, env, name, server, preApproved = true) {
24982
- Object.assign(env, server.env);
24983
25203
  const prefix = `mcp_servers.${name}`;
24984
- appServerArgs.push(
24985
- "-c",
24986
- `${prefix}.command=${JSON.stringify(server.command)}`,
24987
- "-c",
24988
- `${prefix}.args=${JSON.stringify(server.args)}`,
24989
- // Values stay in the child environment; argv contains names only so
24990
- // credentials never appear in process listings or diagnostics.
24991
- "-c",
24992
- `${prefix}.env_vars=${JSON.stringify(Object.keys(server.env))}`
24993
- );
25204
+ if ("url" in server) {
25205
+ appServerArgs.push("-c", `${prefix}.url=${JSON.stringify(server.url)}`);
25206
+ const stem = `OMB_MCP_HEADER_${name.toUpperCase().replace(/[^A-Z0-9]/g, "_")}`;
25207
+ const variables = {};
25208
+ Object.entries(server.headers).forEach(([header, value], index) => {
25209
+ const bearer = header.toLowerCase() === "authorization" ? /^Bearer\s+(\S+)$/i.exec(value) : null;
25210
+ if (bearer) {
25211
+ env[`${stem}_BEARER`] = bearer[1];
25212
+ appServerArgs.push("-c", `${prefix}.bearer_token_env_var=${JSON.stringify(`${stem}_BEARER`)}`);
25213
+ return;
25214
+ }
25215
+ const variable = `${stem}_${index}`;
25216
+ env[variable] = value;
25217
+ variables[header] = variable;
25218
+ });
25219
+ if (Object.keys(variables).length) {
25220
+ appServerArgs.push("-c", `${prefix}.env_http_headers=${tomlInlineTable(variables)}`);
25221
+ }
25222
+ } else {
25223
+ Object.assign(env, server.env);
25224
+ appServerArgs.push(
25225
+ "-c",
25226
+ `${prefix}.command=${JSON.stringify(server.command)}`,
25227
+ "-c",
25228
+ `${prefix}.args=${JSON.stringify(server.args)}`,
25229
+ // Values stay in the child environment; argv contains names only so
25230
+ // credentials never appear in process listings or diagnostics.
25231
+ "-c",
25232
+ `${prefix}.env_vars=${JSON.stringify(Object.keys(server.env))}`
25233
+ );
25234
+ }
24994
25235
  if (preApproved) {
24995
25236
  appServerArgs.push("-c", `${prefix}.default_tools_approval_mode="auto"`);
24996
25237
  }
24997
25238
  }
24998
- var DRIVER_KIND3, ASTRA_MODEL_ID, ASTRA_MIN_CODEX_VERSION, CodexRpcError, QUESTION_TIMEOUT_NOTE2, DENY_TIMEOUT_NOTE2, plainRecord, containsControlCharacter, CodexDriver;
25239
+ var DRIVER_KIND3, ASTRA_MODEL_ID, ASTRA_MIN_CODEX_VERSION, CodexRpcError, QUESTION_TIMEOUT_NOTE2, DENY_TIMEOUT_NOTE2, skippedSseServers, plainRecord, containsControlCharacter, CodexDriver;
24999
25240
  var init_codex = __esm({
25000
25241
  "server/drivers/codex.ts"() {
25001
25242
  "use strict";
@@ -25026,6 +25267,7 @@ var init_codex = __esm({
25026
25267
  };
25027
25268
  QUESTION_TIMEOUT_NOTE2 = "No answer was given \u2014 use your best judgment.";
25028
25269
  DENY_TIMEOUT_NOTE2 = "OpenMausBot: nobody answered this permission request in time. Skip this action and finish what you can without it.";
25270
+ skippedSseServers = /* @__PURE__ */ new Set();
25029
25271
  plainRecord = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : null;
25030
25272
  containsControlCharacter = (value) => {
25031
25273
  for (const character of value) {
@@ -25092,8 +25334,19 @@ var init_codex = __esm({
25092
25334
  createdAt: (/* @__PURE__ */ new Date()).toISOString()
25093
25335
  });
25094
25336
  const sendTurn = async (turn) => {
25095
- if (config2.managed && (!turn.model || !config2.managed.models.includes(turn.model) || !input.environment.OPENMAUSBOT_COMPANY_API_KEY || !input.environment.CODEX_HOME)) {
25096
- throw new Error("Company model access is unavailable. Reconnect your organization; personal billing will not be used.");
25337
+ if (config2.managed) {
25338
+ if (!turn.model) {
25339
+ throw new Error("Company model access is unavailable: no model is selected. Reconnect your organization; personal billing will not be used.");
25340
+ }
25341
+ if (!config2.managed.models.includes(turn.model)) {
25342
+ throw new Error("Company model access is unavailable: " + turn.model + " is not approved for your organization. Reconnect your organization; personal billing will not be used.");
25343
+ }
25344
+ if (!input.environment.OPENMAUSBOT_COMPANY_API_KEY) {
25345
+ throw new Error("Company model access is unavailable: OPENMAUSBOT_COMPANY_API_KEY is missing. Reconnect your organization; personal billing will not be used.");
25346
+ }
25347
+ if (!input.environment.CODEX_HOME) {
25348
+ throw new Error("Company model access is unavailable: CODEX_HOME is missing. Reconnect your organization; personal billing will not be used.");
25349
+ }
25097
25350
  }
25098
25351
  let stopRequested = false;
25099
25352
  let promptSubmitted = false;
@@ -25102,6 +25355,7 @@ var init_codex = __esm({
25102
25355
  const { threadId } = turn;
25103
25356
  const approvalMode = turn.approvalMode ?? (config2.fullAuto ? "full" : "ask");
25104
25357
  for (const [name, server] of Object.entries(turn.integrations?.custom ?? {})) {
25358
+ if ("url" in server) continue;
25105
25359
  const reserved = Object.keys(server.env).find(isHarnessOwnedMcpEnvName);
25106
25360
  if (reserved) {
25107
25361
  throw new Error(`Custom MCP server \u201C${name}\u201D cannot set reserved environment variable \u201C${reserved}\u201D`);
@@ -25127,6 +25381,10 @@ var init_codex = __esm({
25127
25381
  mountMcpServer(appServerArgs, env, "browser", turn.integrations.browser);
25128
25382
  }
25129
25383
  for (const [name, server] of Object.entries(turn.integrations?.custom ?? {})) {
25384
+ if ("url" in server && server.type === "sse") {
25385
+ noteSkippedSseServer(name);
25386
+ continue;
25387
+ }
25130
25388
  mountMcpServer(appServerArgs, env, name, server, false);
25131
25389
  }
25132
25390
  if (turn.integrations?.phone) {
@@ -25271,6 +25529,18 @@ var init_codex = __esm({
25271
25529
  }
25272
25530
  return;
25273
25531
  }
25532
+ if (isQuestion && (!Array.isArray(params.questions) || params.questions.length !== 1)) {
25533
+ const bundled = Array.isArray(params.questions) && params.questions.length > 1;
25534
+ send({
25535
+ jsonrpc: "2.0",
25536
+ id: msg.id,
25537
+ error: {
25538
+ code: -32602,
25539
+ message: bundled ? `ask supports one question per call; this request bundled ${params.questions.length}. Split it into separate asks, one question each.` : Array.isArray(params.questions) ? "ask supports one question per call; this request sent none." : "ask supports one question per call; params.questions must be an array with exactly one question."
25540
+ }
25541
+ });
25542
+ return;
25543
+ }
25274
25544
  const mcpTool = isLegacyMcpPermission ? String(params.message ?? "").match(/tool "([^"]+)"/)?.[1] : void 0;
25275
25545
  const tool = mcpAppApproval ? mcpAppApproval.tool : isLegacyMcpPermission ? mcpTool ?? "mcp" : isAdditionalPermission ? "permissions" : method === "item/fileChange/requestApproval" || method === "applyPatchApproval" ? "edit" : isQuestion ? "ask_user" : "shell";
25276
25546
  const permissionResult = (allow) => isMcpPermission ? allow ? mcpAppApproval?.allowResult ?? { action: "accept", content: {} } : { action: "decline" } : isAdditionalPermission ? { permissions: allow ? grantedPermissions(params.permissions) : {}, scope: "turn" } : { decision: allow ? legacy ? "approved" : "accept" : legacy ? "denied" : "decline" };
@@ -33542,7 +33812,9 @@ async function mountChatTools(integrations, signal) {
33542
33812
  const servers = [];
33543
33813
  if (integrations?.agents) servers.push(["agents", integrations.agents]);
33544
33814
  if (integrations?.composio) servers.push(["composio", integrations.composio]);
33545
- servers.push(...Object.entries(integrations?.custom ?? {}));
33815
+ for (const [name, server] of Object.entries(integrations?.custom ?? {})) {
33816
+ if ("command" in server) servers.push([name, server]);
33817
+ }
33546
33818
  if (servers.length > 32) throw new Error("MCP server count exceeds the 32-server limit");
33547
33819
  const clients = [];
33548
33820
  let closed = false;
@@ -35767,12 +36039,14 @@ function parseOpenCodeModelsOutput(stdout2) {
35767
36039
  const name = typeof record3.name === "string" && record3.name.trim() ? record3.name.trim() : labelForModel(model);
35768
36040
  const limit = record3.limit && typeof record3.limit === "object" && !Array.isArray(record3.limit) ? record3.limit : {};
35769
36041
  const contextWindow = typeof limit.context === "number" && Number.isFinite(limit.context) && limit.context > 0 ? Math.floor(limit.context) : void 0;
36042
+ const variants = record3.variants && typeof record3.variants === "object" && !Array.isArray(record3.variants) ? Object.entries(record3.variants).filter(([, settings2]) => settings2 && typeof settings2 === "object" && !Array.isArray(settings2) && settings2.disabled !== true).map(([id]) => ({ id, label: labelForModel(id) })) : void 0;
35770
36043
  seen.add(slug);
35771
36044
  options.push({
35772
36045
  id: slug,
35773
36046
  label: `${providerLabel(provider)} \xB7 ${name}`,
35774
36047
  ...localModelRecord(record3) ? { custom: true, loaded: true } : {},
35775
- ...contextWindow ? { contextWindow } : {}
36048
+ ...contextWindow ? { contextWindow } : {},
36049
+ ...variants ? { variants } : {}
35776
36050
  });
35777
36051
  };
35778
36052
  for (const line of stdout2.split(/\r?\n/u)) {
@@ -35979,8 +36253,31 @@ var init_opencode_go = __esm({
35979
36253
  spawnArgs: () => ["acp"],
35980
36254
  credentialEnv: ["OPENCODE_API_KEY"],
35981
36255
  selectModel: { configId: "model" },
36256
+ modelVariants: true,
35982
36257
  resolveTurnModel: (model, env) => model ? ensureOpenCodeInjectModel(normalizeLegacyOpenCodeModel(model, env), env) : model,
35983
36258
  transformEnv: stripForeignProviderKeys,
36259
+ applyTurnEnv: (env, { fullAuto }) => {
36260
+ if (!fullAuto) return;
36261
+ env.OPENCODE_PERMISSION = JSON.stringify(Object.fromEntries([
36262
+ "*",
36263
+ "external_directory",
36264
+ "read",
36265
+ "edit",
36266
+ "bash",
36267
+ "glob",
36268
+ "grep",
36269
+ "list",
36270
+ "task",
36271
+ "lsp",
36272
+ "skill",
36273
+ "webfetch",
36274
+ "websearch",
36275
+ "codesearch",
36276
+ "todoread",
36277
+ "todowrite",
36278
+ "doom_loop"
36279
+ ].map((permission) => [permission, "allow"])));
36280
+ },
35984
36281
  pickAuthMethod: () => null,
35985
36282
  authFailure: "continue",
35986
36283
  isAuthenticated: async (env, config2) => Boolean(env.OPENCODE_API_KEY) || hasStoredOpenCodeAuth(env) || await canListOpenCodeModels(env, config2.cli),
@@ -43571,7 +43868,7 @@ var init_openai_compat = __esm({
43571
43868
  });
43572
43869
 
43573
43870
  // server/drivers/pi.ts
43574
- import { chmodSync as chmodSync6, existsSync as existsSync17, mkdirSync as mkdirSync15, mkdtempSync as mkdtempSync3, readFileSync as readFileSync17, rmSync as rmSync5, writeFileSync as writeFileSync12 } from "node:fs";
43871
+ import { chmodSync as chmodSync6, existsSync as existsSync17, mkdirSync as mkdirSync15, mkdtempSync as mkdtempSync3, readFileSync as readFileSync17, rmSync as rmSync6, writeFileSync as writeFileSync12 } from "node:fs";
43575
43872
  import { homedir as homedir21, tmpdir as tmpdir4 } from "node:os";
43576
43873
  import { join as join30 } from "node:path";
43577
43874
  function readPiPromptImages(turn) {
@@ -43907,7 +44204,7 @@ var init_pi = __esm({
43907
44204
  writeFileSync12(join30(mcpTempDir, "mcp.json"), JSON.stringify({ mcpServers }), { mode: 384 });
43908
44205
  } catch (err) {
43909
44206
  try {
43910
- rmSync5(mcpTempDir, { recursive: true, force: true });
44207
+ rmSync6(mcpTempDir, { recursive: true, force: true });
43911
44208
  } catch {
43912
44209
  }
43913
44210
  throw err;
@@ -43928,7 +44225,7 @@ var init_pi = __esm({
43928
44225
  } catch (err) {
43929
44226
  if (mcpTempDir) {
43930
44227
  try {
43931
- rmSync5(mcpTempDir, { recursive: true, force: true });
44228
+ rmSync6(mcpTempDir, { recursive: true, force: true });
43932
44229
  } catch {
43933
44230
  }
43934
44231
  }
@@ -43985,7 +44282,7 @@ var init_pi = __esm({
43985
44282
  }
43986
44283
  if (mcpTempDir) {
43987
44284
  try {
43988
- rmSync5(mcpTempDir, { recursive: true, force: true });
44285
+ rmSync6(mcpTempDir, { recursive: true, force: true });
43989
44286
  } catch {
43990
44287
  }
43991
44288
  }
@@ -44998,7 +45295,7 @@ __export(browser_engine_exports, {
44998
45295
  });
44999
45296
  import { spawn as spawn8 } from "node:child_process";
45000
45297
  import { createHash as createHash8, randomBytes as randomBytes3, randomUUID as randomUUID11 } from "node:crypto";
45001
- import { chmodSync as chmodSync7, constants as constants2, copyFileSync, existsSync as existsSync18, mkdirSync as mkdirSync16, readFileSync as readFileSync20, readdirSync as readdirSync3, renameSync as renameSync5, rmSync as rmSync6, statSync as statSync3, unlinkSync as unlinkSync4, writeFileSync as writeFileSync13 } from "node:fs";
45298
+ import { chmodSync as chmodSync7, constants as constants2, copyFileSync, existsSync as existsSync18, mkdirSync as mkdirSync16, readFileSync as readFileSync20, readdirSync as readdirSync3, renameSync as renameSync6, rmSync as rmSync7, statSync as statSync4, unlinkSync as unlinkSync4, writeFileSync as writeFileSync13 } from "node:fs";
45002
45299
  import { homedir as homedir23, tmpdir as tmpdir5 } from "node:os";
45003
45300
  import { delimiter as delimiter6, dirname as dirname11, join as join35, resolve as resolve10 } from "node:path";
45004
45301
  function browserRestoreKey(session) {
@@ -45116,7 +45413,7 @@ async function prepareBrowserSessionState(binaryPath, session, options = {}) {
45116
45413
  const candidates = [".json.enc", ".json"].flatMap((suffix) => {
45117
45414
  const path3 = join35(directory, `${session}-${session}${suffix}`);
45118
45415
  try {
45119
- const stat4 = statSync3(path3);
45416
+ const stat4 = statSync4(path3);
45120
45417
  if (!stat4.isFile()) throw new Error("Saved browser state is not a file.");
45121
45418
  return [{ path: path3, suffix, modified: stat4.mtimeMs }];
45122
45419
  } catch (error51) {
@@ -45211,7 +45508,7 @@ async function installAgentBrowserBinary(options = {}) {
45211
45508
  const staging = `${destination}.${randomBytes3(6).toString("hex")}.part`;
45212
45509
  writeFileSync13(staging, body, { mode: 493 });
45213
45510
  if (platform !== "win32") chmodSync7(staging, 493);
45214
- renameSync5(staging, destination);
45511
+ renameSync6(staging, destination);
45215
45512
  return destination;
45216
45513
  }
45217
45514
  function ensureChrome(binaryPath, options = {}) {
@@ -45328,7 +45625,7 @@ function agentBrowserFrame(input) {
45328
45625
  } catch {
45329
45626
  fail(new Error("the browser reported a picture it did not write"));
45330
45627
  } finally {
45331
- rmSync6(file2, { force: true });
45628
+ rmSync7(file2, { force: true });
45332
45629
  }
45333
45630
  };
45334
45631
  child.on("error", (error51) => done(error51 instanceof Error ? error51 : new Error(String(error51))));
@@ -45347,7 +45644,7 @@ function describeBrowserEngine(status) {
45347
45644
  }
45348
45645
  function agentBrowserBinaryExists(dataDir = DATA_DIR) {
45349
45646
  try {
45350
- return statSync3(pinnedBinaryPath(dataDir)).isFile();
45647
+ return statSync4(pinnedBinaryPath(dataDir)).isFile();
45351
45648
  } catch {
45352
45649
  return false;
45353
45650
  }
@@ -45410,7 +45707,7 @@ var init_browser_engine = __esm({
45410
45707
  // server/cli.ts
45411
45708
  var import_qrcode_terminal = __toESM(require_main(), 1);
45412
45709
  import { spawn as spawn9 } from "node:child_process";
45413
- import { closeSync as closeSync4, existsSync as existsSync19, mkdirSync as mkdirSync17, openSync as openSync4, readFileSync as readFileSync21 } from "node:fs";
45710
+ import { closeSync as closeSync5, existsSync as existsSync19, mkdirSync as mkdirSync17, openSync as openSync5, readFileSync as readFileSync21 } from "node:fs";
45414
45711
  import { homedir as homedir24 } from "node:os";
45415
45712
  import { dirname as dirname12, join as join36, resolve as resolve11 } from "node:path";
45416
45713
  import { createInterface as createInterface2 } from "node:readline/promises";
@@ -48373,7 +48670,7 @@ async function runPhoneSetup(options, io, deps) {
48373
48670
  const device = await io.choose("Use OpenMausBot on your phone?", [
48374
48671
  "Skip for now",
48375
48672
  "iPhone / iPad \u2014 native app or Safari",
48376
- "Android \u2014 web browser"
48673
+ "Android \u2014 app or browser"
48377
48674
  ], 0);
48378
48675
  if (device === 0) return skip();
48379
48676
  const phone = device === 1 ? "ios" : "android";
@@ -48436,7 +48733,7 @@ function phonePairingInstructions(phone, input) {
48436
48733
  ];
48437
48734
  }
48438
48735
  return [
48439
- phone === "ios" ? "On iPhone or iPad, scan the QR with Camera to open Safari. If you already have the OpenMausBot iOS app, use its pairing scanner or paste the full pairing link there." : "On Android, scan the QR with Camera and open it in your web browser. This link is for the web app, not the Android native pairing scanner.",
48736
+ phone === "ios" ? "On iPhone or iPad, scan the QR with Camera to open Safari. If you already have the OpenMausBot iOS app, use its pairing scanner or paste the full pairing link there." : "On Android, open the OpenMausBot app and scan the QR with its pairing scanner. The QR is an app link, so Camera will not open it in a browser.",
48440
48737
  `Or open ${origin}/pair on your phone and enter the code.`,
48441
48738
  "Choose Connect on the phone. Scanning a QR does not mean the phone is paired.",
48442
48739
  "The code works once and expires after five minutes. This phone receives client access: chat and approvals, not settings or pairing administration."
@@ -48727,7 +49024,11 @@ function parseArgs(argv, env = process.env) {
48727
49024
  options.domain = domain2;
48728
49025
  } else if (arg === "--tunnel") options.tunnel = true;
48729
49026
  else if (arg === "--client") options.client = true;
48730
- else if (arg === "--no-pair") options.pair = false;
49027
+ else if (arg === "--phone") {
49028
+ const kind = value().toLowerCase();
49029
+ if (kind !== "ios" && kind !== "android") return { error: "--phone takes ios or android" };
49030
+ options.phone = kind;
49031
+ } else if (arg === "--no-pair") options.pair = false;
48731
49032
  else if (arg === "--no-open") options.open = false;
48732
49033
  else if (arg === "--local") options.local = true;
48733
49034
  else if (arg === "--json") options.json = true;
@@ -48787,7 +49088,8 @@ var USAGE = `openmausbot \u2014 your team of AI bots, ready in a few steps
48787
49088
  openmausbot start [the same options as serve]
48788
49089
  openmausbot serve [--port 8799] [--data-dir DIR] [--label NAME]
48789
49090
  [--public-url https://host] [--tailscale | --tunnel | --domain HOST] [--no-pair]
48790
- openmausbot pair [--label NAME] [--client] [--public-url https://host]
49091
+ openmausbot pair [--label NAME] [--client] [--phone ios|android]
49092
+ [--public-url https://host]
48791
49093
  openmausbot sessions [revoke ID]
48792
49094
  openmausbot status
48793
49095
  openmausbot login [--email you@example.com]
@@ -48962,22 +49264,46 @@ async function showPhonePairing(options, origin, log) {
48962
49264
  return false;
48963
49265
  }
48964
49266
  for (const line of phonePairingInstructions(options.phone ?? "ios", { origin, ready })) log(line);
48965
- log(await mintPairing(options.port, { client: true, label: options.label ?? (options.phone === "android" ? "Android" : "iPhone / iPad"), publicUrl: origin }));
49267
+ log(await mintPairing(options.port, { client: true, label: options.label ?? (options.phone === "android" ? "Android" : "iPhone / iPad"), publicUrl: origin, phone: options.phone }));
48966
49268
  log("Waiting for you to connect on the phone. Keep this terminal and the code private.");
48967
49269
  return true;
48968
49270
  }
48969
49271
  function pairingBlock(input) {
48970
49272
  const lines = [`pairing code: ${input.code}`, `expires: ${new Date(input.expiresAt).toLocaleTimeString()} (single use)`];
48971
- if (input.url) {
48972
- lines.push(`open or scan: ${input.url}`);
48973
- lines.push("");
48974
- lines.push(qrToString(input.url));
48975
- } else {
49273
+ if (!input.url && !input.inviteUrl) {
48976
49274
  lines.push(`open: /pair on the address you use for this server, and type the code`);
48977
49275
  if (input.hint) lines.push(` (${input.hint})`);
49276
+ return lines.join("\n");
49277
+ }
49278
+ const scanInvite = input.phone === "android" && !!input.inviteUrl;
49279
+ if (input.url) lines.push(scanInvite ? `web browser: ${input.url}` : `open or scan: ${input.url}`);
49280
+ if (input.inviteUrl) lines.push(`phone app: ${input.inviteUrl}`);
49281
+ const target = scanInvite ? input.inviteUrl : input.url;
49282
+ if (target) {
49283
+ lines.push("");
49284
+ lines.push(qrToString(target));
49285
+ lines.push("");
49286
+ if (scanInvite) {
49287
+ lines.push(`Scan that in the OpenMausBot app. For a browser instead, open the web`);
49288
+ lines.push(`address above and type the code.`);
49289
+ } else if (input.phone === "android") {
49290
+ lines.push(`That QR opens the web app. The Android app needs the phone-app link,`);
49291
+ lines.push(`which this server cannot build without a public address: set`);
49292
+ lines.push(`OMB_PUBLIC_URL, or open the web address above and type the code.`);
49293
+ } else if (input.inviteUrl) {
49294
+ lines.push(`Scan that with Camera for the browser, or paste the phone-app link`);
49295
+ lines.push(`above into the OpenMausBot app.`);
49296
+ }
48978
49297
  }
48979
49298
  return lines.join("\n");
48980
49299
  }
49300
+ function originOf(link) {
49301
+ try {
49302
+ return new URL(link).origin;
49303
+ } catch {
49304
+ return null;
49305
+ }
49306
+ }
48981
49307
  function qrToString(text4) {
48982
49308
  let out = "";
48983
49309
  import_qrcode_terminal.default.generate(text4, { small: true }, (rendered) => {
@@ -48992,7 +49318,9 @@ async function mintPairing(port, options) {
48992
49318
  const { status, body } = await api(port, "/api/auth/pairing", { method: "POST", body: JSON.stringify(request) });
48993
49319
  if (status !== 200) throw new Error(`server refused to mint a pairing code: ${typeof body?.error === "string" ? body.error : status}`);
48994
49320
  const url2 = options.publicUrl ? `${options.publicUrl}/pair#code=${body.code}` : typeof body.url === "string" ? body.url : null;
48995
- return pairingBlock({ code: body.code, url: url2, expiresAt: body.expiresAt, hint: typeof body.hint === "string" ? body.hint : null });
49321
+ const address = options.publicUrl ?? (typeof body.url === "string" ? originOf(body.url) : null);
49322
+ const invite = typeof body.credential === "string" && address ? `openmausbot://pair?address=${encodeURIComponent(address)}&token=${encodeURIComponent(body.credential)}${typeof body.serverName === "string" ? `&name=${encodeURIComponent(body.serverName)}` : ""}` : typeof body.inviteUrl === "string" ? body.inviteUrl : null;
49323
+ return pairingBlock({ code: body.code, url: url2, inviteUrl: invite, expiresAt: body.expiresAt, hint: typeof body.hint === "string" ? body.hint : null, phone: options.phone });
48996
49324
  }
48997
49325
  async function runPair(options) {
48998
49326
  if (!await serverUp(options.port)) {
@@ -49020,7 +49348,7 @@ async function runPair(options) {
49020
49348
  }
49021
49349
  const ui = defaultSetupIo();
49022
49350
  try {
49023
- const selected = await ui.choose("Which phone are you connecting?", ["iPhone / iPad \u2014 app or Safari", "Android \u2014 web browser", "Cancel"], 0);
49351
+ const selected = await ui.choose("Which phone are you connecting?", ["iPhone / iPad \u2014 app or Safari", "Android \u2014 app or browser", "Cancel"], 0);
49024
49352
  if (selected === 2) return 0;
49025
49353
  launch = { ...launch, phone: selected === 0 ? "ios" : "android" };
49026
49354
  return await showPhonePairing(launch, origin, ui.log) ? 0 : 1;
@@ -49030,7 +49358,7 @@ async function runPair(options) {
49030
49358
  return 130;
49031
49359
  }
49032
49360
  }
49033
- console.log(await mintPairing(options.port, { label: options.label, client: options.client, publicUrl: options.publicUrl }));
49361
+ console.log(await mintPairing(options.port, { label: options.label, client: options.client, publicUrl: options.publicUrl, phone: options.phone }));
49034
49362
  if (options.client) console.log("(client scope: chat and approvals only; cannot change settings or pair others)");
49035
49363
  return 0;
49036
49364
  }
@@ -49354,7 +49682,7 @@ async function runServe(options, log = console.log) {
49354
49682
  const logsDir = join36(options.dataDir, "logs");
49355
49683
  mkdirSync17(logsDir, { recursive: true, mode: 448 });
49356
49684
  logPath = join36(logsDir, `server-${Date.now()}-${process.pid}.log`);
49357
- logFd = openSync4(logPath, "wx", 384);
49685
+ logFd = openSync5(logPath, "wx", 384);
49358
49686
  log("\nStarting your workspace\u2026");
49359
49687
  }
49360
49688
  if (tailscale) {
@@ -49378,7 +49706,7 @@ async function runServe(options, log = console.log) {
49378
49706
  }
49379
49707
  throw error51;
49380
49708
  } finally {
49381
- if (logFd !== void 0) closeSync4(logFd);
49709
+ if (logFd !== void 0) closeSync5(logFd);
49382
49710
  process.removeListener("SIGINT", cancelStartup);
49383
49711
  process.removeListener("SIGTERM", cancelStartup);
49384
49712
  }