nolo-cli 0.148.0-alpha.1 → 0.151.0-alpha.2
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/{_executeModel-NFJAMAGJ.js → _executeModel-XHAFJ2MM.js} +134 -91
- package/accountResolver-7RYB4D2P.js +300 -0
- package/action-SXJD5N5R.js +23 -0
- package/{addContentAction-B2DRF7P6.js → addContentAction-ZV774PHH.js} +20 -14
- package/{addDialogAgentAction-2P2FLTLD.js → addDialogAgentAction-A6SMKAGH.js} +26 -22
- package/{agentDeleteCommand-7IDWF6E3.js → agentDeleteCommand-DPUUS56Z.js} +181 -93
- package/{agentEmailCommands-4KW33JII.js → agentEmailCommands-V4P5EFLP.js} +132 -112
- package/{agentGrantCommands-M3XFO6EN.js → agentGrantCommands-HOJ3JBBQ.js} +3 -3
- package/agentListCommands-TWFLUYIZ.js +802 -0
- package/{agentMachineCommands-CMQR4RQ4.js → agentMachineCommands-MELPAZZI.js} +58 -62
- package/{agentProviderCommands-YK2BIIXZ.js → agentProviderCommands-HL6MBLCL.js} +149 -2
- package/{agentPullCommand-HF4A42D4.js → agentPullCommand-FA43RBRU.js} +7 -7
- package/agentRecordCommands-FSWSSQIO.js +154 -0
- package/agentRunCommand-GDBJVOAT.js +183 -0
- package/{agentRunControl-2SKCR4SB.js → agentRunControl-IFM7IGMG.js} +10 -2
- package/{agentRuntimeLocal-AK2QVJHY.js → agentRuntimeLocal-KKSQJMTE.js} +47 -50
- package/{agentSlice-OZ33K224.js → agentSlice-YO2ZLJMD.js} +23 -16
- package/{appDeleteCommands-N3XEVVYC.js → appDeleteCommands-4PL4KYRN.js} +58 -59
- package/{appDeployCommands-AWFIXYVW.js → appDeployCommands-EEEXCEB6.js} +67 -63
- package/{appDeployStatusCommands-CZRON2FO.js → appDeployStatusCommands-35ZX7T66.js} +56 -56
- package/{appGetCommands-XQIUY4TD.js → appGetCommands-WFCHXJWF.js} +56 -56
- package/{appListCommands-NNG4H36Y.js → appListCommands-XY2U6DPF.js} +57 -59
- package/{authCommand-UBJ4EWYZ.js → authCommand-QYN47CQD.js} +371 -68
- package/{authCommands-H6WKKCRW.js → authCommands-77CHYBGF.js} +12 -9
- package/{builtinSkillRegistry-ZMF6YGNO.js → builtinSkillRegistry-TDQ6JB2E.js} +13 -11
- package/{chromeCommands-2N6MEWRU.js → chromeCommands-OBBNJCLS.js} +314 -13
- package/chromeOpsCommands-3Z34RKSE.js +472 -0
- package/{chunk-ALXHV7NK.js → chunk-2GGXHQIW.js} +43 -4
- package/{localRuntimeAuthority-5W6IJRAD.js → chunk-2IXDJNZI.js} +154 -75
- package/{chunk-IVZJWTDI.js → chunk-36OMEXXR.js} +89 -21
- package/{chunk-H6A6X4EK.js → chunk-3MZAGMYY.js} +16 -0
- package/chunk-3PVYUPXK.js +40 -0
- package/chunk-3SZA4U64.js +1192 -0
- package/{chunk-GNMAFAIS.js → chunk-44WXLHFI.js} +1 -1
- package/{chunk-AGGIDPCZ.js → chunk-4JH2UMJR.js} +6 -0
- package/chunk-4LCOCOR5.js +50 -0
- package/chunk-4YRTZIVN.js +871 -0
- package/chunk-556RW72X.js +14110 -0
- package/{chunk-N7J63Q6C.js → chunk-56GBUMO5.js} +10 -1
- package/{chunk-W7XHHYWM.js → chunk-5BHQICF2.js} +5 -1
- package/{chunk-XUWRURDF.js → chunk-5EBLANFT.js} +1 -1
- package/{chunk-3AH7LNUB.js → chunk-5EMSBTQ6.js} +33 -14
- package/{chunk-YRRASA5Q.js → chunk-5MM7NNO6.js} +113 -52
- package/chunk-5MMAK75Q.js +15 -0
- package/{chunk-M5PVFLD4.js → chunk-64K7WX26.js} +90 -21
- package/{chunk-AKG5W4LG.js → chunk-6CCFTVBP.js} +76 -221
- package/chunk-6MJNEXUU.js +614 -0
- package/{chunk-62IL3KQV.js → chunk-6MXDOUMH.js} +15 -3
- package/{chunk-QOKAKDPE.js → chunk-6Q7UTJKR.js} +6 -94
- package/chunk-6QDYSCPW.js +223 -0
- package/{chunk-HYZIYH2H.js → chunk-74RAWNQN.js} +56 -63
- package/chunk-7DPNBQSV.js +9 -0
- package/chunk-7MOXKPHC.js +119 -0
- package/{chunk-523WHFSL.js → chunk-7XKL25PW.js} +1973 -754
- package/{chunk-IAESIMK7.js → chunk-A4MW3YZL.js} +10 -14
- package/chunk-AGIM7IMJ.js +147 -0
- package/{chunk-7CHC4W22.js → chunk-AIZE5TDM.js} +3 -3
- package/{chunk-P4BX6FFU.js → chunk-AOCM7HPT.js} +214 -51
- package/{chunk-5GVBDNTO.js → chunk-AU7AXHUL.js} +1 -1
- package/{chunk-4H22P2JV.js → chunk-AUIJZACN.js} +2073 -1275
- package/{chunk-CIF4X6ZM.js → chunk-AWVFQMOV.js} +25 -3
- package/{chunk-RYZINGPM.js → chunk-BPMPOFWP.js} +347 -235
- package/{chunk-YUNJN43V.js → chunk-BU2YQBV4.js} +215 -28
- package/{chunk-XPRWFQQK.js → chunk-CBHGZRCE.js} +38 -12
- package/{chunk-Y3PMXGLH.js → chunk-CHCS6XPJ.js} +365 -144
- package/chunk-CHOZJMJS.js +78 -0
- package/{chunk-GBRG75BQ.js → chunk-D2MDMY2M.js} +1 -0
- package/{chunk-3SV7RM24.js → chunk-D2QN6CTI.js} +117 -72
- package/{chunk-5TEEECZM.js → chunk-E2AKSW7L.js} +10 -7
- package/{chunk-RSEEDUMS.js → chunk-EDX5XSJM.js} +2 -2
- package/{chunk-BKWMNZ2X.js → chunk-ES633ZS5.js} +280 -98
- package/{chunk-QDSINZM3.js → chunk-EUGTFFO6.js} +2 -23
- package/{chunk-UNYJU5L6.js → chunk-EYLGAEB4.js} +5 -1
- package/{chunk-CU3QB4HW.js → chunk-EYWJT7A2.js} +12 -12
- package/{chunk-BNGW5ASC.js → chunk-EZVK3N5L.js} +66 -18
- package/{chunk-6FGTMQCL.js → chunk-F32Z66T4.js} +6 -10
- package/chunk-FJMRBOPO.js +11507 -0
- package/{chunk-ZOVKKMF6.js → chunk-FY4SDK2E.js} +1 -1
- package/chunk-G6OOUQWJ.js +417 -0
- package/{chunk-PQWU42UQ.js → chunk-GCKHQECX.js} +6 -6
- package/chunk-GKXCHNTE.js +15 -0
- package/{chunk-T7F46O36.js → chunk-GQN2WLAI.js} +20 -11
- package/{chunk-P6GMCCAT.js → chunk-GQV5KHJU.js} +78 -12
- package/{chunk-B2XUQXQX.js → chunk-HIQ4GF53.js} +191 -68
- package/{chunk-YUWMM4US.js → chunk-HKX3ULQB.js} +34 -22
- package/{chunk-GMZLP5DV.js → chunk-HNUWUO75.js} +30 -19
- package/{chunk-5UTFL4QJ.js → chunk-I2ZMGXXF.js} +10 -5
- package/chunk-I42VC5KR.js +15 -0
- package/{chunk-3BXNTXYI.js → chunk-I4SN4NUQ.js} +1199 -56
- package/chunk-IA7TRNNR.js +11 -0
- package/{chunk-WIWM4FE3.js → chunk-IJERIUXS.js} +55 -748
- package/{chunk-CPUFH46E.js → chunk-ILU2NPWY.js} +10 -10
- package/{chunk-SLLGJZDT.js → chunk-ISQL4V7C.js} +4 -4
- package/chunk-IT3ZZRJE.js +12 -0
- package/{chunk-LPVCRQMM.js → chunk-IZ7OJB6K.js} +20 -16
- package/{chunk-DY7ANLKG.js → chunk-JKMLSTZK.js} +43 -12
- package/{chunk-6TPOEJMQ.js → chunk-JZX2XLKC.js} +64 -23
- package/{chunk-27ONFIEY.js → chunk-K2AER7RX.js} +19 -12
- package/{chunk-O2OWEFII.js → chunk-KFHQQ3NO.js} +1 -1
- package/{chunk-QXGPFY6J.js → chunk-LGN377P3.js} +27 -17
- package/{chunk-L36OWPP3.js → chunk-LIQJTQRN.js} +118 -139
- package/{chunk-FWQEKRTD.js → chunk-LTRUTOHX.js} +36 -31
- package/{chunk-BFU6C242.js → chunk-NMFV26UG.js} +1 -1
- package/{chunk-5J3PYNUP.js → chunk-NYYFIKHI.js} +17 -3
- package/{chunk-6O4IK4V2.js → chunk-OFFL4DCR.js} +1 -1
- package/{chunk-B6SAMB33.js → chunk-OWYYDMGJ.js} +209 -9
- package/{chunk-KQHGZWSV.js → chunk-OXME3PEE.js} +1 -1
- package/{chunk-CTUQJVQM.js → chunk-P5QACQDK.js} +1 -1
- package/{chunk-DIXLJWCD.js → chunk-PCKVQXYV.js} +274 -93
- package/{chunk-4JMDLJAD.js → chunk-PJNFNEJE.js} +114 -37
- package/{chunk-Z4CSIXYP.js → chunk-PLSVXHSA.js} +3 -3
- package/{chunk-BIDGLJQ3.js → chunk-PLXDDGBK.js} +45 -8
- package/chunk-PWTNI4N7.js +47 -0
- package/chunk-QFEW2ULV.js +42 -0
- package/{chunk-RGNEEJ3Y.js → chunk-QLQ573SU.js} +35 -9
- package/chunk-QQ56R7KS.js +88 -0
- package/chunk-QYPE3TS3.js +43 -0
- package/{chunk-4DPP773Q.js → chunk-RSHPU4PD.js} +59 -11
- package/chunk-RVWMAFIT.js +134 -0
- package/{chunk-ESHYE7WU.js → chunk-S73EPYNC.js} +10 -7
- package/{chunk-UQNS32WZ.js → chunk-SCMHQUMK.js} +1 -1
- package/{chunk-T767MPHB.js → chunk-SMENBO3M.js} +354 -120
- package/{chunk-PCVVZ2MH.js → chunk-SSFDFOVT.js} +26 -31
- package/chunk-SW4S4XYB.js +1794 -0
- package/{chunk-HUFPWXFW.js → chunk-T6X3D7NB.js} +572 -430
- package/chunk-TY3TIUJK.js +89 -0
- package/{chunk-X2QRZXZH.js → chunk-TYJ5XFEA.js} +4 -4
- package/{chunk-ALFH24P3.js → chunk-TYYRSFBG.js} +197 -155
- package/{chunk-7KVZ5LDQ.js → chunk-UBUH2TCC.js} +18 -4
- package/chunk-UEILX4EV.js +15 -0
- package/chunk-UHTDNB5Y.js +140 -0
- package/{chunk-YYLHWIW6.js → chunk-UKZX3NHN.js} +1 -1
- package/{chunk-OHOVNO7I.js → chunk-UOKFKFPO.js} +6 -3
- package/{chunk-XISEYOGH.js → chunk-USHQX6VT.js} +5 -5
- package/chunk-UUPR3TD7.js +53 -0
- package/{chunk-HDLLCBRF.js → chunk-VEA7H5FG.js} +2 -2
- package/chunk-VHGD6FHV.js +116 -0
- package/chunk-VIWDGTDA.js +8 -0
- package/{chunk-GOH6RW6F.js → chunk-VJXHL6MH.js} +34 -11
- package/{chunk-4XEYAFLY.js → chunk-VL5KOY2C.js} +623 -94
- package/{chunk-EADNWBAZ.js → chunk-VP3BINY7.js} +13 -0
- package/{chunk-EOARMOVD.js → chunk-W4S6OTSC.js} +6 -3
- package/chunk-WD4XYDYC.js +567 -0
- package/chunk-WJZOQR2J.js +151 -0
- package/{chunk-AUDNYVRC.js → chunk-XETG4RIX.js} +5 -5
- package/{chunk-MX574M24.js → chunk-XI7DGLUK.js} +4 -4
- package/{chunk-3L2WKC3W.js → chunk-XPWGOTBQ.js} +8 -1
- package/chunk-XRUSXPCQ.js +514 -0
- package/{chunk-I4OPU5TK.js → chunk-XS47QOFV.js} +273 -11
- package/{chunk-S5XGT6GB.js → chunk-XZNCX52W.js} +30 -9
- package/{chunk-2JMMBU6V.js → chunk-Y2DXOPSO.js} +1920 -644
- package/{chunk-5OA5HMTL.js → chunk-YLMMECCH.js} +1 -1
- package/{chunk-CFT5HBGW.js → chunk-YVLQXTUB.js} +448 -12
- package/chunk-YVPQATSC.js +709 -0
- package/{chunk-DCFPQW22.js → chunk-Z7OJUX6S.js} +122 -107
- package/chunk-ZEZC7IIM.js +126 -0
- package/{chunk-ALGORS6Y.js → chunk-ZF6526IQ.js} +43 -8
- package/chunk-ZIWPQEFU.js +165 -0
- package/chunk-ZKZ5X2HS.js +7012 -0
- package/chunk-ZRAU6T47.js +56 -0
- package/{cliExecutor-AWYR3T33.js → cliExecutor-BWBF2MTI.js} +4 -3
- package/{clipboardy-BX4NMYHE.js → clipboardy-GCO3GT3I.js} +3 -8
- package/{commandRegistry-YOEQUA7T.js → commandRegistry-XN7C5GHO.js} +183 -107
- package/contentThunks-5L7YQI3S.js +84 -0
- package/{cooldownCommands-ZLEPIB4A.js → cooldownCommands-JFKTYS2D.js} +2 -2
- package/{createAgentAutomationAction-5EJL6ROU.js → createAgentAutomationAction-QOPOLCRK.js} +53 -44
- package/createDialogAction-MZ66VY5Q.js +76 -0
- package/{createPageAction-75ELC64R.js → createPageAction-VQDJIXMY.js} +65 -54
- package/cursorProvider-YGGNMVWX.js +6123 -0
- package/{db-VQSAOWAO.js → db-2QOTXYGH.js} +11 -5
- package/{dbSlice-DQ32LYTJ.js → dbSlice-J37OGIXH.js} +19 -13
- package/{deleteFile-NX6QHCWZ.js → deleteFile-6JWDDRVP.js} +10 -6
- package/dialogCommands-YZLSJB6X.js +113 -0
- package/{dialogSlice-KSXPGJB2.js → dialogSlice-6RODVYCK.js} +53 -39
- package/{docCreateCommands-ZSLGVBZT.js → docCreateCommands-JB2RRMRJ.js} +54 -57
- package/{docDeleteCommands-5N4GVZBG.js → docDeleteCommands-AQENULNM.js} +56 -58
- package/{docReadCommands-YJSAA7AV.js → docReadCommands-DYD25YLQ.js} +53 -56
- package/{docStore-AROBVRCR.js → docStore-ZZA3OPM5.js} +25 -16
- package/{docUpdateCommands-WEMLSW6W.js → docUpdateCommands-3GVS23QL.js} +54 -57
- package/{fetchMessages-67MEFPZR.js → fetchMessages-IIJWQ3HR.js} +5 -4
- package/{getPricing-NMMFT3YI.js → getPricing-FKK3F5EJ.js} +11 -9
- package/{handleSendMessageAction-EGNWHAUB.js → handleSendMessageAction-DVG5U26T.js} +619 -165
- package/index.js +11 -7
- package/{lib-FRRS27KN.js → lib-4YTWAGY7.js} +581 -248
- package/{localDialogRead-EBRS242U.js → localDialogRead-FSOMOPDZ.js} +2 -2
- package/localRuntimeAdapter-HXRHFKRC.js +244 -0
- package/localRuntimeAuthority-JDAD7CB6.js +16 -0
- package/{localRuntimeDb-3LRMNLCM.js → localRuntimeDb-CVC62PM5.js} +1 -1
- package/{machineCommands-ARZYKGGE.js → machineCommands-5KBF4RCC.js} +503 -161
- package/{machineInfo-66ZFUMLE.js → machineInfo-UKPTZO5Y.js} +2 -2
- package/machineWsRunDispatch-4OQDEDOR.js +169 -0
- package/{memberThunks-5ZXPR63F.js → memberThunks-66DH6ABQ.js} +25 -18
- package/{models-ZPU5EDF4.js → models-LAS4B2B5.js} +2 -2
- package/package.json +1 -1
- package/{providerPresetApply-DD6NGWW6.js → providerPresetApply-MY66I6NT.js} +10 -10
- package/{providerRegistry-XDRLMYGJ.js → providerRegistry-QOE2FT57.js} +13 -9
- package/{read-EPTJRAEF.js → read-DC4NUGN6.js} +12 -8
- package/{readDocTool-W4NDXNEX.js → readDocTool-NCJ5NT25.js} +1 -1
- package/{readlineWorkspace-3XOFVARF.js → readlineWorkspace-RIE62QVZ.js} +5022 -921
- package/{removeDialogAgentAction-OSYNZRX7.js → removeDialogAgentAction-E2H3OZU6.js} +29 -28
- package/{runAgentClientLoop-UMF7VZYZ.js → runAgentClientLoop-KLUUXBZS.js} +154 -97
- package/{runtimeDoctorCommands-ID23PNNZ.js → runtimeDoctorCommands-GIOGC5SC.js} +17 -17
- package/secretCommands-ZXZRLR3R.js +700 -0
- package/setDialogExtraReferencesAction-SITRPMYU.js +37 -0
- package/{setPrimaryDialogAgentAction-WZ7KI3YA.js → setPrimaryDialogAgentAction-U7N45TGC.js} +30 -29
- package/{chunk-SONWNI3L.js → settingThunks-Y7SVJOMZ.js} +223 -928
- package/{skillDiscovery-ZBU76COE.js → skillDiscovery-QC35EQY4.js} +2 -2
- package/{slateUtils-RIZES3PV.js → slateUtils-NGV2NMKM.js} +1 -1
- package/{spaceCommands-UJOVU4XI.js → spaceCommands-ZR5BR3GX.js} +59 -59
- package/{streamAgentChatTurn-GYGMYOSM.js → streamAgentChatTurn-MN2SSPGA.js} +309 -165
- package/{switchDialogAgentAction-GMR7WN3P.js → switchDialogAgentAction-OMGBD3N2.js} +26 -22
- package/{tableCommands-G7RV3RU3.js → tableCommands-U3QDY4L6.js} +19 -17
- package/{toolApiClient-CVGWM4PN.js → toolApiClient-7WSJJJBV.js} +6 -5
- package/tools-W2MOLERY.js +124 -0
- package/tuiSummaryLlmCaller-BM3VSTBU.js +130 -0
- package/{updateCommands-4EHDLF5J.js → updateCommands-NQXAGFM7.js} +3 -3
- package/{updateDialogTitleAction-4U5W4AMH.js → updateDialogTitleAction-Z4JX6REN.js} +102 -57
- package/workloadLifecycle-36DXRZKU.js +320 -0
- package/action-GI44T4DR.js +0 -31
- package/agentListCommands-IW3GQVUP.js +0 -427
- package/agentRecordCommands-4YBSYPIY.js +0 -134
- package/agentRunCommand-5MU5X5OT.js +0 -161
- package/browser-image-compression-MFDTD7K6.js +0 -1763
- package/chatQueueReduxAdapter-HVHA2HPM.js +0 -186
- package/chromeOpsCommands-IXRLN57H.js +0 -289
- package/chunk-42Z3EMC4.js +0 -25363
- package/chunk-CQ3WA2NE.js +0 -299
- package/chunk-CYSYR67N.js +0 -93
- package/chunk-IAG4FHOJ.js +0 -34
- package/chunk-JBTJAPUF.js +0 -35
- package/chunk-K2CSUCHJ.js +0 -620
- package/chunk-LTRXGFMS.js +0 -83
- package/chunk-M76ECQRD.js +0 -13658
- package/chunk-MEYIRKDN.js +0 -8967
- package/chunk-OCZKATRC.js +0 -129
- package/chunk-OTGFTBLI.js +0 -13
- package/chunk-PUOC4YRO.js +0 -394
- package/chunk-QU5A4LYX.js +0 -2415
- package/chunk-URMS4XY2.js +0 -10
- package/chunk-UV6VISQJ.js +0 -20
- package/chunk-VJVNU77E.js +0 -83
- package/chunk-WRMKCKZ3.js +0 -13807
- package/chunk-Y5GCAMGP.js +0 -9
- package/chunk-YPREESF4.js +0 -225
- package/contentThunks-AK2F3KNC.js +0 -73
- package/createDialogAction-F5WEWA47.js +0 -66
- package/dialogCommands-G7YBVAMC.js +0 -102
- package/levelLazyShim-GZBKODGI.js +0 -7
- package/machineWsRunDispatch-I7F4L532.js +0 -149
- package/serverStoreFactory-YMXDF4NI.js +0 -20
- package/setDialogExtraReferencesAction-M32UYNCG.js +0 -30
- package/settingSlice-Q6EKL7EQ.js +0 -204
- package/syncMapping-N6RGXGBW.js +0 -437
- package/tools-DU4JXVAV.js +0 -111
- package/tuiSummaryLlmCaller-3YFNHCTW.js +0 -128
- package/{chunk-SV5IJK6K.js → chunk-NBTY7YJF.js} +3 -3
- package/{chunk-2SYTUTLI.js → chunk-YBNHPX4S.js} +3 -3
- package/{offlineMarxistsAgentCommand-HGXZV4XJ.js → offlineMarxistsAgentCommand-2QTHJCHV.js} +4 -4
|
@@ -0,0 +1,709 @@
|
|
|
1
|
+
// packages/agent-runtime/dockerQuotaStore.ts
|
|
2
|
+
import { join, resolve, dirname } from "node:path";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { existsSync, readFileSync, writeFileSync, renameSync, unlinkSync, mkdirSync } from "node:fs";
|
|
5
|
+
var InMemoryDockerQuotaKvStore = class {
|
|
6
|
+
store = /* @__PURE__ */ new Map();
|
|
7
|
+
async get(key) {
|
|
8
|
+
const val = this.store.get(key);
|
|
9
|
+
return val ? JSON.parse(JSON.stringify(val)) : null;
|
|
10
|
+
}
|
|
11
|
+
async put(key, record) {
|
|
12
|
+
this.store.set(key, JSON.parse(JSON.stringify(record)));
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
var FileDockerQuotaKvStore = class {
|
|
16
|
+
filePath;
|
|
17
|
+
constructor(filePath) {
|
|
18
|
+
this.filePath = filePath ?? join(process.env.NOLO_HOME || resolve(homedir(), ".nolo"), "docker-quota.json");
|
|
19
|
+
}
|
|
20
|
+
async get(key) {
|
|
21
|
+
try {
|
|
22
|
+
if (!existsSync(this.filePath)) {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
const raw = readFileSync(this.filePath, "utf-8");
|
|
26
|
+
const parsed = JSON.parse(raw);
|
|
27
|
+
if (!parsed || typeof parsed !== "object") {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
if (parsed.key === key) {
|
|
31
|
+
return parsed;
|
|
32
|
+
}
|
|
33
|
+
if (key in parsed) {
|
|
34
|
+
return parsed[key] ?? null;
|
|
35
|
+
}
|
|
36
|
+
return null;
|
|
37
|
+
} catch (err) {
|
|
38
|
+
console.warn(`[FileDockerQuotaKvStore] Warning: failed to parse quota file at ${this.filePath}:`, err);
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
async put(key, record) {
|
|
43
|
+
const dir = dirname(this.filePath);
|
|
44
|
+
if (!existsSync(dir)) {
|
|
45
|
+
mkdirSync(dir, { recursive: true });
|
|
46
|
+
}
|
|
47
|
+
let data = {};
|
|
48
|
+
try {
|
|
49
|
+
if (existsSync(this.filePath)) {
|
|
50
|
+
const raw = readFileSync(this.filePath, "utf-8");
|
|
51
|
+
const parsed = JSON.parse(raw);
|
|
52
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
53
|
+
if (parsed.key && typeof parsed.key === "string" && !parsed[parsed.key]) {
|
|
54
|
+
data[parsed.key] = parsed;
|
|
55
|
+
} else {
|
|
56
|
+
data = parsed;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
} catch (err) {
|
|
61
|
+
console.warn(`[FileDockerQuotaKvStore] Warning: corrupted quota file at ${this.filePath}, resetting content:`, err);
|
|
62
|
+
data = {};
|
|
63
|
+
}
|
|
64
|
+
data[key] = record;
|
|
65
|
+
const tmpPath = `${this.filePath}.tmp.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2)}`;
|
|
66
|
+
try {
|
|
67
|
+
writeFileSync(tmpPath, JSON.stringify(data, null, 2), "utf-8");
|
|
68
|
+
renameSync(tmpPath, this.filePath);
|
|
69
|
+
} catch (err) {
|
|
70
|
+
try {
|
|
71
|
+
if (existsSync(tmpPath)) {
|
|
72
|
+
unlinkSync(tmpPath);
|
|
73
|
+
}
|
|
74
|
+
} catch {
|
|
75
|
+
}
|
|
76
|
+
throw err;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
function buildDockerQuotaKey(owner, dockerAccount) {
|
|
81
|
+
if (!owner || !dockerAccount) {
|
|
82
|
+
throw new Error("Both owner and dockerAccount must be non-empty to build quota key");
|
|
83
|
+
}
|
|
84
|
+
return `docker-quota:${owner}:${dockerAccount}`;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// packages/agent-runtime/dockerBudgetAdmission.ts
|
|
88
|
+
var DEFAULT_MAX_STALENESS_MS = 5 * 60 * 1e3;
|
|
89
|
+
function calculateEstimatedBudget(ttlSeconds, rateNanoUsd) {
|
|
90
|
+
if (!Number.isFinite(ttlSeconds) || ttlSeconds <= 0) {
|
|
91
|
+
throw new Error("Invalid ttlSeconds: must be positive finite number");
|
|
92
|
+
}
|
|
93
|
+
if (rateNanoUsd <= 0n) {
|
|
94
|
+
throw new Error("Invalid rateNanoUsd: must be positive non-zero bigint");
|
|
95
|
+
}
|
|
96
|
+
const ceilSeconds = BigInt(Math.ceil(ttlSeconds));
|
|
97
|
+
return ceilSeconds * rateNanoUsd;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// packages/agent-runtime/dockerSandboxCloudSpec.ts
|
|
101
|
+
var SANDBOX_BILLABLE_SHAPES = [
|
|
102
|
+
{ id: "micro1/2G", cpus: 1, memory: "2g" },
|
|
103
|
+
{ id: "small2/4G", cpus: 2, memory: "4g" },
|
|
104
|
+
{ id: "medium4/8G", cpus: 4, memory: "8g" },
|
|
105
|
+
{ id: "large8/16G", cpus: 8, memory: "16g" },
|
|
106
|
+
{ id: "xl16/32G", cpus: 16, memory: "32g" }
|
|
107
|
+
];
|
|
108
|
+
var SANDBOX_DEFAULT_SHAPE = "medium4/8G";
|
|
109
|
+
function billableSandboxShape(id) {
|
|
110
|
+
const found = SANDBOX_BILLABLE_SHAPES.find((x) => x.id === id);
|
|
111
|
+
if (!found) throw Error(`Non-billable sandbox shape: ${id}`);
|
|
112
|
+
return found;
|
|
113
|
+
}
|
|
114
|
+
function pinnedBunSandboxImage(pinnedBun) {
|
|
115
|
+
return `docker.io/oven/bun:${pinnedBun}`;
|
|
116
|
+
}
|
|
117
|
+
var SANDBOX_DEFAULT_PLATFORM = "linux/amd64";
|
|
118
|
+
function buildSbxCloudCreateArgv(spec) {
|
|
119
|
+
const shape = billableSandboxShape(spec.shapeId ?? SANDBOX_DEFAULT_SHAPE);
|
|
120
|
+
const args = ["--cloud", "create", "--name", spec.name];
|
|
121
|
+
if (spec.template !== void 0) {
|
|
122
|
+
args.push("--template", spec.template);
|
|
123
|
+
} else {
|
|
124
|
+
args.push(
|
|
125
|
+
"--image-ref",
|
|
126
|
+
spec.image ?? pinnedBunSandboxImage(Bun.version),
|
|
127
|
+
"--cpus",
|
|
128
|
+
String(shape.cpus),
|
|
129
|
+
"--memory",
|
|
130
|
+
shape.memory
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
args.push(
|
|
134
|
+
"--platform",
|
|
135
|
+
SANDBOX_DEFAULT_PLATFORM,
|
|
136
|
+
"--ttl",
|
|
137
|
+
`${spec.ttlMinutes}m`,
|
|
138
|
+
"--on-timeout",
|
|
139
|
+
spec.onTimeout ?? "stop"
|
|
140
|
+
);
|
|
141
|
+
return args;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// packages/agent-runtime/dockerSandboxExecutionBackend.ts
|
|
145
|
+
var DOCKER_SANDBOX_BACKEND_ID = "docker-sandbox-cloud";
|
|
146
|
+
var SAFE_MESSAGE = {
|
|
147
|
+
"invalid-ticket": "Rejected: execution ticket cannot be satisfied by the docker sandbox backend",
|
|
148
|
+
"quota-unverifiable": "Rejected before any sandbox side effect: docker quota could not be verified",
|
|
149
|
+
"quota-insufficient": "Rejected before any sandbox side effect: docker quota is below the estimated budget",
|
|
150
|
+
"concurrency-rejected": "Rejected: this docker account already holds the maximum number of active sandboxes",
|
|
151
|
+
"create-failed": "Sandbox create failed",
|
|
152
|
+
"lease-not-found": "Unknown execution lease",
|
|
153
|
+
"lease-mismatch": "Lease does not match this backend or ticket",
|
|
154
|
+
"lease-released": "Lease was already released",
|
|
155
|
+
"unsupported-request": "Request kind is not supported by the docker sandbox backend",
|
|
156
|
+
"exec-timeout": "Sandbox exec exceeded the backend execution bound and was killed",
|
|
157
|
+
"exec-failed": "Sandbox exec could not be completed",
|
|
158
|
+
"quota-record-failed": "Sandbox run aborted: docker quota bookkeeping could not be persisted",
|
|
159
|
+
"stop-unverified": "Sandbox stop could not be confirmed; cleanup is pending"
|
|
160
|
+
};
|
|
161
|
+
var DockerSandboxBackendError = class extends Error {
|
|
162
|
+
code;
|
|
163
|
+
/** true = 云端可能仍有存活沙盒,调用方不得当作已清理。 */
|
|
164
|
+
cleanupPending;
|
|
165
|
+
constructor(code, detail, cleanupPending = false) {
|
|
166
|
+
super(detail ? `${SAFE_MESSAGE[code]} (${detail})` : SAFE_MESSAGE[code]);
|
|
167
|
+
this.name = "DockerSandboxBackendError";
|
|
168
|
+
this.code = code;
|
|
169
|
+
this.cleanupPending = cleanupPending;
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
var DEFAULT_TTL_MINUTES = 15;
|
|
173
|
+
var DEFAULT_MAX_OUTPUT_CHARS = 8e3;
|
|
174
|
+
var DEFAULT_EXEC_TIMEOUT_MS = 6e4;
|
|
175
|
+
var DEFAULT_MAX_ACTIVE_SANDBOXES_PER_ACCOUNT = 3;
|
|
176
|
+
function resolveMaxActiveSandboxesFromEnv(env = process.env) {
|
|
177
|
+
const raw = (env.NOLO_DOCKER_MAX_ACTIVE_SANDBOXES ?? "").trim();
|
|
178
|
+
if (!raw) return null;
|
|
179
|
+
const parsed = Number(raw);
|
|
180
|
+
if (!Number.isInteger(parsed) || parsed <= 0) {
|
|
181
|
+
throw new Error(
|
|
182
|
+
`NOLO_DOCKER_MAX_ACTIVE_SANDBOXES \u5FC5\u987B\u662F\u6B63\u6574\u6570\uFF0C\u6536\u5230 "${raw}"\uFF1B\u62D2\u7EDD\u9759\u9ED8\u56DE\u843D\u9ED8\u8BA4\u503C`
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
return parsed;
|
|
186
|
+
}
|
|
187
|
+
var UNVERIFIED_QUOTA_RATE_NANO_USD = 77780n;
|
|
188
|
+
var SHELL_EXECUTABLES = /* @__PURE__ */ new Set([
|
|
189
|
+
"bash",
|
|
190
|
+
"sh",
|
|
191
|
+
"zsh",
|
|
192
|
+
"dash",
|
|
193
|
+
"ksh",
|
|
194
|
+
"mksh",
|
|
195
|
+
"ash",
|
|
196
|
+
"fish"
|
|
197
|
+
]);
|
|
198
|
+
var EXEC_TIMEOUT_KILL_GRACE_MS = 2e3;
|
|
199
|
+
var STALE_RUNNING_SLACK_MS = 6e4;
|
|
200
|
+
var SANDBOX_NAME_PATTERN = /^agent-sbx-[A-Za-z0-9][A-Za-z0-9_-]{0,47}-[a-z0-9]{1,13}$/;
|
|
201
|
+
var SATISFIABLE_ISOLATION = /* @__PURE__ */ new Set(["none", "os-sandbox", "container"]);
|
|
202
|
+
function buildDockerSandboxName(ticketId, ts) {
|
|
203
|
+
const safeId = ticketId.replace(/[^A-Za-z0-9_-]/g, "").slice(0, 48) || "run";
|
|
204
|
+
const name = `agent-sbx-${safeId}-${ts.toString(36)}`;
|
|
205
|
+
if (!SANDBOX_NAME_PATTERN.test(name)) {
|
|
206
|
+
throw new DockerSandboxBackendError("invalid-ticket", "sandbox name rejected");
|
|
207
|
+
}
|
|
208
|
+
return name;
|
|
209
|
+
}
|
|
210
|
+
function argvToShellScript(argv) {
|
|
211
|
+
const quote = (token) => `'${token.replaceAll("'", `'\\''`)}'`;
|
|
212
|
+
if (argv.length === 3 && argv[0] === "bash" && argv[1] === "-lc") return argv[2];
|
|
213
|
+
return argv.map(quote).join(" ");
|
|
214
|
+
}
|
|
215
|
+
function truncateOutput(value, limitChars) {
|
|
216
|
+
if (value.length <= limitChars) return value;
|
|
217
|
+
return `${value.slice(0, limitChars)}\u2026[truncated ${value.length - limitChars} chars]`;
|
|
218
|
+
}
|
|
219
|
+
async function defaultRunSbx(args, options = {}) {
|
|
220
|
+
const { spawn } = await import("node:child_process");
|
|
221
|
+
const timeoutMs = typeof options.timeoutMs === "number" && Number.isFinite(options.timeoutMs) && options.timeoutMs > 0 ? options.timeoutMs : void 0;
|
|
222
|
+
return await new Promise((resolve2) => {
|
|
223
|
+
const child = spawn("sbx", [...args], {
|
|
224
|
+
shell: false,
|
|
225
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
226
|
+
});
|
|
227
|
+
let stdout = "";
|
|
228
|
+
let stderr = "";
|
|
229
|
+
let timedOut = false;
|
|
230
|
+
let settled = false;
|
|
231
|
+
let timeoutTimer;
|
|
232
|
+
let killGraceTimer;
|
|
233
|
+
const settle = (exitCode) => {
|
|
234
|
+
if (settled) return;
|
|
235
|
+
settled = true;
|
|
236
|
+
if (timeoutTimer) clearTimeout(timeoutTimer);
|
|
237
|
+
if (killGraceTimer) clearTimeout(killGraceTimer);
|
|
238
|
+
resolve2({ exitCode, stdout, stderr, ...timedOut ? { timedOut: true } : {} });
|
|
239
|
+
};
|
|
240
|
+
child.stdout?.on("data", (chunk) => {
|
|
241
|
+
stdout += String(chunk);
|
|
242
|
+
});
|
|
243
|
+
child.stderr?.on("data", (chunk) => {
|
|
244
|
+
stderr += String(chunk);
|
|
245
|
+
});
|
|
246
|
+
child.on("error", () => settle(null));
|
|
247
|
+
child.on("close", (code) => settle(code ?? null));
|
|
248
|
+
if (timeoutMs !== void 0) {
|
|
249
|
+
timeoutTimer = setTimeout(() => {
|
|
250
|
+
timedOut = true;
|
|
251
|
+
try {
|
|
252
|
+
child.kill("SIGKILL");
|
|
253
|
+
} catch {
|
|
254
|
+
}
|
|
255
|
+
killGraceTimer = setTimeout(() => settle(null), EXEC_TIMEOUT_KILL_GRACE_MS);
|
|
256
|
+
}, timeoutMs);
|
|
257
|
+
}
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
function buildSandboxSlotKey(quotaKey, slot) {
|
|
261
|
+
return slot === 1 ? quotaKey : `${quotaKey}#slot:${slot}`;
|
|
262
|
+
}
|
|
263
|
+
function isShellArgv(argv) {
|
|
264
|
+
const head = argv[0];
|
|
265
|
+
if (typeof head !== "string" || !head) return false;
|
|
266
|
+
const base = head.split(/[\\/]/).pop()?.toLowerCase().replace(/\.exe$/, "") ?? "";
|
|
267
|
+
return SHELL_EXECUTABLES.has(base);
|
|
268
|
+
}
|
|
269
|
+
function staleRunningReason(record, nowMs, staleRunningAfterMs) {
|
|
270
|
+
const observedAt = record.observedAt;
|
|
271
|
+
if (typeof observedAt !== "number" || !Number.isFinite(observedAt)) return null;
|
|
272
|
+
const age = nowMs - observedAt;
|
|
273
|
+
if (!Number.isFinite(age) || age <= staleRunningAfterMs) return null;
|
|
274
|
+
return "observed-at-beyond-lifetime";
|
|
275
|
+
}
|
|
276
|
+
function buildStaleAuditKey(quotaKey, sandboxName) {
|
|
277
|
+
const safeSandbox = typeof sandboxName === "string" ? sandboxName.replace(/[^A-Za-z0-9_-]/g, "").slice(0, 64) || "unknown" : "unknown";
|
|
278
|
+
return `${quotaKey}#stale:${safeSandbox}`;
|
|
279
|
+
}
|
|
280
|
+
function deriveStaleRunningMs(ttlMinutes, execTimeoutMs) {
|
|
281
|
+
return ttlMinutes * 6e4 + execTimeoutMs + STALE_RUNNING_SLACK_MS;
|
|
282
|
+
}
|
|
283
|
+
function warnBookkeeping(phase, extra = {}) {
|
|
284
|
+
console.warn(
|
|
285
|
+
`[dockerSandboxExecutionBackend] ${JSON.stringify({
|
|
286
|
+
event: "quota-bookkeeping-failed",
|
|
287
|
+
phase,
|
|
288
|
+
...extra
|
|
289
|
+
})}`
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
var accountLockTails = /* @__PURE__ */ new Map();
|
|
293
|
+
function createDockerSandboxExecutionBackend(deps = {}) {
|
|
294
|
+
const runSbx = deps.runSbx ?? defaultRunSbx;
|
|
295
|
+
const kvStore = deps.kvStore ?? new InMemoryDockerQuotaKvStore();
|
|
296
|
+
const now = deps.now ?? (() => Date.now());
|
|
297
|
+
const owner = deps.owner ?? "local-owner";
|
|
298
|
+
const dockerAccount = deps.dockerAccount ?? "default-account";
|
|
299
|
+
const shapeId = deps.shapeId ?? SANDBOX_DEFAULT_SHAPE;
|
|
300
|
+
const ttlMinutes = deps.ttlMinutes ?? DEFAULT_TTL_MINUTES;
|
|
301
|
+
const maxOutputChars = deps.maxOutputChars ?? DEFAULT_MAX_OUTPUT_CHARS;
|
|
302
|
+
const leases = /* @__PURE__ */ new Map();
|
|
303
|
+
const descriptor = {
|
|
304
|
+
id: DOCKER_SANDBOX_BACKEND_ID,
|
|
305
|
+
kind: "cloud",
|
|
306
|
+
capabilities: {
|
|
307
|
+
executionClasses: ["oneshot"],
|
|
308
|
+
streamingExec: false,
|
|
309
|
+
filesystem: true,
|
|
310
|
+
persistentFilesystem: false,
|
|
311
|
+
processSignals: false,
|
|
312
|
+
inboundPorts: false,
|
|
313
|
+
networkPolicy: false,
|
|
314
|
+
snapshots: false,
|
|
315
|
+
gpu: false,
|
|
316
|
+
isolationModes: ["container"]
|
|
317
|
+
}
|
|
318
|
+
};
|
|
319
|
+
const writeQuotaRecord = async (key, patch, before) => {
|
|
320
|
+
const previous = await kvStore.get(key).catch(() => null);
|
|
321
|
+
await kvStore.put(key, {
|
|
322
|
+
key,
|
|
323
|
+
owner,
|
|
324
|
+
dockerAccount,
|
|
325
|
+
remainingNanoUsd: patch.remainingNanoUsd ?? previous?.remainingNanoUsd ?? "0",
|
|
326
|
+
observedAt: patch.observedAt ?? previous?.observedAt ?? now(),
|
|
327
|
+
pendingEstimateNanoUsd: patch.pendingEstimateNanoUsd ?? "0",
|
|
328
|
+
estimatedSpendNanoUsd: patch.estimatedSpendNanoUsd ?? previous?.estimatedSpendNanoUsd ?? "0",
|
|
329
|
+
taskId: patch.taskId ?? previous?.taskId ?? "",
|
|
330
|
+
sandbox: patch.sandbox ?? previous?.sandbox ?? "",
|
|
331
|
+
status: patch.status ?? previous?.status ?? "idle",
|
|
332
|
+
// stale 必须能表达「显式清掉」:只有调用方完全不表态(undefined)才继承上一条
|
|
333
|
+
// 记录。否则一次 stale:true 会沿 running→终态→running 永久连锁继承,把并发
|
|
334
|
+
// 闸门的年龄判定打穿(C1)。new running 写入一律显式传 stale:false。
|
|
335
|
+
...patch.stale === void 0 ? previous?.stale === void 0 ? {} : { stale: previous.stale } : { stale: patch.stale },
|
|
336
|
+
beforeAfter: {
|
|
337
|
+
...before ? { before } : previous?.beforeAfter?.before ? { before: previous.beforeAfter.before } : {},
|
|
338
|
+
...previous?.beforeAfter?.after ? { after: previous.beforeAfter.after } : {}
|
|
339
|
+
}
|
|
340
|
+
});
|
|
341
|
+
};
|
|
342
|
+
const positiveMs = (value) => typeof value === "number" && Number.isFinite(value) && value > 0 ? value : null;
|
|
343
|
+
const baseExecTimeoutMs = positiveMs(deps.execTimeoutMs) ?? DEFAULT_EXEC_TIMEOUT_MS;
|
|
344
|
+
const defaultExecTimeoutMs = positiveMs(deps.execTimeouts?.defaultMs) ?? baseExecTimeoutMs;
|
|
345
|
+
const shellExecTimeoutMs = positiveMs(deps.execTimeouts?.shellMs) ?? baseExecTimeoutMs;
|
|
346
|
+
const maxExecTimeoutMs = Math.max(defaultExecTimeoutMs, shellExecTimeoutMs);
|
|
347
|
+
const maxActiveSandboxes = typeof deps.maxActiveSandboxesPerAccount === "number" && Number.isInteger(deps.maxActiveSandboxesPerAccount) && deps.maxActiveSandboxesPerAccount > 0 ? deps.maxActiveSandboxesPerAccount : resolveMaxActiveSandboxesFromEnv() ?? DEFAULT_MAX_ACTIVE_SANDBOXES_PER_ACCOUNT;
|
|
348
|
+
const staleRunningAfterMs = typeof deps.staleRunningAfterMs === "number" && Number.isFinite(deps.staleRunningAfterMs) && deps.staleRunningAfterMs > 0 ? deps.staleRunningAfterMs : deriveStaleRunningMs(ttlMinutes, maxExecTimeoutMs);
|
|
349
|
+
const controlTimeoutMs = maxExecTimeoutMs;
|
|
350
|
+
const persistQuotaRecord = async (phase, key, patch, before) => {
|
|
351
|
+
try {
|
|
352
|
+
await writeQuotaRecord(key, patch, before);
|
|
353
|
+
return true;
|
|
354
|
+
} catch (error) {
|
|
355
|
+
warnBookkeeping(phase, {
|
|
356
|
+
sandbox: patch.sandbox ?? "",
|
|
357
|
+
reason: error instanceof Error ? error.name : typeof error
|
|
358
|
+
});
|
|
359
|
+
return false;
|
|
360
|
+
}
|
|
361
|
+
};
|
|
362
|
+
const readConcurrencyGate = async (key) => {
|
|
363
|
+
try {
|
|
364
|
+
return await kvStore.get(key);
|
|
365
|
+
} catch {
|
|
366
|
+
throw new DockerSandboxBackendError("quota-unverifiable", "quota store read failed");
|
|
367
|
+
}
|
|
368
|
+
};
|
|
369
|
+
const runningRecordPersisted = async (key, sandboxName) => {
|
|
370
|
+
try {
|
|
371
|
+
const record = await kvStore.get(key);
|
|
372
|
+
return record?.status === "running" && record.sandbox === sandboxName;
|
|
373
|
+
} catch (error) {
|
|
374
|
+
warnBookkeeping("running-verify", {
|
|
375
|
+
sandbox: sandboxName,
|
|
376
|
+
reason: error instanceof Error ? error.name : typeof error
|
|
377
|
+
});
|
|
378
|
+
return false;
|
|
379
|
+
}
|
|
380
|
+
};
|
|
381
|
+
const runWithAccountLock = async (key, fn) => {
|
|
382
|
+
const previous = accountLockTails.get(key) ?? Promise.resolve();
|
|
383
|
+
const run = previous.catch(() => void 0).then(fn);
|
|
384
|
+
const tail = run.catch(() => void 0);
|
|
385
|
+
accountLockTails.set(key, tail);
|
|
386
|
+
try {
|
|
387
|
+
return await run;
|
|
388
|
+
} finally {
|
|
389
|
+
if (accountLockTails.get(key) === tail) accountLockTails.delete(key);
|
|
390
|
+
}
|
|
391
|
+
};
|
|
392
|
+
const archiveStaleSlotRecord = async (slotKey, record) => {
|
|
393
|
+
await persistQuotaRecord(
|
|
394
|
+
"stale-running-archive",
|
|
395
|
+
buildStaleAuditKey(slotKey, record.sandbox),
|
|
396
|
+
// 归档记的是「可能仍在计费的泄漏沙盒」:金额与归属必须从被回收的那条
|
|
397
|
+
// 记录原样带入。改用 0/空串顶替(本函数对缺省字段的回落行为)会把敞口
|
|
398
|
+
// 与责任归属一起抹掉,事后无法对账、无法定位泄漏来源(M-2)。
|
|
399
|
+
{
|
|
400
|
+
status: "running",
|
|
401
|
+
stale: true,
|
|
402
|
+
sandbox: record.sandbox ?? "",
|
|
403
|
+
observedAt: record.observedAt,
|
|
404
|
+
remainingNanoUsd: record.remainingNanoUsd,
|
|
405
|
+
pendingEstimateNanoUsd: record.pendingEstimateNanoUsd,
|
|
406
|
+
estimatedSpendNanoUsd: record.estimatedSpendNanoUsd,
|
|
407
|
+
taskId: record.taskId
|
|
408
|
+
}
|
|
409
|
+
);
|
|
410
|
+
};
|
|
411
|
+
const reserveActiveSlot = async (quotaKey) => {
|
|
412
|
+
let cloudPresence = null;
|
|
413
|
+
const cloudSandboxPresence = () => {
|
|
414
|
+
cloudPresence ??= probeCloudSandboxPresence(runSbx, controlTimeoutMs);
|
|
415
|
+
return cloudPresence;
|
|
416
|
+
};
|
|
417
|
+
for (let slot = 1; slot <= maxActiveSandboxes; slot += 1) {
|
|
418
|
+
const slotKey = buildSandboxSlotKey(quotaKey, slot);
|
|
419
|
+
const record = await readConcurrencyGate(slotKey);
|
|
420
|
+
if (record?.status !== "running") return slotKey;
|
|
421
|
+
const staleReason = staleRunningReason(record, now(), staleRunningAfterMs);
|
|
422
|
+
if (staleReason) {
|
|
423
|
+
warnBookkeeping("stale-running-reclaimed", {
|
|
424
|
+
sandbox: record.sandbox ?? "",
|
|
425
|
+
reason: staleReason
|
|
426
|
+
});
|
|
427
|
+
await archiveStaleSlotRecord(slotKey, record);
|
|
428
|
+
return slotKey;
|
|
429
|
+
}
|
|
430
|
+
const sandboxName = typeof record.sandbox === "string" ? record.sandbox : "";
|
|
431
|
+
if (!sandboxName) {
|
|
432
|
+
warnBookkeeping("sandbox-presence-unverifiable", { reason: "no-sandbox-reference" });
|
|
433
|
+
throw new DockerSandboxBackendError(
|
|
434
|
+
"quota-unverifiable",
|
|
435
|
+
"active sandbox record carries no sandbox reference"
|
|
436
|
+
);
|
|
437
|
+
}
|
|
438
|
+
const presence = await cloudSandboxPresence();
|
|
439
|
+
if (!presence.ok) {
|
|
440
|
+
warnBookkeeping("sandbox-presence-unverifiable", {
|
|
441
|
+
sandbox: sandboxName,
|
|
442
|
+
reason: presence.reason
|
|
443
|
+
});
|
|
444
|
+
throw new DockerSandboxBackendError(
|
|
445
|
+
"quota-unverifiable",
|
|
446
|
+
"active sandbox presence could not be verified against the cloud listing"
|
|
447
|
+
);
|
|
448
|
+
}
|
|
449
|
+
if (presence.names.has(sandboxName)) continue;
|
|
450
|
+
warnBookkeeping("stale-running-reclaimed", {
|
|
451
|
+
sandbox: sandboxName,
|
|
452
|
+
reason: "absent-from-cloud-listing"
|
|
453
|
+
});
|
|
454
|
+
await archiveStaleSlotRecord(slotKey, record);
|
|
455
|
+
return slotKey;
|
|
456
|
+
}
|
|
457
|
+
throw new DockerSandboxBackendError(
|
|
458
|
+
"concurrency-rejected",
|
|
459
|
+
`active sandbox limit ${maxActiveSandboxes} reached`
|
|
460
|
+
);
|
|
461
|
+
};
|
|
462
|
+
return {
|
|
463
|
+
describe() {
|
|
464
|
+
return descriptor;
|
|
465
|
+
},
|
|
466
|
+
async prepare(ticket) {
|
|
467
|
+
if (ticket.version !== 1 || !ticket.ticketId || !ticket.grantId || ticket.shellAllowed !== true || !SATISFIABLE_ISOLATION.has(ticket.requestedIsolation)) {
|
|
468
|
+
throw new DockerSandboxBackendError("invalid-ticket");
|
|
469
|
+
}
|
|
470
|
+
if (ticket.target.kind !== "server" || ticket.executionClass !== "oneshot") {
|
|
471
|
+
throw new DockerSandboxBackendError("invalid-ticket", "target/class unsupported");
|
|
472
|
+
}
|
|
473
|
+
const quotaVerifyEnabled = deps.quotaVerify !== false;
|
|
474
|
+
const quota = quotaVerifyEnabled && deps.quotaProvider ? await deps.quotaProvider() : { ok: false, reason: "verification-disabled" };
|
|
475
|
+
if (quotaVerifyEnabled && !quota.ok) {
|
|
476
|
+
throw new DockerSandboxBackendError("quota-unverifiable");
|
|
477
|
+
}
|
|
478
|
+
const quotaRateNanoUsd = quota.ok ? quota.rateNanoUsd : UNVERIFIED_QUOTA_RATE_NANO_USD;
|
|
479
|
+
const estimatedNanoUsd = calculateEstimatedBudget(ttlMinutes * 60, quotaRateNanoUsd);
|
|
480
|
+
if (quota.ok && quota.availableNanoUsd < estimatedNanoUsd) {
|
|
481
|
+
throw new DockerSandboxBackendError("quota-insufficient");
|
|
482
|
+
}
|
|
483
|
+
const quotaKey = buildDockerQuotaKey(owner, dockerAccount);
|
|
484
|
+
return await runWithAccountLock(quotaKey, async () => {
|
|
485
|
+
const slotKey = await reserveActiveSlot(quotaKey);
|
|
486
|
+
const startedAt = now();
|
|
487
|
+
const sandboxName = deps.sandboxName ?? buildDockerSandboxName(ticket.ticketId, startedAt);
|
|
488
|
+
const reservedRecorded = await persistQuotaRecord(
|
|
489
|
+
"running",
|
|
490
|
+
slotKey,
|
|
491
|
+
{
|
|
492
|
+
// 未核实时没有 availableNanoUsd:记账写 "unverified" 而不是崩溃(RV-FATAL)
|
|
493
|
+
remainingNanoUsd: quota.ok ? quota.availableNanoUsd.toString() : "unverified",
|
|
494
|
+
// 槽位记录的年龄必须从「这条记录落盘的时刻」起算,沿用配额快照的
|
|
495
|
+
// observedAt 会让陈旧阈值被偷偷缩短(M-1)。配额观察时间没丢,仍在
|
|
496
|
+
// beforeAfter.before 里。
|
|
497
|
+
observedAt: now(),
|
|
498
|
+
pendingEstimateNanoUsd: estimatedNanoUsd.toString(),
|
|
499
|
+
estimatedSpendNanoUsd: "0",
|
|
500
|
+
taskId: ticket.ticketId,
|
|
501
|
+
sandbox: sandboxName,
|
|
502
|
+
status: "running",
|
|
503
|
+
// 显式清 stale:这条槽位记录代表**占位中的沙盒**,绝不能继承上一条终态
|
|
504
|
+
// 记录残留的 stale:true(否则它会被年龄之外的捷径判成可回收)。
|
|
505
|
+
stale: false
|
|
506
|
+
},
|
|
507
|
+
{
|
|
508
|
+
// 未核实时没有 availableNanoUsd:记账写 "unverified" 而不是崩溃(RV-FATAL)
|
|
509
|
+
remainingNanoUsd: quota.ok ? quota.availableNanoUsd.toString() : "unverified",
|
|
510
|
+
observedAt: quota.ok ? quota.observedAt : now()
|
|
511
|
+
}
|
|
512
|
+
);
|
|
513
|
+
if (!reservedRecorded || !await runningRecordPersisted(slotKey, sandboxName)) {
|
|
514
|
+
throw new DockerSandboxBackendError("quota-record-failed");
|
|
515
|
+
}
|
|
516
|
+
const create = await runSbx(
|
|
517
|
+
buildSbxCloudCreateArgv({ name: sandboxName, shapeId, ttlMinutes }),
|
|
518
|
+
{ timeoutMs: controlTimeoutMs }
|
|
519
|
+
);
|
|
520
|
+
if (create.exitCode !== 0) {
|
|
521
|
+
await persistQuotaRecord("create-failed", slotKey, {
|
|
522
|
+
status: "failed",
|
|
523
|
+
sandbox: sandboxName,
|
|
524
|
+
taskId: ticket.ticketId
|
|
525
|
+
});
|
|
526
|
+
throw new DockerSandboxBackendError("create-failed");
|
|
527
|
+
}
|
|
528
|
+
const lease = {
|
|
529
|
+
leaseId: sandboxName,
|
|
530
|
+
backendId: descriptor.id,
|
|
531
|
+
ticketId: ticket.ticketId,
|
|
532
|
+
grantId: ticket.grantId,
|
|
533
|
+
createdAt: startedAt,
|
|
534
|
+
readableRoots: [],
|
|
535
|
+
writableRoots: [],
|
|
536
|
+
metadata: {
|
|
537
|
+
sandboxName,
|
|
538
|
+
estimatedNanoUsd: estimatedNanoUsd.toString(),
|
|
539
|
+
quotaKey,
|
|
540
|
+
slotKey,
|
|
541
|
+
/**
|
|
542
|
+
* release() 只 stop 不删除:沙盒在云端以 stopped 状态保留,
|
|
543
|
+
* 本后端从不发 `sbx rm`。调用方据此判断是否需要另行清理。
|
|
544
|
+
*/
|
|
545
|
+
sandboxRetainedOnRelease: true
|
|
546
|
+
}
|
|
547
|
+
};
|
|
548
|
+
leases.set(lease.leaseId, {
|
|
549
|
+
ticketId: ticket.ticketId,
|
|
550
|
+
grantId: ticket.grantId,
|
|
551
|
+
sandboxName,
|
|
552
|
+
quotaKey,
|
|
553
|
+
slotKey,
|
|
554
|
+
estimatedNanoUsd,
|
|
555
|
+
startedAt,
|
|
556
|
+
released: false
|
|
557
|
+
});
|
|
558
|
+
return lease;
|
|
559
|
+
});
|
|
560
|
+
},
|
|
561
|
+
async execute(lease, request, onEvent) {
|
|
562
|
+
const entry = leases.get(lease.leaseId);
|
|
563
|
+
if (!entry) throw new DockerSandboxBackendError("lease-not-found");
|
|
564
|
+
if (lease.backendId !== descriptor.id || lease.ticketId !== entry.ticketId) {
|
|
565
|
+
throw new DockerSandboxBackendError("lease-mismatch");
|
|
566
|
+
}
|
|
567
|
+
if (entry.released) throw new DockerSandboxBackendError("lease-released");
|
|
568
|
+
if (request.kind !== "command") {
|
|
569
|
+
throw new DockerSandboxBackendError("unsupported-request");
|
|
570
|
+
}
|
|
571
|
+
if (request.env !== void 0 || request.stdin !== void 0 || request.cwd !== void 0) {
|
|
572
|
+
throw new DockerSandboxBackendError("unsupported-request", "request options not honored");
|
|
573
|
+
}
|
|
574
|
+
const tierExecTimeoutMs = isShellArgv(request.argv) ? shellExecTimeoutMs : defaultExecTimeoutMs;
|
|
575
|
+
const requestedTimeoutMs = request.timeoutMs;
|
|
576
|
+
if (requestedTimeoutMs !== void 0) {
|
|
577
|
+
if (!Number.isFinite(requestedTimeoutMs) || requestedTimeoutMs <= 0) {
|
|
578
|
+
throw new DockerSandboxBackendError(
|
|
579
|
+
"unsupported-request",
|
|
580
|
+
"timeoutMs must be a positive finite number"
|
|
581
|
+
);
|
|
582
|
+
}
|
|
583
|
+
if (requestedTimeoutMs > tierExecTimeoutMs) {
|
|
584
|
+
throw new DockerSandboxBackendError(
|
|
585
|
+
"unsupported-request",
|
|
586
|
+
"timeoutMs exceeds the backend execution bound"
|
|
587
|
+
);
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
const execBoundMs = requestedTimeoutMs ?? tierExecTimeoutMs;
|
|
591
|
+
const executionId = `${entry.sandboxName}:${now().toString(36)}`;
|
|
592
|
+
const startedAt = now();
|
|
593
|
+
onEvent?.({ type: "started", executionId, at: startedAt });
|
|
594
|
+
const script = argvToShellScript(request.argv);
|
|
595
|
+
const outcome = await runSbx(
|
|
596
|
+
["--cloud", "exec", entry.sandboxName, "bash", "-lc", script],
|
|
597
|
+
{ timeoutMs: execBoundMs }
|
|
598
|
+
);
|
|
599
|
+
if (outcome.timedOut) {
|
|
600
|
+
throw new DockerSandboxBackendError("exec-timeout");
|
|
601
|
+
}
|
|
602
|
+
if (outcome.exitCode === null) {
|
|
603
|
+
throw new DockerSandboxBackendError("exec-failed");
|
|
604
|
+
}
|
|
605
|
+
const stdout = truncateOutput(outcome.stdout, maxOutputChars);
|
|
606
|
+
const stderr = truncateOutput(outcome.stderr, maxOutputChars);
|
|
607
|
+
if (stdout) onEvent?.({ type: "stdout", executionId, chunk: stdout, seq: 1 });
|
|
608
|
+
if (stderr) onEvent?.({ type: "stderr", executionId, chunk: stderr, seq: 1 });
|
|
609
|
+
onEvent?.({
|
|
610
|
+
type: "usage",
|
|
611
|
+
executionId,
|
|
612
|
+
usage: { wallMs: Math.max(0, now() - startedAt) }
|
|
613
|
+
});
|
|
614
|
+
onEvent?.({ type: "exited", executionId, exitCode: outcome.exitCode });
|
|
615
|
+
return {
|
|
616
|
+
executionId,
|
|
617
|
+
ok: outcome.exitCode === 0,
|
|
618
|
+
exitCode: outcome.exitCode,
|
|
619
|
+
output: { stdout, stderr }
|
|
620
|
+
};
|
|
621
|
+
},
|
|
622
|
+
async release(lease, reason) {
|
|
623
|
+
const entry = leases.get(lease.leaseId);
|
|
624
|
+
if (!entry || entry.released) return;
|
|
625
|
+
if (lease.backendId !== descriptor.id || lease.ticketId !== entry.ticketId) {
|
|
626
|
+
throw new DockerSandboxBackendError("lease-mismatch");
|
|
627
|
+
}
|
|
628
|
+
const stop = await runSbx(["--cloud", "stop", entry.sandboxName], {
|
|
629
|
+
timeoutMs: controlTimeoutMs
|
|
630
|
+
});
|
|
631
|
+
if (stop.exitCode !== 0 || !await confirmDockerSandboxStopped(runSbx, entry.sandboxName, controlTimeoutMs)) {
|
|
632
|
+
throw new DockerSandboxBackendError("stop-unverified", void 0, true);
|
|
633
|
+
}
|
|
634
|
+
entry.released = true;
|
|
635
|
+
await persistQuotaRecord("release-final-state", entry.slotKey, {
|
|
636
|
+
status: reason === "failed" ? "failed" : "stopped",
|
|
637
|
+
pendingEstimateNanoUsd: "0",
|
|
638
|
+
estimatedSpendNanoUsd: entry.estimatedNanoUsd.toString(),
|
|
639
|
+
sandbox: entry.sandboxName
|
|
640
|
+
});
|
|
641
|
+
}
|
|
642
|
+
};
|
|
643
|
+
}
|
|
644
|
+
function parseSbxListingJson(stdout) {
|
|
645
|
+
const text = stdout ?? "";
|
|
646
|
+
let start = -1;
|
|
647
|
+
for (let i = 0; i < text.length; i += 1) {
|
|
648
|
+
const ch = text[i];
|
|
649
|
+
if (ch === "[" || ch === "{") {
|
|
650
|
+
start = i;
|
|
651
|
+
break;
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
if (start < 0) return void 0;
|
|
655
|
+
return JSON.parse(text.slice(start));
|
|
656
|
+
}
|
|
657
|
+
async function probeCloudSandboxPresence(runSbx, timeoutMs) {
|
|
658
|
+
try {
|
|
659
|
+
const listing = await runSbx(["--cloud", "ls", "--format", "json"], { timeoutMs });
|
|
660
|
+
if (listing.exitCode !== 0 || listing.timedOut) return { ok: false, reason: "ls-failed" };
|
|
661
|
+
const parsed = parseSbxListingJson(listing.stdout);
|
|
662
|
+
if (!Array.isArray(parsed)) return { ok: false, reason: "malformed-listing" };
|
|
663
|
+
const names = /* @__PURE__ */ new Set();
|
|
664
|
+
for (const entry of parsed) {
|
|
665
|
+
if (!entry || typeof entry !== "object") return { ok: false, reason: "malformed-listing" };
|
|
666
|
+
const name = entry.name;
|
|
667
|
+
if (typeof name !== "string" || !name) return { ok: false, reason: "malformed-listing" };
|
|
668
|
+
names.add(name);
|
|
669
|
+
}
|
|
670
|
+
return { ok: true, names };
|
|
671
|
+
} catch {
|
|
672
|
+
return { ok: false, reason: "ls-failed" };
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
async function confirmDockerSandboxStopped(runSbx, sandboxName, timeoutMs = 1e4, options = {}) {
|
|
676
|
+
const attempts = Math.max(1, options.attempts ?? 1);
|
|
677
|
+
const delayMs = Math.max(0, options.delayMs ?? 0);
|
|
678
|
+
for (let attempt = 0; attempt < attempts; attempt += 1) {
|
|
679
|
+
const verdict = await probeOnce(runSbx, sandboxName, timeoutMs);
|
|
680
|
+
if (verdict !== "pending") return verdict;
|
|
681
|
+
if (attempt < attempts - 1 && delayMs > 0) {
|
|
682
|
+
await new Promise((resolve2) => setTimeout(resolve2, delayMs));
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
return false;
|
|
686
|
+
}
|
|
687
|
+
async function probeOnce(runSbx, sandboxName, timeoutMs) {
|
|
688
|
+
try {
|
|
689
|
+
const listing = await runSbx(["--cloud", "ls", "--format", "json"], { timeoutMs });
|
|
690
|
+
if (listing.exitCode !== 0 || listing.timedOut) return false;
|
|
691
|
+
const parsed = parseSbxListingJson(listing.stdout);
|
|
692
|
+
if (!Array.isArray(parsed)) return false;
|
|
693
|
+
const matches = parsed.filter((x) => x && typeof x === "object" && x.name === sandboxName);
|
|
694
|
+
if (matches.length === 0) return true;
|
|
695
|
+
if (matches.length === 1 && matches[0].status === "stopped") return true;
|
|
696
|
+
return matches.length === 1 ? "pending" : false;
|
|
697
|
+
} catch {
|
|
698
|
+
return false;
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
export {
|
|
703
|
+
FileDockerQuotaKvStore,
|
|
704
|
+
DOCKER_SANDBOX_BACKEND_ID,
|
|
705
|
+
resolveMaxActiveSandboxesFromEnv,
|
|
706
|
+
defaultRunSbx,
|
|
707
|
+
createDockerSandboxExecutionBackend,
|
|
708
|
+
confirmDockerSandboxStopped
|
|
709
|
+
};
|