limixbot 0.3.4 → 0.3.11
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.
- package/dist/assets/{ActivityPanel-BG18nJiz.js → ActivityPanel-BODxWmfO.js} +1 -1
- package/dist/assets/{App-8heymKKb.js → App-DPG1sq1r.js} +40 -40
- package/dist/assets/{AppEventEditor-D5dGKlRO.js → AppEventEditor-QyHQdp7V.js} +1 -1
- package/dist/assets/{BotSettingsDialog-Ct_Yktk8.js → BotSettingsDialog-BCNOcqOS.js} +1 -1
- package/dist/assets/{BrowserPanel-Cyws1YTJ.js → BrowserPanel-BqPnj2Bz.js} +1 -1
- package/dist/assets/{BrowserProfilesManager-BEUgS3Me.js → BrowserProfilesManager-BMk7i2tc.js} +1 -1
- package/dist/assets/{CloudAddDialog-UEPtzvka.js → CloudAddDialog-S7ngrMRc.js} +1 -1
- package/dist/assets/{ComputerFilesPane-BGywdHPB.js → ComputerFilesPane-BWmAv8JT.js} +1 -1
- package/dist/assets/{ComputerPanel-DLV2INTM.js → ComputerPanel-BDPrbwmC.js} +2 -2
- package/dist/assets/{DataPanel-CIvvbY5b.js → DataPanel-BgfqxYo9.js} +1 -1
- package/dist/assets/{DesktopViewer-BNIgo9Nm.js → DesktopViewer-BMD84CFj.js} +1 -1
- package/dist/assets/{FilesView-Cd5kwljm.js → FilesView-CGYpajry.js} +1 -1
- package/dist/assets/{GrokMoveDialog-B2Vgd-vB.js → GrokMoveDialog-B3aEa4hb.js} +1 -1
- package/dist/assets/{InspectorPanel-Bxm2bVHy.js → InspectorPanel-CTNcZYHr.js} +1 -1
- package/dist/assets/{KeyboardShortcutsModal-Dfcfy-EA.js → KeyboardShortcutsModal-CBmbtwhH.js} +1 -1
- package/dist/assets/{LiveDesktop-fbnBEFRL.js → LiveDesktop-7h7Mxr41.js} +1 -1
- package/dist/assets/{LocalVmWorkspace-CBe48xft.js → LocalVmWorkspace-Bxvn8CQX.js} +1 -1
- package/dist/assets/{MacLocalControl-CpVqwJts.js → MacLocalControl-C3rHusSl.js} +1 -1
- package/dist/assets/{MarkdownFilePreview--PNcCEvm.js → MarkdownFilePreview-_1MC5Db0.js} +1 -1
- package/dist/assets/{MenuMotion-IQitGKH4.js → MenuMotion-C1J3xC0E.js} +1 -1
- package/dist/assets/{NewBotDialog-CZbVbamH.js → NewBotDialog-C35vqYL4.js} +1 -1
- package/dist/assets/{PhoneAppDialog-B9oozUp0.js → PhoneAppDialog-DhEEfxbl.js} +1 -1
- package/dist/assets/{PlacesRail-v1XIqiBG.js → PlacesRail-DnedL3jM.js} +2 -2
- package/dist/assets/{PluginsPanel-Bj3Pn0Iw.js → PluginsPanel-CvZhoTE-.js} +1 -1
- package/dist/assets/{ProIntroduction-qFeFSzyk.js → ProIntroduction-B1QqcZmr.js} +1 -1
- package/dist/assets/{RemoteAgentSettingsPanel-CcbUTgTg.js → RemoteAgentSettingsPanel-DU6QyjIY.js} +1 -1
- package/dist/assets/{RoomPanel-k0hQwspJ.js → RoomPanel-Om9r5yMG.js} +1 -1
- package/dist/assets/{RoutinesPage-aANJQ--g.js → RoutinesPage-BWJ5Z1Sk.js} +1 -1
- package/dist/assets/SettingsModal-DmFTwQIH.js +11 -0
- package/dist/assets/{ShareTeamDialog-DeRaL6zM.js → ShareTeamDialog-DkKdhm01.js} +1 -1
- package/dist/assets/{SignedInSites-CfhTex9p.js → SignedInSites-Dxnqcjlx.js} +1 -1
- package/dist/assets/{TeamLibraryPanel-C7LZ7Onp.js → TeamLibraryPanel-C3QFOJmQ.js} +1 -1
- package/dist/assets/{TeamMapPage-BSvk8tOC.js → TeamMapPage-CpPh11mE.js} +1 -1
- package/dist/assets/{TriggersPage-BdePfInC.js → TriggersPage-YjklCz2p.js} +1 -1
- package/dist/assets/{VisibilitySection-CCwpvTlJ.js → VisibilitySection-DiD7SczA.js} +6 -6
- package/dist/assets/{WebhookTrigger-wqj5vki5.js → WebhookTrigger-w_Pjci5K.js} +1 -1
- package/dist/assets/{WorksOnGrid-CenOXsZe.js → WorksOnGrid-B84wDbPo.js} +1 -1
- package/dist/assets/{abnfDiagram-YUKMZFIV-CBzJU2O6.js → abnfDiagram-YUKMZFIV-Dr-8nZOt.js} +1 -1
- package/dist/assets/{activity-bd7MWK8m.js → activity-bTJO9mSI.js} +1 -1
- package/dist/assets/{architectureDiagram-47P4ROYG-DVR1DP3v.js → architectureDiagram-47P4ROYG-23P1y-s5.js} +1 -1
- package/dist/assets/{blockDiagram-E7TT5TSB-BYqUHhoh.js → blockDiagram-E7TT5TSB-D1mBvWuR.js} +1 -1
- package/dist/assets/{c4Diagram-CRA5TL53-BtQERHwr.js → c4Diagram-CRA5TL53-BKK5iCAq.js} +1 -1
- package/dist/assets/{calendar-days-YMHxUvlg.js → calendar-days-Ckb7s5k5.js} +1 -1
- package/dist/assets/channel-_uzpXF9W.js +1 -0
- package/dist/assets/{chunk-2Q5K7J3B-Cwf_xeHP.js → chunk-2Q5K7J3B-C6ElI8Gj.js} +1 -1
- package/dist/assets/{chunk-5VM5RSS4-Doo-zxI4.js → chunk-5VM5RSS4-B9VKa5BI.js} +1 -1
- package/dist/assets/{chunk-7TKQ45FW-CD2AgrRj.js → chunk-7TKQ45FW-CrgJv6B8.js} +1 -1
- package/dist/assets/{chunk-AW2ZBBNX-e5OuWeq_.js → chunk-AW2ZBBNX-CVe28FV5.js} +1 -1
- package/dist/assets/{chunk-FVRAUYC3-C8jo43it.js → chunk-FVRAUYC3-BLPrVPPQ.js} +2 -2
- package/dist/assets/{chunk-HTAEGDNF-DLsD4agj.js → chunk-HTAEGDNF-BB9kWhE5.js} +1 -1
- package/dist/assets/{chunk-JWPE2WC7-UZDUbeTy.js → chunk-JWPE2WC7-y2b0u5g-.js} +1 -1
- package/dist/assets/{chunk-KQW6MTUR-DR9SuVX3.js → chunk-KQW6MTUR-BMp5NWQw.js} +1 -1
- package/dist/assets/{chunk-XXDRQBXY-DwEl4815.js → chunk-XXDRQBXY-gUdzE10_.js} +1 -1
- package/dist/assets/{classDiagram-v2-K4WV4PDN-CQZho8e0.js → classDiagram-v2-K4WV4PDN-DNfrg4LR.js} +1 -1
- package/dist/assets/{coins-DpzGN3sd.js → coins-CBbaS4z-.js} +1 -1
- package/dist/assets/{core-C_G-J8y-.js → core-BNIINisk.js} +1 -1
- package/dist/assets/{cose-bilkent-JH36ORCC-DUZ9OWzu.js → cose-bilkent-JH36ORCC-D84YhUzh.js} +1 -1
- package/dist/assets/{cynefin-EF2NZ3EQ-DVHnJdjN.js → cynefin-EF2NZ3EQ-DMBZCt-X.js} +1 -1
- package/dist/assets/{cynefinDiagram-3GCD6N5R-dYhbIkRt.js → cynefinDiagram-3GCD6N5R-BZhcnyNF.js} +1 -1
- package/dist/assets/{dagre-W4DXFKR2-DqJNpw5w.js → dagre-W4DXFKR2-DY68eKcA.js} +1 -1
- package/dist/assets/{diagram-2UJZ2QOL-DKNDqAnE.js → diagram-2UJZ2QOL-Bv-0-qqw.js} +1 -1
- package/dist/assets/{diagram-OVF4WLC6-DENMVM-d.js → diagram-OVF4WLC6-CEnRLa7p.js} +1 -1
- package/dist/assets/{diagram-PFPMY2P6-CrRFeR5V.js → diagram-PFPMY2P6-D3UjLOE9.js} +1 -1
- package/dist/assets/{diagram-UMYRVEAY-B5w-desO.js → diagram-UMYRVEAY-CJCYH8n7.js} +1 -1
- package/dist/assets/{diagram-YEKJPTXX-C7AVX3MB.js → diagram-YEKJPTXX-D_YEp1XW.js} +1 -1
- package/dist/assets/{diagram-ZIFT7M5P-BgkTRwwr.js → diagram-ZIFT7M5P-C7RgDQ_I.js} +1 -1
- package/dist/assets/{ebnfDiagram-VR2GEFS7-yY_2KNsK.js → ebnfDiagram-VR2GEFS7-CVbikCfA.js} +1 -1
- package/dist/assets/{elk-IJKZMXRS-xWtfkoqT.js → elk-IJKZMXRS-BQv73EB6.js} +1 -1
- package/dist/assets/{erDiagram-O2IAPWRE-BMM4saPI.js → erDiagram-O2IAPWRE-ZMuLXVBq.js} +1 -1
- package/dist/assets/{file-up-Bl7ViNTL.js → file-up-BsMMeeDy.js} +1 -1
- package/dist/assets/{flowDiagram-OXPTDLAJ-BCRe8i6T.js → flowDiagram-OXPTDLAJ-CkF5DDZj.js} +1 -1
- package/dist/assets/{ganttDiagram-R7TSEDQI-COBrcDi7.js → ganttDiagram-R7TSEDQI-BaYEK3at.js} +1 -1
- package/dist/assets/{gitGraphDiagram-XJZIOB7I-Dc4RKydD.js → gitGraphDiagram-XJZIOB7I-eVpEz6un.js} +1 -1
- package/dist/assets/{import-name-CKE76uoY.js → import-name-DzZIJafj.js} +1 -1
- package/dist/assets/{index-jlMtdqfJ.js → index-BSIcl6wl.js} +1 -1
- package/dist/assets/{index-BBsdMDZV.js → index-BkcZ5EE3.js} +2 -2
- package/dist/assets/{index-BXLNny-w.js → index-DvRd5uj9.js} +1 -1
- package/dist/assets/{infoDiagram-5W2HQ5XZ-BV8RRI_4.js → infoDiagram-5W2HQ5XZ-cLDLLbyG.js} +1 -1
- package/dist/assets/{ishikawaDiagram-K3B6WC7H-H-XILdGm.js → ishikawaDiagram-K3B6WC7H-Dxie_tdO.js} +1 -1
- package/dist/assets/{journeyDiagram-COXZFDF6-ChLduKEj.js → journeyDiagram-COXZFDF6-DWzHn4oz.js} +1 -1
- package/dist/assets/{kanban-definition-P3RFRI5V-FSZ9F3JH.js → kanban-definition-P3RFRI5V-BFNqkDTK.js} +1 -1
- package/dist/assets/{mcp-servers-DGQioE-C.js → mcp-servers-DJceaBjT.js} +1 -1
- package/dist/assets/{mermaid.core-BRsJ02Pz.js → mermaid.core-Cn7buzY3.js} +4 -4
- package/dist/assets/{mindmap-definition-LZFPQGKD-TWHzZjNT.js → mindmap-definition-LZFPQGKD-DVS-3JPN.js} +1 -1
- package/dist/assets/{minimize-BYscOtPM.js → minimize-BpyGl78C.js} +1 -1
- package/dist/assets/{monitor-DFEzCfQk.js → monitor-FKQbSjca.js} +1 -1
- package/dist/assets/{new-bot-7IqfI7S4.js → new-bot-Df7cGS3Z.js} +1 -1
- package/dist/assets/{org-library-BW4CqVd6.js → org-library-CT3HZbof.js} +1 -1
- package/dist/assets/{package-format-BOH5_nvi.js → package-format-CgnYlqV5.js} +1 -1
- package/dist/assets/{pegDiagram-BGZESJAR-LQWOF7IE.js → pegDiagram-BGZESJAR-qwRUMAPC.js} +1 -1
- package/dist/assets/{pieDiagram-CAPJLFHJ-BRXp4qFw.js → pieDiagram-CAPJLFHJ-CTzsGZac.js} +1 -1
- package/dist/assets/{quadrantDiagram-GDMTTRAM-DyIdGBZo.js → quadrantDiagram-GDMTTRAM-B7Mx793h.js} +1 -1
- package/dist/assets/{rail-Du7tlPwj.js → rail-B_BFwBxk.js} +1 -1
- package/dist/assets/{railroadDiagram-SM67HX2B-BrL1MhW1.js → railroadDiagram-SM67HX2B-zMy3LSMK.js} +1 -1
- package/dist/assets/{remote-desktop-panel-B9rqQRMc.js → remote-desktop-panel-DPutXzoI.js} +1 -1
- package/dist/assets/{requirementDiagram-X7JNWC4A-DTYjaC8j.js → requirementDiagram-X7JNWC4A-Dm_K18IR.js} +1 -1
- package/dist/assets/{sankeyDiagram-UM26HJRW-BR9dASO5.js → sankeyDiagram-UM26HJRW-Dx_FP9cL.js} +1 -1
- package/dist/assets/{sequenceDiagram-ZO4K6R2Y-BR1k8h2_.js → sequenceDiagram-ZO4K6R2Y-D3tIz4Te.js} +1 -1
- package/dist/assets/{sizeCapture-INFHLROL-u8zOvZVi.js → sizeCapture-INFHLROL-CwiX8-_Z.js} +1 -1
- package/dist/assets/{stateDiagram-v2-TBUQTH76-mQ64BAFv.js → stateDiagram-v2-TBUQTH76-Biv0Rjec.js} +1 -1
- package/dist/assets/{styles-BlayaJti.js → styles-Cm26P6hN.js} +3 -3
- package/dist/assets/{swimlanes-N4OXWK64-CmzC3Mu0.js → swimlanes-N4OXWK64-Daie2jEh.js} +1 -1
- package/dist/assets/swimlanesDiagram-K3J5GTZL-EP8Nb7CO.js +8 -0
- package/dist/assets/{team-share-C3w1L3WL.js → team-share-DyK3j6N2.js} +1 -1
- package/dist/assets/{timeline-definition-YOQAKGHF-9aPCRsK2.js → timeline-definition-YOQAKGHF-_OnzqB6-.js} +1 -1
- package/dist/assets/{usecaseDiagram-VIAY4XPW--ngAouwr.js → usecaseDiagram-VIAY4XPW-CITmAUqx.js} +1 -1
- package/dist/assets/{vennDiagram-BLWOH2XV-C3zKxuAB.js → vennDiagram-BLWOH2XV-BAx_UxlU.js} +1 -1
- package/dist/assets/{vi-uLErku_e.js → vi-GKa1-xWX.js} +2 -2
- package/dist/assets/{wardleyDiagram-YMQ3BMBF-CZWsQPSF.js → wardleyDiagram-YMQ3BMBF-DjWzxLzl.js} +1 -1
- package/dist/assets/{xychartDiagram-TAQBALBS-BTcsDP6v.js → xychartDiagram-TAQBALBS-DpQbiSMS.js} +1 -1
- package/dist/index.html +2 -2
- package/dist/rail.html +4 -4
- package/dist-server/index.js +2456 -2424
- package/dist-server/package.json +1 -1
- package/dist-server/server/boat-create-idempotency.js +1 -1
- package/dist-server/server/boat-delete-journal.js +1 -1
- package/dist-server/server/boat.js +10 -3
- package/dist-server/server/bot-api-host.js +9 -5
- package/dist-server/server/index.js +2 -1
- package/dist-server/server/limix-instabox.js +382 -0
- package/dist-server/server/limix-owner-lane.js +51 -0
- package/dist-server/shared/first-reply-rescue.js +3 -3
- package/package.json +1 -1
- package/dist/assets/SettingsModal-t10M3Ahi.js +0 -11
- package/dist/assets/channel-D6e78p3W.js +0 -1
- package/dist/assets/swimlanesDiagram-K3J5GTZL-Coei7rJr.js +0 -8
package/dist-server/package.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"type":"module","version":"0.3.
|
|
1
|
+
{"type":"module","version":"0.3.11"}
|
|
@@ -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
|
|
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
|
-
|
|
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}
|
|
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}
|
|
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}
|
|
15
|
+
return `notice: ${ai} đã trả lời, sau một lúc chậm.`;
|
|
16
16
|
}
|