limixbot 0.3.4 → 0.3.12

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 (133) hide show
  1. package/dist/assets/{ActivityPanel-BG18nJiz.js → ActivityPanel-CJM61CWQ.js} +1 -1
  2. package/dist/assets/{App-8heymKKb.js → App-DRnqqiJP.js} +125 -125
  3. package/dist/assets/{AppEventEditor-D5dGKlRO.js → AppEventEditor-1hupO_kL.js} +1 -1
  4. package/dist/assets/{BotSettingsDialog-Ct_Yktk8.js → BotSettingsDialog-B56-kmya.js} +4 -4
  5. package/dist/assets/{BrowserPanel-Cyws1YTJ.js → BrowserPanel-BXBygd1y.js} +1 -1
  6. package/dist/assets/{BrowserProfilesManager-BEUgS3Me.js → BrowserProfilesManager-CzzGhZGz.js} +1 -1
  7. package/dist/assets/CloudAddDialog-MMhMrVK3.js +1 -0
  8. package/dist/assets/{ComputerFilesPane-BGywdHPB.js → ComputerFilesPane-Hpxjnk2e.js} +1 -1
  9. package/dist/assets/ComputerPanel-CMce64RT.js +2 -0
  10. package/dist/assets/{DataPanel-CIvvbY5b.js → DataPanel-BQ7Ua1Oq.js} +1 -1
  11. package/dist/assets/{DesktopViewer-BNIgo9Nm.js → DesktopViewer-BRzNtGEm.js} +1 -1
  12. package/dist/assets/{FilesView-Cd5kwljm.js → FilesView-DQYXl0zm.js} +1 -1
  13. package/dist/assets/{GrokMoveDialog-B2Vgd-vB.js → GrokMoveDialog-Dk-pwczv.js} +1 -1
  14. package/dist/assets/{InspectorPanel-Bxm2bVHy.js → InspectorPanel-CvnM0TFW.js} +1 -1
  15. package/dist/assets/{KeyboardShortcutsModal-Dfcfy-EA.js → KeyboardShortcutsModal-BQ8aAw16.js} +1 -1
  16. package/dist/assets/{LiveDesktop-fbnBEFRL.js → LiveDesktop-Cr7uAFoQ.js} +1 -1
  17. package/dist/assets/LocalVmWorkspace-BJ-i68MR.js +1 -0
  18. package/dist/assets/MacLocalControl-DE8MVZkU.js +1 -0
  19. package/dist/assets/{MarkdownFilePreview--PNcCEvm.js → MarkdownFilePreview-B_6azXNT.js} +1 -1
  20. package/dist/assets/{MenuMotion-IQitGKH4.js → MenuMotion-BDYr4RxL.js} +1 -1
  21. package/dist/assets/{NewBotDialog-CZbVbamH.js → NewBotDialog-D9fUysLU.js} +1 -1
  22. package/dist/assets/PhoneAppDialog-DUv4Qx8n.js +1 -0
  23. package/dist/assets/{PlacesRail-v1XIqiBG.js → PlacesRail-yZdo57pP.js} +2 -2
  24. package/dist/assets/{PluginsPanel-Bj3Pn0Iw.js → PluginsPanel-DYuWdWZd.js} +1 -1
  25. package/dist/assets/{ProIntroduction-qFeFSzyk.js → ProIntroduction-DapZkP7I.js} +1 -1
  26. package/dist/assets/{RemoteAgentSettingsPanel-CcbUTgTg.js → RemoteAgentSettingsPanel-sNv3TbBd.js} +1 -1
  27. package/dist/assets/{RoomPanel-k0hQwspJ.js → RoomPanel-DrlyOJKZ.js} +1 -1
  28. package/dist/assets/{RoutinesPage-aANJQ--g.js → RoutinesPage-CIo30TVq.js} +2 -2
  29. package/dist/assets/SettingsModal-BpJhi2nc.js +11 -0
  30. package/dist/assets/{ShareTeamDialog-DeRaL6zM.js → ShareTeamDialog-CH1ts7Uj.js} +1 -1
  31. package/dist/assets/{SignedInSites-CfhTex9p.js → SignedInSites-CisTAQW0.js} +1 -1
  32. package/dist/assets/{TeamLibraryPanel-C7LZ7Onp.js → TeamLibraryPanel-DvnKye05.js} +1 -1
  33. package/dist/assets/TeamMapPage-JOiilgs0.js +1 -0
  34. package/dist/assets/TriggersPage-DQ3HQ6_i.js +1 -0
  35. package/dist/assets/{VisibilitySection-CCwpvTlJ.js → VisibilitySection-DXfPp0Ql.js} +6 -6
  36. package/dist/assets/{WebhookTrigger-wqj5vki5.js → WebhookTrigger-BYeOyOpX.js} +1 -1
  37. package/dist/assets/{WorksOnGrid-CenOXsZe.js → WorksOnGrid-8fCnUzpQ.js} +1 -1
  38. package/dist/assets/{abnfDiagram-YUKMZFIV-CBzJU2O6.js → abnfDiagram-YUKMZFIV-Boj-Bkbs.js} +1 -1
  39. package/dist/assets/{activity-bd7MWK8m.js → activity-MpAxS_kw.js} +1 -1
  40. package/dist/assets/{architectureDiagram-47P4ROYG-DVR1DP3v.js → architectureDiagram-47P4ROYG-Cl-ocqce.js} +1 -1
  41. package/dist/assets/{blockDiagram-E7TT5TSB-BYqUHhoh.js → blockDiagram-E7TT5TSB-0nSQiv6g.js} +1 -1
  42. package/dist/assets/{c4Diagram-CRA5TL53-BtQERHwr.js → c4Diagram-CRA5TL53-Cx9iymkb.js} +1 -1
  43. package/dist/assets/{calendar-days-YMHxUvlg.js → calendar-days-DXHDRm_4.js} +1 -1
  44. package/dist/assets/channel-BStWWrSt.js +1 -0
  45. package/dist/assets/{chunk-2Q5K7J3B-Cwf_xeHP.js → chunk-2Q5K7J3B-0rIrumUG.js} +1 -1
  46. package/dist/assets/{chunk-5VM5RSS4-Doo-zxI4.js → chunk-5VM5RSS4-w9e6iKvs.js} +1 -1
  47. package/dist/assets/{chunk-7TKQ45FW-CD2AgrRj.js → chunk-7TKQ45FW-c4KxVKkf.js} +1 -1
  48. package/dist/assets/{chunk-AW2ZBBNX-e5OuWeq_.js → chunk-AW2ZBBNX-DmaBcuOP.js} +1 -1
  49. package/dist/assets/{chunk-FVRAUYC3-C8jo43it.js → chunk-FVRAUYC3-BLzfqLHR.js} +2 -2
  50. package/dist/assets/{chunk-HTAEGDNF-DLsD4agj.js → chunk-HTAEGDNF-BN48XtBm.js} +1 -1
  51. package/dist/assets/{chunk-JWPE2WC7-UZDUbeTy.js → chunk-JWPE2WC7-D2NACa6i.js} +1 -1
  52. package/dist/assets/{chunk-KQW6MTUR-DR9SuVX3.js → chunk-KQW6MTUR-CbAfnxxG.js} +1 -1
  53. package/dist/assets/{chunk-XXDRQBXY-DwEl4815.js → chunk-XXDRQBXY-Bwg-DPP_.js} +1 -1
  54. package/dist/assets/{classDiagram-v2-K4WV4PDN-CQZho8e0.js → classDiagram-v2-K4WV4PDN-Dh3ohoZp.js} +1 -1
  55. package/dist/assets/{coins-DpzGN3sd.js → coins-Bv8-qll4.js} +1 -1
  56. package/dist/assets/{core-C_G-J8y-.js → core-DeJAGBXR.js} +1 -1
  57. package/dist/assets/{cose-bilkent-JH36ORCC-DUZ9OWzu.js → cose-bilkent-JH36ORCC-DCHadn0L.js} +1 -1
  58. package/dist/assets/{cynefin-EF2NZ3EQ-DVHnJdjN.js → cynefin-EF2NZ3EQ-ClpPPxUg.js} +1 -1
  59. package/dist/assets/{cynefinDiagram-3GCD6N5R-dYhbIkRt.js → cynefinDiagram-3GCD6N5R-DZ5l7nkL.js} +1 -1
  60. package/dist/assets/{dagre-W4DXFKR2-DqJNpw5w.js → dagre-W4DXFKR2-BagTvT62.js} +1 -1
  61. package/dist/assets/{diagram-2UJZ2QOL-DKNDqAnE.js → diagram-2UJZ2QOL-DuByootb.js} +1 -1
  62. package/dist/assets/{diagram-OVF4WLC6-DENMVM-d.js → diagram-OVF4WLC6-BQ8Cvr0D.js} +1 -1
  63. package/dist/assets/{diagram-PFPMY2P6-CrRFeR5V.js → diagram-PFPMY2P6-D_pklaf9.js} +1 -1
  64. package/dist/assets/{diagram-UMYRVEAY-B5w-desO.js → diagram-UMYRVEAY-1BuHq7tR.js} +1 -1
  65. package/dist/assets/{diagram-YEKJPTXX-C7AVX3MB.js → diagram-YEKJPTXX-CK0cPx8n.js} +1 -1
  66. package/dist/assets/{diagram-ZIFT7M5P-BgkTRwwr.js → diagram-ZIFT7M5P-DVzPho45.js} +1 -1
  67. package/dist/assets/{ebnfDiagram-VR2GEFS7-yY_2KNsK.js → ebnfDiagram-VR2GEFS7-D8UO_5sE.js} +1 -1
  68. package/dist/assets/{elk-IJKZMXRS-xWtfkoqT.js → elk-IJKZMXRS-BRigfR5V.js} +1 -1
  69. package/dist/assets/{erDiagram-O2IAPWRE-BMM4saPI.js → erDiagram-O2IAPWRE-CXHPobWj.js} +1 -1
  70. package/dist/assets/{file-up-Bl7ViNTL.js → file-up-CQezfdJk.js} +1 -1
  71. package/dist/assets/{flowDiagram-OXPTDLAJ-BCRe8i6T.js → flowDiagram-OXPTDLAJ-CqKD6jh8.js} +1 -1
  72. package/dist/assets/{ganttDiagram-R7TSEDQI-COBrcDi7.js → ganttDiagram-R7TSEDQI-C9FV3gsA.js} +1 -1
  73. package/dist/assets/{gitGraphDiagram-XJZIOB7I-Dc4RKydD.js → gitGraphDiagram-XJZIOB7I-CRu4dnr_.js} +1 -1
  74. package/dist/assets/{import-name-CKE76uoY.js → import-name-dEG0japD.js} +1 -1
  75. package/dist/assets/{index-jlMtdqfJ.js → index-B4HW_T-k.js} +1 -1
  76. package/dist/assets/{index-BBsdMDZV.js → index-COnd2OMZ.js} +2 -2
  77. package/dist/assets/{infoDiagram-5W2HQ5XZ-BV8RRI_4.js → infoDiagram-5W2HQ5XZ-CCRjb2CQ.js} +1 -1
  78. package/dist/assets/{ishikawaDiagram-K3B6WC7H-H-XILdGm.js → ishikawaDiagram-K3B6WC7H-TaNPsGZS.js} +1 -1
  79. package/dist/assets/{journeyDiagram-COXZFDF6-ChLduKEj.js → journeyDiagram-COXZFDF6-Dz7YIIc2.js} +1 -1
  80. package/dist/assets/{kanban-definition-P3RFRI5V-FSZ9F3JH.js → kanban-definition-P3RFRI5V-Cg69Fpp8.js} +1 -1
  81. package/dist/assets/{mcp-servers-DGQioE-C.js → mcp-servers-Ih6dI7Ga.js} +1 -1
  82. package/dist/assets/{mermaid.core-BRsJ02Pz.js → mermaid.core-DiG3LoLz.js} +4 -4
  83. package/dist/assets/{mindmap-definition-LZFPQGKD-TWHzZjNT.js → mindmap-definition-LZFPQGKD-nxP4f0fq.js} +1 -1
  84. package/dist/assets/{minimize-BYscOtPM.js → minimize-B_k_fmmo.js} +1 -1
  85. package/dist/assets/{monitor-DFEzCfQk.js → monitor-BfmnPU-I.js} +1 -1
  86. package/dist/assets/{new-bot-7IqfI7S4.js → new-bot-CQm0FPc5.js} +2 -2
  87. package/dist/assets/{org-library-BW4CqVd6.js → org-library-C2li0IRL.js} +1 -1
  88. package/dist/assets/{package-format-BOH5_nvi.js → package-format-B3whu9DW.js} +1 -1
  89. package/dist/assets/{pegDiagram-BGZESJAR-LQWOF7IE.js → pegDiagram-BGZESJAR-gPjpGcNH.js} +1 -1
  90. package/dist/assets/{pieDiagram-CAPJLFHJ-BRXp4qFw.js → pieDiagram-CAPJLFHJ-Cgh25Z4_.js} +1 -1
  91. package/dist/assets/{quadrantDiagram-GDMTTRAM-DyIdGBZo.js → quadrantDiagram-GDMTTRAM-DD9luyqA.js} +1 -1
  92. package/dist/assets/{rail-Du7tlPwj.js → rail-B1mj4zlo.js} +1 -1
  93. package/dist/assets/{railroadDiagram-SM67HX2B-BrL1MhW1.js → railroadDiagram-SM67HX2B-CS_TYko7.js} +1 -1
  94. package/dist/assets/{remote-desktop-panel-B9rqQRMc.js → remote-desktop-panel-DIllbP7i.js} +1 -1
  95. package/dist/assets/{requirementDiagram-X7JNWC4A-DTYjaC8j.js → requirementDiagram-X7JNWC4A-BePHia6E.js} +1 -1
  96. package/dist/assets/{sankeyDiagram-UM26HJRW-BR9dASO5.js → sankeyDiagram-UM26HJRW-24ExCXGS.js} +1 -1
  97. package/dist/assets/{sequenceDiagram-ZO4K6R2Y-BR1k8h2_.js → sequenceDiagram-ZO4K6R2Y-Dtmx27I5.js} +1 -1
  98. package/dist/assets/{sizeCapture-INFHLROL-u8zOvZVi.js → sizeCapture-INFHLROL-2LvmoT1T.js} +1 -1
  99. package/dist/assets/{stateDiagram-v2-TBUQTH76-mQ64BAFv.js → stateDiagram-v2-TBUQTH76-moL2SIo9.js} +1 -1
  100. package/dist/assets/{styles-BlayaJti.js → styles-6RbfQIY7.js} +3 -3
  101. package/dist/assets/{swimlanes-N4OXWK64-CmzC3Mu0.js → swimlanes-N4OXWK64-D95Pk8qh.js} +1 -1
  102. package/dist/assets/swimlanesDiagram-K3J5GTZL-DByi9G3i.js +8 -0
  103. package/dist/assets/{team-share-C3w1L3WL.js → team-share-0GXKeixY.js} +1 -1
  104. package/dist/assets/{timeline-definition-YOQAKGHF-9aPCRsK2.js → timeline-definition-YOQAKGHF-HM-J25k2.js} +1 -1
  105. package/dist/assets/{usecaseDiagram-VIAY4XPW--ngAouwr.js → usecaseDiagram-VIAY4XPW-C6q6bCzJ.js} +1 -1
  106. package/dist/assets/{vennDiagram-BLWOH2XV-C3zKxuAB.js → vennDiagram-BLWOH2XV-hf0dh2yT.js} +1 -1
  107. package/dist/assets/{vi-uLErku_e.js → vi-CVDyZGky.js} +3 -3
  108. package/dist/assets/{wardleyDiagram-YMQ3BMBF-CZWsQPSF.js → wardleyDiagram-YMQ3BMBF-CpT_eRn9.js} +1 -1
  109. package/dist/assets/{xychartDiagram-TAQBALBS-BTcsDP6v.js → xychartDiagram-TAQBALBS-D6g1V9DG.js} +1 -1
  110. package/dist/index.html +2 -2
  111. package/dist/rail.html +4 -4
  112. package/dist-server/index.js +2456 -2424
  113. package/dist-server/package.json +1 -1
  114. package/dist-server/server/boat-create-idempotency.js +1 -1
  115. package/dist-server/server/boat-delete-journal.js +1 -1
  116. package/dist-server/server/boat.js +10 -3
  117. package/dist-server/server/bot-api-host.js +9 -5
  118. package/dist-server/server/index.js +2 -1
  119. package/dist-server/server/limix-instabox.js +382 -0
  120. package/dist-server/server/limix-owner-lane.js +51 -0
  121. package/dist-server/shared/first-reply-rescue.js +3 -3
  122. package/package.json +1 -1
  123. package/dist/assets/CloudAddDialog-UEPtzvka.js +0 -1
  124. package/dist/assets/ComputerPanel-DLV2INTM.js +0 -2
  125. package/dist/assets/LocalVmWorkspace-CBe48xft.js +0 -1
  126. package/dist/assets/MacLocalControl-CpVqwJts.js +0 -1
  127. package/dist/assets/PhoneAppDialog-B9oozUp0.js +0 -1
  128. package/dist/assets/SettingsModal-t10M3Ahi.js +0 -11
  129. package/dist/assets/TeamMapPage-BSvk8tOC.js +0 -1
  130. package/dist/assets/TriggersPage-BdePfInC.js +0 -1
  131. package/dist/assets/channel-D6e78p3W.js +0 -1
  132. package/dist/assets/index-BXLNny-w.js +0 -1
  133. package/dist/assets/swimlanesDiagram-K3J5GTZL-Coei7rJr.js +0 -8
@@ -1 +1 @@
1
- {"type":"module","version":"0.3.4"}
1
+ {"type":"module","version":"0.3.12"}
@@ -12,7 +12,7 @@ const LOCK_RETRY_MS = 20;
12
12
  const MAX_REAPER_GENERATIONS = 128;
13
13
  const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
14
14
  const BOT_ID = /^[A-Za-z0-9_-]{1,120}$/;
15
- const BOAT_ID = /^bx_[23456789abcdefghjkmnpqrstuvwxyz]{8}$/;
15
+ const BOAT_ID = /^(?:bx_[23456789abcdefghjkmnpqrstuvwxyz]{8}|m_[a-z0-9]{20,40})$/;
16
16
  const lockWait = new Int32Array(new SharedArrayBuffer(4));
17
17
  function isRequest(value) {
18
18
  if (!value || typeof value !== "object" || Array.isArray(value))
@@ -11,7 +11,7 @@ const LOCK_RETRY_MS = 20;
11
11
  const MAX_REAPER_GENERATIONS = 128;
12
12
  const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
13
13
  const BOT_ID = /^[A-Za-z0-9_-]{1,120}$/;
14
- const BOAT_ID = /^bx_[23456789abcdefghjkmnpqrstuvwxyz]{8}$/;
14
+ const BOAT_ID = /^(?:bx_[23456789abcdefghjkmnpqrstuvwxyz]{8}|m_[a-z0-9]{20,40})$/;
15
15
  const OPERATION_ID = /^bdop_[a-f0-9]{32}$/;
16
16
  const PROVIDER_STATUSES = new Set(["pending", "processing", "blocked", "completed"]);
17
17
  /** Codes of a create refused while the bot's previous computer is being removed, or right after its removal finished. */
@@ -7,6 +7,7 @@ import { DATA_DIR } from "./config.js";
7
7
  import { loadEnvironmentId } from "./environment.js";
8
8
  import { boatCredential, boatProviderApi } from "./included-services.js";
9
9
  import { relayBody, relayPath } from "./boat-relay.js";
10
+ import { instaboxFetch, isInstaboxKey, verifyInstaboxKey } from "./limix-instabox.js";
10
11
  import { blockedUntilBoatFinishes, boatDeletionOperation, deletionBlockedError, keptForNewerSnapshots, } from "./boat-deletion.js";
11
12
  import { adoptResolvedBoat, beginBoatCreate, boatCreateRecoverySnapshot, discardBoatCreate, rememberCreatedBoat, resolveBoatCreate, retireDeletedBoatCreate, } from "./boat-create-idempotency.js";
12
13
  import { BOAT_DELETING, BOAT_DELETION_FINISHED, boatDeletionSnapshot, getBoatDeletion, hasPendingBoatDeletionForBot, markBoatDeletionAccepted, markBoatDeletionBlocked, prepareBoatDeletion, retireBoatDeletion, } from "./boat-delete-journal.js";
@@ -41,7 +42,7 @@ const BOAT_INVENTORY_PAGE_SIZE = 200;
41
42
  const MAX_BOAT_INVENTORY_PAGES = 10;
42
43
  const LEGACY_MANAGED_BOAT_NAME = /^ogb-[a-z0-9]{1,8}-[a-f0-9]{6}$/;
43
44
  const SCOPED_MANAGED_BOAT_NAME = /^ogb-[a-f0-9]{12}-[a-z0-9]{1,8}-[a-f0-9]{6}$/;
44
- const BOAT_ID = /^bx_[23456789abcdefghjkmnpqrstuvwxyz]{8}$/;
45
+ const BOAT_ID = /^(?:bx_[23456789abcdefghjkmnpqrstuvwxyz]{8}|m_[a-z0-9]{20,40})$/;
45
46
  /** Sandbox.state in v1, plus OpenMausBot's own `removing` for a sandbox whose deletion is under way. */
46
47
  const BOAT_STATES = new Set([
47
48
  "init", "provisioning", "provisioned", "cloning", "ready", "idle", "running", "archiving", "archived", "error", "cancelled", "removing",
@@ -71,6 +72,8 @@ function snapshotBoatConfig(cfg) {
71
72
  /** An own token goes to Boat; Cloud Pro's included token only to its relay. */
72
73
  export function boatFetch(cfg, path, opts = {}) {
73
74
  const account = boatAccount(cfg);
75
+ if (isInstaboxKey(account?.token))
76
+ return instaboxFetch(account.token, path, opts);
74
77
  return fetch(`${account?.api ?? boatProviderApi()}${relayPath(account, path)}`, {
75
78
  ...opts,
76
79
  headers: {
@@ -785,6 +788,8 @@ export function describeBoatAccount(cfg) {
785
788
  return { configured: Boolean(account), ...(account?.included ? { included: true } : {}) };
786
789
  }
787
790
  export async function verifyToken(token) {
791
+ if (isInstaboxKey(token))
792
+ return verifyInstaboxKey(token);
788
793
  try {
789
794
  const res = await fetch(`${boatProviderApi()}/sandboxes?limit=1`, {
790
795
  headers: { authorization: `Bearer ${token}` },
@@ -798,7 +803,7 @@ export async function verifyToken(token) {
798
803
  ok: false, rejected: true,
799
804
  message: /^(boat|box)_/.test(token)
800
805
  ? "boat.dev rejected that key — it may have been revoked or expired. Copy a fresh one from your boat.dev account."
801
- : "That doesn't look like a Boat API key: they start with boat_ (older ones with box_). Copy the API key from your boat.dev account (an account or session token won't work here).",
806
+ : "That isn't a Boat or Instabox API key: Boat keys start with boat_ (older ones with box_), Instabox keys with ibk_. Copy the whole key, first letters included.",
802
807
  };
803
808
  }
804
809
  return { ok: false, message: `boat.dev returned ${res.status} for that token — try again in a moment.` };
@@ -810,6 +815,8 @@ export async function verifyToken(token) {
810
815
  const INCLUDED_BOAT_UNAVAILABLE = "The cloud computers included with your Cloud plan aren't available right now. Try again later.";
811
816
  const boatServiceName = (included) => included ? "Limix Bot Cloud's computer service" : "boat.dev";
812
817
  export function boatErrorMessage(status, what, body, included = false) {
818
+ if (typeof body?.message === "string" && body.message.startsWith("Instabox:"))
819
+ return `${what} failed. ${body.message}`;
813
820
  const theirs = typeof body?.message === "string" ? body.message.trim() : "";
814
821
  const link = typeof body?.error?.details?.billingUrl === "string" ? body.error.details.billingUrl : "";
815
822
  if (status === 402) {
@@ -849,7 +856,7 @@ function idempotentCreateInProgress(result) {
849
856
  }
850
857
  async function requestBoatCreate(cfg, botId, ttlSeconds) {
851
858
  // noEnv: no key this server holds ever reaches the computer. A trial-TTL retry needs a different idempotency key.
852
- const body = JSON.stringify({ ttlSeconds, noEnv: true });
859
+ const body = JSON.stringify({ ttlSeconds, noEnv: true, ...(isInstaboxKey(boatAccount(cfg)?.token) ? { name: await boatNameFor(botId) } : {}) });
853
860
  let attempt = beginBoatCreate(botId, body);
854
861
  let request = attempt.request;
855
862
  let createdThisAttempt = attempt.startedNow;
@@ -10,6 +10,7 @@ import { personKey } from "./person-key.js";
10
10
  import { PASS } from "./routes/table.js";
11
11
  import { parseJson, schemaIssue } from "./schema.js";
12
12
  import { bearerSecret, header, json, readRawBody } from "./webhook-ingress.js";
13
+ import { isLimixOwnerThread, LIMIX_OWNER_HEADER, limixOwnerCall, markLimixOwnerThread } from "./limix-owner-lane.js";
13
14
  /** How long a call waits by default before answering "working" with its thread id. Under the 60 s that Fly's proxy is reported
14
15
  * (community.fly.io, staff replies, 2022-2024) to allow a connection with no bytes: a dropped answer would lose the thread id. */
15
16
  export const BOT_API_DEFAULT_WAIT_MS = 50_000;
@@ -93,13 +94,13 @@ export function createBotApiHost(deps) {
93
94
  return { thread: threadId, status: "working" };
94
95
  return { reply: replyText(after), thread: threadId };
95
96
  }
96
- async function runCall(botId, call, turnStarted) {
97
+ async function runCall(botId, call, turnStarted, owner = false) {
97
98
  const store = deps.store;
98
99
  let threadId = call.thread;
99
100
  if (threadId !== undefined) {
100
101
  const task = store.taskByThread(botId, threadId);
101
102
  // Only a thread this API opened: the caller never writes into the person's own conversations.
102
- if (!task || task.botApi !== true)
103
+ if (!task || task.botApi !== true || isLimixOwnerThread(threadId) !== owner)
103
104
  fail(404, "no such thread");
104
105
  if (deps.threadBusy(botId, threadId))
105
106
  return { status: 409, body: { thread: threadId, status: "working" } };
@@ -116,6 +117,8 @@ export function createBotApiHost(deps) {
116
117
  threadId = task.threadId;
117
118
  opened = true;
118
119
  store.patchTask(botId, threadId, { botApi: true, outside: true });
120
+ if (owner)
121
+ markLimixOwnerThread(threadId);
119
122
  }
120
123
  const receivedAt = Date.now();
121
124
  const userMessage = store.appendMessage(threadId, { role: "user", kind: "text", text: message, via: "api" });
@@ -192,7 +195,8 @@ export function createBotApiHost(deps) {
192
195
  json(res, 403, { error: BOT_API_UPGRADE });
193
196
  return true;
194
197
  }
195
- if (!deps.canConfine(botId)) {
198
+ const limixOwner = limixOwnerCall(req.headers[LIMIX_OWNER_HEADER]);
199
+ if (!limixOwner && !deps.canConfine(botId)) {
196
200
  manager.recordRefused(botId, 403, CANNOT_CONFINE);
197
201
  json(res, 403, { error: CANNOT_CONFINE });
198
202
  return true;
@@ -200,7 +204,7 @@ export function createBotApiHost(deps) {
200
204
  if (req.method === "GET") {
201
205
  const threadId = url.searchParams.get("thread") ?? "";
202
206
  const task = /^[\w-]{1,128}$/.test(threadId) ? deps.store.taskByThread(botId, threadId) : undefined;
203
- if (!task || task.botApi !== true) {
207
+ if (!task || task.botApi !== true || isLimixOwnerThread(threadId) !== limixOwner) {
204
208
  json(res, 404, { error: "no such thread" });
205
209
  return true;
206
210
  }
@@ -237,7 +241,7 @@ export function createBotApiHost(deps) {
237
241
  return true;
238
242
  }
239
243
  try {
240
- const answer = await runCall(botId, call, release);
244
+ const answer = await runCall(botId, call, release, limixOwner);
241
245
  manager.record(botId, { outcome: "accepted", statusCode: answer.status, threadId: answer.body.thread, preview: call.message,
242
246
  reason: "reply" in answer.body ? "Replied" : answer.body.status === "needs_approval" ? "Stopped at an approval" : "Still working" });
243
247
  json(res, answer.status, answer.body);
@@ -588,6 +588,7 @@ import { createPersonCalendar } from "./host/person-calendar.js";
588
588
  import { createDevsReports } from "./host/devs-reports.js";
589
589
  import { limixReportAlias, limixReportSender, limixStatusAsker } from "./limix-report-mail.js";
590
590
  import { captureRecentLog } from "./recent-log.js";
591
+ import { isLimixOwnerThread } from "./limix-owner-lane.js";
591
592
  import { createShareAnswer } from "./host/share-answer.js";
592
593
  import { createCardRequester, linePersonKey } from "./host/thread-person.js";
593
594
  import { selfThreadRefusal } from "./host/self-threads.js";
@@ -2786,7 +2787,7 @@ const outsideLineage = createOutsideLineage({
2786
2787
  node: (id) => coordinator.node(id), active: () => coordinator.active(), language: (threadId) => threadLanguage(threadId),
2787
2788
  });
2788
2789
  const { outsideDrivenThread, cloudGuestDriven, cloudGuestDrivenHandoff, outsideWorkStaysHere, confinedWhy, guestEngineRefusal } = outsideLineage;
2789
- function botApiThread(id) { const owner = store.botByThread(id); return Boolean(owner && store.taskByThread(owner.id, id)?.botApi); }
2790
+ function botApiThread(id) { const owner = store.botByThread(id); return Boolean(owner && store.taskByThread(owner.id, id)?.botApi) && !isLimixOwnerThread(id); }
2790
2791
  const { messagePage, messageWindow } = messagePages(store);
2791
2792
  const replyVoiceNotes = createReplyVoiceNotes({ store, config: () => cfg, voiceReady: tts.voiceReady, speak: tts.speak, save: saveAudio, remove: deleteAttachment, redact: redactSecretsInText, language: threadLanguage });
2792
2793
  const { cloudLendingTurn, cloudLendingRefusalNotice, cloudLendingRefusalForBot, cloudLendingRefusal, cloudThreadStarters, cloudBotOpened, cloudLendingView, cloudReportFromOthers, handsOnLending, handedInUnproven, cloudOwnTurnRefusal, roomTurnStarted, roomTurnEnded, roomRun } = createCloudLendingTurns({
@@ -0,0 +1,382 @@
1
+ // Limix Bot: Instabox (https://instabox.sh) as the cloud computer service, behind the same transport as Boat.
2
+ // An Instabox API key (ibk_…) pasted in Settings → API keys sends every Boat REST call here, translated to the Instabox
3
+ // API (https://instabox.sh/docs/api.md, checked 2026-11-12): machines instead of sandboxes, operations instead of
4
+ // deletion operations, /exec instead of /commands. The answers come back in Boat's shape, so the rest of the app is
5
+ // unchanged. Instabox cannot rename a machine, so the create carries the name (see `instaboxCreateName`).
6
+ import { createHash, randomUUID } from "node:crypto";
7
+ import { readFileSync, writeFileSync } from "node:fs";
8
+ import { join } from "node:path";
9
+ import { DATA_DIR } from "./config.js";
10
+ export const INSTABOX_API = "https://api.instabox.sh";
11
+ /** Instabox's free plan creates only from agent-browser, android, omarchy or ci (refused with template_not_allowed, seen
12
+ * 2026-11-12): universal first, then agent-browser, a Linux desktop with a browser. */
13
+ export const INSTABOX_TEMPLATES = ["universal", "agent-browser"];
14
+ const DESKTOP_WIDTH = 1280;
15
+ const DESKTOP_HEIGHT = 800;
16
+ const EXEC_MAX_SECONDS = 600;
17
+ const STREAM_TTL_SECONDS = 3600;
18
+ const IDLE_SUSPEND_SECONDS = 3600;
19
+ const BG_DIR = "/tmp/limix-bg";
20
+ export const isInstaboxKey = (token) => typeof token === "string" && token.trim().startsWith("ibk_");
21
+ /** Boat's sandbox states, from an Instabox machine state. */
22
+ export function boatStateFromInstabox(state) {
23
+ switch (state) {
24
+ case "running": return "running";
25
+ case "creating":
26
+ case "starting":
27
+ case "resuming": return "provisioning";
28
+ case "stopping":
29
+ case "suspending": return "archiving";
30
+ case "stopped":
31
+ case "suspended": return "archived";
32
+ case "deleting": return "removing";
33
+ case "error": return "error";
34
+ default: return "init";
35
+ }
36
+ }
37
+ /** A machine the person made in Instabox themselves, taken over by a bot: Instabox names cannot change, so the bot's
38
+ * computer name is remembered here, by machine id. */
39
+ const adoptedFile = () => join(DATA_DIR, "limix-instabox-adopted.json");
40
+ function adopted() {
41
+ try {
42
+ const saved = JSON.parse(readFileSync(adoptedFile(), "utf8"));
43
+ return saved && typeof saved === "object" && !Array.isArray(saved) ? saved : {};
44
+ }
45
+ catch {
46
+ return {};
47
+ }
48
+ }
49
+ function adopt(machineId, name) {
50
+ writeFileSync(adoptedFile(), JSON.stringify({ ...adopted(), [machineId]: name }));
51
+ }
52
+ const MANAGED_NAME = /^ogb-/;
53
+ /** The one machine in the account that no bot uses yet and that Limix did not make, else null: with several, none is
54
+ * taken, since the others may be for something else. */
55
+ async function adoptable(token, signal) {
56
+ const listed = await ib(token, "/v1/machines?limit=100", { signal });
57
+ if (!listed.ok || !Array.isArray(listed.body?.machines))
58
+ return null;
59
+ const taken = adopted();
60
+ const free = listed.body.machines.filter((m) => typeof m?.id === "string" && !taken[m.id] && !MANAGED_NAME.test(String(m?.name ?? ""))
61
+ && !["deleting", "error"].includes(m?.state));
62
+ return free.length === 1 ? free[0] : null;
63
+ }
64
+ function sandboxFrom(machine) {
65
+ const id = String(machine?.id ?? "");
66
+ return {
67
+ id,
68
+ name: adopted()[id] ?? (typeof machine?.name === "string" ? machine.name : ""),
69
+ state: boatStateFromInstabox(machine?.state),
70
+ desktopAvailable: true,
71
+ createdAt: machine?.createdAt ?? null,
72
+ };
73
+ }
74
+ /** Boat's deletion receipts carry bdop_ + 32 hex; Instabox operations are op_…, so each gets a stable stand-in. */
75
+ const deletionOps = new Map();
76
+ export function deletionOperationId(opId) {
77
+ return `bdop_${createHash("sha256").update(opId).digest("hex").slice(0, 32)}`;
78
+ }
79
+ function deletionStatus(state) {
80
+ if (state === "succeeded")
81
+ return "completed";
82
+ if (state === "queued")
83
+ return "pending";
84
+ if (state === "failed" || state === "cancelled")
85
+ return "blocked";
86
+ return "processing";
87
+ }
88
+ function json(status, body) {
89
+ return new Response(JSON.stringify(body), { status, headers: { "content-type": "application/json" } });
90
+ }
91
+ /** Boat's error envelope ({message, error:{code}}) from Instabox's ({error:{code,message}}). */
92
+ function boatError(status, body) {
93
+ const code = typeof body?.error?.code === "string" ? body.error.code : undefined;
94
+ const message = typeof body?.error?.message === "string" ? body.error.message : `Instabox ${status}`;
95
+ const scopes = Array.isArray(body?.error?.requiredScopes) ? ` (key needs: ${body.error.requiredScopes.join(", ")})` : "";
96
+ const hint = typeof body?.error?.hint === "string" ? ` ${body.error.hint}` : "";
97
+ // Boat's 403 means "wrong key" to the rest of the app; Instabox's 403 is a missing scope, unaccepted terms or a plan
98
+ // limit, so it travels as 409 and keeps Instabox's own words.
99
+ return json(status === 403 ? 409 : status, { ok: false, message: `Instabox: ${message}${scopes}${hint}${code ? ` [${code}]` : ""}`, error: { code, message } });
100
+ }
101
+ async function ib(token, path, call = {}) {
102
+ const headers = { "X-API-Key": token, ...call.headers };
103
+ if (call.body !== undefined)
104
+ headers["content-type"] = "application/json";
105
+ if (call.idempotent)
106
+ headers["Idempotency-Key"] ??= randomUUID();
107
+ const raw = await fetch(`${INSTABOX_API}${path}`, {
108
+ method: call.method ?? "GET",
109
+ headers,
110
+ body: call.body === undefined ? undefined : JSON.stringify(call.body),
111
+ signal: call.signal ?? undefined,
112
+ });
113
+ const body = raw.headers.get("content-type")?.includes("json") ? await raw.clone().json().catch(() => null) : null;
114
+ return { status: raw.status, ok: raw.ok, body, raw };
115
+ }
116
+ /** Commands run with DISPLAY pointed at the machine's X server, wherever Instabox started it. */
117
+ const WITH_DISPLAY = 'if [ -z "$DISPLAY" ]; then for s in /tmp/.X11-unix/X*; do [ -e "$s" ] && export DISPLAY=":${s##*X}" && break; done; fi; ';
118
+ async function exec(token, machineId, cmd, timeoutSeconds, signal) {
119
+ return ib(token, `/v1/machines/${machineId}/exec`, {
120
+ method: "POST",
121
+ body: { cmd: WITH_DISPLAY + cmd, timeoutSeconds: Math.max(1, Math.min(EXEC_MAX_SECONDS, Math.round(timeoutSeconds))), noHistory: true },
122
+ signal,
123
+ });
124
+ }
125
+ const shellQuote = (value) => `'${value.replace(/'/g, `'\\''`)}'`;
126
+ /** The screen tools the computer-use driver calls; installed once per machine, quietly, if the template lacks them. */
127
+ const SCREEN_TOOLS = "command -v xdotool >/dev/null && command -v xclip >/dev/null && (command -v scrot || command -v import || command -v ffmpeg) >/dev/null"
128
+ + " || { S=; [ \"$(id -u)\" = 0 ] || S='sudo -n'; $S apt-get update -qq >/dev/null 2>&1;"
129
+ + " DEBIAN_FRONTEND=noninteractive $S apt-get install -y -qq xdotool xclip scrot imagemagick >/dev/null 2>&1; }; command -v xdotool >/dev/null";
130
+ const toolsReady = new Set();
131
+ async function startDesktop(token, machineId, signal) {
132
+ const status = await ib(token, `/v1/machines/${machineId}/desktop`, { method: "POST", body: { action: "status" }, signal });
133
+ if (!status.ok)
134
+ return boatError(status.status, status.body);
135
+ const running = status.body?.running === true || status.body?.state === "running";
136
+ if (!running) {
137
+ const started = await ib(token, `/v1/machines/${machineId}/desktop`, {
138
+ method: "POST", body: { action: "start", width: DESKTOP_WIDTH, height: DESKTOP_HEIGHT }, signal,
139
+ });
140
+ if (!started.ok) {
141
+ if (started.status === 409 || started.status === 202)
142
+ return "provisioning";
143
+ return boatError(started.status, started.body);
144
+ }
145
+ if (!(started.body?.running === true || started.body?.state === "running"))
146
+ return "provisioning";
147
+ }
148
+ if (!toolsReady.has(machineId)) {
149
+ const tools = await exec(token, machineId, SCREEN_TOOLS, 300, signal);
150
+ if (tools.ok && tools.body?.exitCode === 0)
151
+ toolsReady.add(machineId);
152
+ }
153
+ return "ready";
154
+ }
155
+ /** POST /sandboxes/{id}/commands with detached: the process writes its output and exit code under /tmp/limix-bg. */
156
+ async function startDetached(token, machineId, command, signal) {
157
+ const processId = Math.floor(Date.now() / 1000) * 1000 + Math.floor(Math.random() * 1000);
158
+ const base = `${BG_DIR}/${processId}`;
159
+ const script = `mkdir -p ${BG_DIR}; ( bash -lc ${shellQuote(command)} > ${base}.out 2> ${base}.err; echo $? > ${base}.exit ) </dev/null >/dev/null 2>&1 & echo started`;
160
+ const res = await exec(token, machineId, script, 30, signal);
161
+ if (!res.ok)
162
+ return boatError(res.status, res.body);
163
+ if (!/started/.test(String(res.body?.stdout ?? "")))
164
+ return json(502, { ok: false, message: "Instabox did not start the command" });
165
+ return json(200, { processId });
166
+ }
167
+ async function readDetached(token, machineId, processId, tailBytes, signal) {
168
+ if (!/^\d{1,16}$/.test(processId))
169
+ return json(404, { ok: false, message: "unknown process" });
170
+ const base = `${BG_DIR}/${processId}`;
171
+ const n = Math.max(1, Math.min(1_000_000, tailBytes));
172
+ const script = `[ -e ${base}.out ] || { echo GONE; exit 0; }; printf 'EXIT:%s\\n' "$(cat ${base}.exit 2>/dev/null)";`
173
+ + ` printf 'OUT:'; tail -c ${n} ${base}.out | base64 -w0; printf '\\nERR:'; tail -c ${n} ${base}.err | base64 -w0; echo`;
174
+ const res = await exec(token, machineId, script, 30, signal);
175
+ if (!res.ok)
176
+ return boatError(res.status, res.body);
177
+ const out = String(res.body?.stdout ?? "");
178
+ if (out.startsWith("GONE"))
179
+ return json(404, { ok: false, message: "unknown process" });
180
+ const field = (key) => out.match(new RegExp(`^${key}:(.*)$`, "m"))?.[1]?.trim() ?? "";
181
+ const exit = field("EXIT");
182
+ const decode = (b64) => Buffer.from(b64, "base64").toString("utf8");
183
+ const running = exit === "";
184
+ return json(200, {
185
+ running,
186
+ exitCode: running ? null : Number.parseInt(exit, 10),
187
+ stdout: decode(field("OUT")),
188
+ stderr: decode(field("ERR")),
189
+ });
190
+ }
191
+ /** Boat's request (path relative to its API, with its method, headers and JSON body) answered by Instabox. */
192
+ export async function instaboxFetch(rawToken, path, opts = {}) {
193
+ const token = rawToken.trim();
194
+ const method = (opts.method ?? "GET").toUpperCase();
195
+ const signal = opts.signal;
196
+ const url = new URL(path, "http://boat.invalid");
197
+ const parts = url.pathname.split("/").filter(Boolean);
198
+ const reqHeaders = new Headers(opts.headers);
199
+ let reqBody = null;
200
+ if (typeof opts.body === "string" && opts.body) {
201
+ try {
202
+ reqBody = JSON.parse(opts.body);
203
+ }
204
+ catch {
205
+ reqBody = null;
206
+ }
207
+ }
208
+ if (parts[0] === "deletion-operations" && parts.length === 2 && method === "GET") {
209
+ const known = deletionOps.get(parts[1]);
210
+ if (!known)
211
+ return json(404, { ok: false, message: "unknown deletion operation" });
212
+ const op = await ib(token, `/v1/operations/${known.opId}`, { signal });
213
+ if (op.status === 404)
214
+ return json(200, { operation: { id: parts[1], kind: "sandbox", targetId: known.machineId, status: "completed", stage: "completed" } });
215
+ if (!op.ok)
216
+ return boatError(op.status, op.body);
217
+ const status = deletionStatus(op.body?.state);
218
+ return json(200, { operation: { id: parts[1], kind: "sandbox", targetId: known.machineId, status, stage: status === "completed" ? "completed" : status === "blocked" ? "retrying" : "removing" } });
219
+ }
220
+ if (parts[0] !== "sandboxes")
221
+ return json(404, { ok: false, message: `Instabox has no equivalent of ${path}` });
222
+ if (parts.length === 1) {
223
+ if (method === "GET") {
224
+ const q = new URLSearchParams();
225
+ const limit = url.searchParams.get("limit");
226
+ const cursor = url.searchParams.get("cursor");
227
+ if (limit)
228
+ q.set("limit", String(Math.min(100, Number(limit) || 100)));
229
+ if (cursor)
230
+ q.set("cursor", cursor);
231
+ const listed = await ib(token, `/v1/machines${q.toString() ? `?${q}` : ""}`, { signal });
232
+ if (!listed.ok)
233
+ return boatError(listed.status, listed.body);
234
+ const machines = Array.isArray(listed.body?.machines) ? listed.body.machines : [];
235
+ return json(200, { sandboxes: machines.map(sandboxFrom), pageInfo: { nextCursor: listed.body?.nextCursor ?? null } });
236
+ }
237
+ if (method === "POST") {
238
+ const name = typeof reqBody?.name === "string" ? reqBody.name : undefined;
239
+ const existing = name ? await adoptable(token, signal) : null;
240
+ if (existing && name) {
241
+ adopt(existing.id, name);
242
+ return json(201, { sandbox: sandboxFrom(existing) });
243
+ }
244
+ const key = reqHeaders.get("Idempotency-Key");
245
+ let created;
246
+ for (const templateId of INSTABOX_TEMPLATES) {
247
+ created = await ib(token, "/v1/machines", {
248
+ method: "POST",
249
+ idempotent: true,
250
+ headers: key ? { "Idempotency-Key": `${key}-${templateId}` } : {},
251
+ body: { ...(name ? { name } : {}), templateId, autoSuspendAfterSeconds: IDLE_SUSPEND_SECONDS, desiredState: "running" },
252
+ signal,
253
+ });
254
+ if (created.body?.error?.code !== "template_not_allowed")
255
+ break;
256
+ }
257
+ if (!created.ok) {
258
+ if (created.status === 409 && created.body?.error?.code === "idempotency_in_progress")
259
+ return json(409, { ok: false, error: { code: "idempotency_in_progress" } });
260
+ return boatError(created.status, created.body);
261
+ }
262
+ const machineId = created.body?.machineId ?? created.body?.id;
263
+ if (typeof machineId !== "string")
264
+ return json(502, { ok: false, message: "Instabox did not say which machine it created" });
265
+ return json(201, { sandbox: { id: machineId, name: name ?? "", state: "provisioning", desktopAvailable: true } });
266
+ }
267
+ }
268
+ const machineId = parts[1];
269
+ if (!/^m_[a-z0-9]{20,40}$/.test(machineId ?? ""))
270
+ return json(404, { ok: false, message: "not an Instabox machine id" });
271
+ if (parts.length === 2) {
272
+ if (method === "GET") {
273
+ const got = await ib(token, `/v1/machines/${machineId}`, { signal });
274
+ if (!got.ok)
275
+ return got.status === 404 ? json(404, { ok: false, message: "not found" }) : boatError(got.status, got.body);
276
+ return json(200, { sandbox: sandboxFrom(got.body) });
277
+ }
278
+ if (method === "PATCH") {
279
+ // The name was given at create; Instabox names cannot change afterwards.
280
+ const got = await ib(token, `/v1/machines/${machineId}`, { signal });
281
+ if (!got.ok)
282
+ return boatError(got.status, got.body);
283
+ if (typeof reqBody?.name === "string" && sandboxFrom(got.body).name !== reqBody.name) {
284
+ return json(409, { ok: false, message: "Instabox cannot rename a machine; delete it and let Limix Bot create a new one" });
285
+ }
286
+ return json(200, { sandbox: sandboxFrom(got.body) });
287
+ }
288
+ if (method === "DELETE") {
289
+ const removed = await ib(token, `/v1/machines/${machineId}`, { method: "DELETE", idempotent: true, headers: { "X-Confirm-Delete": machineId }, signal });
290
+ if (removed.status === 404 || removed.status === 410)
291
+ return json(404, { ok: false, message: "not found" });
292
+ if (!removed.ok)
293
+ return boatError(removed.status, removed.body);
294
+ const opId = typeof removed.body?.id === "string" ? removed.body.id : null;
295
+ if (!opId)
296
+ return json(202, { operation: { id: deletionOperationId(machineId), kind: "sandbox", targetId: machineId, status: "completed", stage: "completed" } });
297
+ const id = deletionOperationId(opId);
298
+ deletionOps.set(id, { opId, machineId });
299
+ const status = deletionStatus(removed.body?.state);
300
+ return json(202, { operation: { id, kind: "sandbox", targetId: machineId, status, stage: status === "completed" ? "completed" : "removing" } });
301
+ }
302
+ }
303
+ const action = parts.slice(2).join("/");
304
+ if (method === "POST" && (action === "resume" || action === "stop")) {
305
+ let verb = action === "stop" ? "stop" : "start";
306
+ if (action === "resume") {
307
+ const got = await ib(token, `/v1/machines/${machineId}`, { signal });
308
+ if (got.ok && got.body?.state === "suspended")
309
+ verb = "resume";
310
+ }
311
+ const res = await ib(token, `/v1/machines/${machineId}/${verb}`, { method: "POST", idempotent: true, body: {}, signal });
312
+ if (!res.ok)
313
+ return res.status === 409 ? json(409, { ok: false, message: "busy" }) : boatError(res.status, res.body);
314
+ return json(202, { ok: true });
315
+ }
316
+ if (method === "POST" && action === "desktop") {
317
+ const started = await startDesktop(token, machineId, signal);
318
+ if (started instanceof Response)
319
+ return started;
320
+ if (started === "provisioning")
321
+ return json(200, { provisioning: true });
322
+ const stream = await ib(token, `/v1/machines/${machineId}/desktop/stream`, {
323
+ method: "POST", body: { viewOnly: false, ttlSeconds: STREAM_TTL_SECONDS }, signal,
324
+ });
325
+ if (!stream.ok)
326
+ return boatError(stream.status, stream.body);
327
+ return json(200, { desktopUrl: typeof stream.body?.url === "string" ? stream.body.url : null });
328
+ }
329
+ if (method === "POST" && action === "commands") {
330
+ const command = typeof reqBody?.command === "string" ? reqBody.command : "";
331
+ if (reqBody?.detached === true)
332
+ return startDetached(token, machineId, command, signal);
333
+ const res = await exec(token, machineId, command, EXEC_MAX_SECONDS, signal);
334
+ if (!res.ok)
335
+ return boatError(res.status, res.body);
336
+ return json(200, { exitCode: res.body?.exitCode ?? null, stdout: res.body?.stdout ?? "", stderr: res.body?.stderr ?? "", ...(res.body?.timedOut ? { timedOut: true } : {}) });
337
+ }
338
+ if (method === "GET" && parts[2] === "commands" && parts.length === 4) {
339
+ return readDetached(token, machineId, parts[3], Number(url.searchParams.get("tailBytes") ?? 4096), signal);
340
+ }
341
+ if (method === "GET" && (action === "artifacts" || action === "files")) {
342
+ const filePath = url.searchParams.get("path") ?? "";
343
+ if (!filePath.startsWith("/"))
344
+ return json(400, { ok: false, message: "absolute path required" });
345
+ const direct = await ib(token, `/v1/machines/${machineId}/files/content?path=${encodeURIComponent(filePath)}`, { signal });
346
+ if (direct.ok && !direct.raw.headers.get("content-type")?.includes("json")) {
347
+ const bytes = Buffer.from(await direct.raw.arrayBuffer());
348
+ if (action === "files")
349
+ return json(200, { content: bytes.toString("base64") });
350
+ return new Response(new Uint8Array(bytes), { status: 200, headers: { "content-type": "application/octet-stream" } });
351
+ }
352
+ const res = await exec(token, machineId, `base64 -w0 ${shellQuote(filePath)}`, 60, signal);
353
+ if (!res.ok)
354
+ return boatError(res.status, res.body);
355
+ if (res.body?.exitCode !== 0)
356
+ return json(404, { ok: false, message: "file not found" });
357
+ const content = String(res.body?.stdout ?? "").trim();
358
+ if (action === "files")
359
+ return json(200, { content });
360
+ return new Response(new Uint8Array(Buffer.from(content, "base64")), { status: 200, headers: { "content-type": "application/octet-stream" } });
361
+ }
362
+ if (method === "POST" && (action === "files/download-url" || action === "files/upload-url")) {
363
+ return json(501, { ok: false, code: "not_available", error: { code: "not_available" } });
364
+ }
365
+ return json(404, { ok: false, message: `Instabox has no equivalent of ${method} ${url.pathname}` });
366
+ }
367
+ /** Instabox checks a key with GET /v1/me. */
368
+ export async function verifyInstaboxKey(rawToken) {
369
+ const token = rawToken.trim();
370
+ try {
371
+ const res = await fetch(`${INSTABOX_API}/v1/me`, { headers: { "X-API-Key": token }, signal: AbortSignal.timeout(20_000) });
372
+ if (res.ok)
373
+ return { ok: true };
374
+ if (res.status === 401 || res.status === 403) {
375
+ return { ok: false, rejected: true, message: "Instabox rejected that key — it may have been revoked. Create a new one at instabox.sh with the machines scopes." };
376
+ }
377
+ return { ok: false, message: `Instabox returned ${res.status} for that key — try again in a moment.` };
378
+ }
379
+ catch {
380
+ return { ok: false, message: "Couldn't reach Instabox to check that key — check your connection and retry." };
381
+ }
382
+ }
@@ -0,0 +1,51 @@
1
+ // Limix Bot: the bot's owner may talk to it from Telegram / Zalo / Discord with any AI, not only the ones that can run a
2
+ // confined guest turn. The connector (limix-gateway) sends X-Limix-Owner only for a sender the owner linked with /link,
3
+ // in a private chat; the secret is made per launch by the desktop app and never reaches an engine (deleted below,
4
+ // before any engine is started). An owner thread stays an owner thread: a guest's call can never continue it.
5
+ import { timingSafeEqual } from "node:crypto";
6
+ import { readFileSync, writeFileSync } from "node:fs";
7
+ import { join } from "node:path";
8
+ import { DATA_DIR } from "./config.js";
9
+ const TOKEN = process.env.LIMIX_OWNER_LANE_TOKEN?.trim() ?? "";
10
+ delete process.env.LIMIX_OWNER_LANE_TOKEN;
11
+ export const LIMIX_OWNER_HEADER = "x-limix-owner";
12
+ export function limixOwnerCall(header) {
13
+ if (!/^[A-Za-z0-9_-]{43}$/.test(TOKEN) || typeof header !== "string")
14
+ return false;
15
+ const a = Buffer.from(header), b = Buffer.from(TOKEN);
16
+ return a.length === b.length && timingSafeEqual(a, b);
17
+ }
18
+ const MAX_THREADS = 5_000;
19
+ let threads = null;
20
+ const file = () => join(DATA_DIR, "limix-owner-threads.json");
21
+ function loaded() {
22
+ if (threads)
23
+ return threads;
24
+ threads = new Set();
25
+ try {
26
+ const saved = JSON.parse(readFileSync(file(), "utf8"));
27
+ if (Array.isArray(saved))
28
+ for (const id of saved)
29
+ if (typeof id === "string")
30
+ threads.add(id);
31
+ }
32
+ catch {
33
+ // No owner threads yet.
34
+ }
35
+ return threads;
36
+ }
37
+ export function isLimixOwnerThread(threadId) {
38
+ return loaded().has(threadId);
39
+ }
40
+ export function markLimixOwnerThread(threadId) {
41
+ const all = loaded();
42
+ if (all.has(threadId))
43
+ return;
44
+ all.add(threadId);
45
+ try {
46
+ writeFileSync(file(), JSON.stringify([...all].slice(-MAX_THREADS)));
47
+ }
48
+ catch {
49
+ // Kept in memory for this launch.
50
+ }
51
+ }
@@ -4,13 +4,13 @@
4
4
  // still reads it; this payload is what lets the desktop draw its buttons.
5
5
  /** The waiting row's words, for clients that print the tool name. */
6
6
  export function firstReplyWaitingName(botName, ai) {
7
- return `notice: ${botName} hasn't heard back from ${ai} yet.`;
7
+ return `notice: ${botName} vẫn chưa nhận được phản hồi từ ${ai}.`;
8
8
  }
9
9
  /** The same row once its turn ended without an answer: history, true as it stands. */
10
10
  export function firstReplyOverName(botName, ai) {
11
- return `notice: ${botName} didn't hear back from ${ai}.`;
11
+ return `notice: ${botName} không nhận được phản hồi từ ${ai}.`;
12
12
  }
13
13
  /** The same row once the AI's first output arrived. */
14
14
  export function firstReplySettledName(ai) {
15
- return `notice: ${ai} answered after a slow start.`;
15
+ return `notice: ${ai} đã trả lời, sau một lúc chậm.`;
16
16
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "limixbot",
3
- "version": "0.3.4",
3
+ "version": "0.3.12",
4
4
  "description": "Run the Limix Bot server anywhere and pair your devices to it (Leox ৹ Limix AI)",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",