cmdbox 0.5.4__py3-none-any.whl → 0.6.0__py3-none-any.whl

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.

Potentially problematic release.


This version of cmdbox might be problematic. Click here for more details.

Files changed (143) hide show
  1. cmdbox/app/auth/signin.py +463 -303
  2. cmdbox/app/common.py +48 -3
  3. cmdbox/app/edge.py +5 -173
  4. cmdbox/app/edge_tool.py +177 -0
  5. cmdbox/app/feature.py +10 -9
  6. cmdbox/app/features/cli/agent_base.py +477 -0
  7. cmdbox/app/features/cli/audit_base.py +1 -1
  8. cmdbox/app/features/cli/cmdbox_audit_search.py +24 -1
  9. cmdbox/app/features/cli/cmdbox_client_file_download.py +1 -1
  10. cmdbox/app/features/cli/cmdbox_cmd_list.py +105 -0
  11. cmdbox/app/features/cli/cmdbox_cmd_load.py +104 -0
  12. cmdbox/app/features/cli/cmdbox_edge_config.py +2 -2
  13. cmdbox/app/features/cli/cmdbox_edge_start.py +1 -1
  14. cmdbox/app/features/cli/cmdbox_gui_start.py +9 -132
  15. cmdbox/app/features/cli/cmdbox_gui_stop.py +4 -21
  16. cmdbox/app/features/cli/cmdbox_server_start.py +1 -1
  17. cmdbox/app/features/cli/cmdbox_web_apikey_add.py +1 -1
  18. cmdbox/app/features/cli/cmdbox_web_apikey_del.py +1 -1
  19. cmdbox/app/features/cli/cmdbox_web_genpass.py +0 -3
  20. cmdbox/app/features/cli/cmdbox_web_group_add.py +1 -1
  21. cmdbox/app/features/cli/cmdbox_web_group_del.py +1 -1
  22. cmdbox/app/features/cli/cmdbox_web_group_edit.py +1 -1
  23. cmdbox/app/features/cli/cmdbox_web_group_list.py +1 -1
  24. cmdbox/app/features/cli/cmdbox_web_start.py +119 -104
  25. cmdbox/app/features/cli/cmdbox_web_stop.py +1 -1
  26. cmdbox/app/features/cli/cmdbox_web_user_add.py +1 -1
  27. cmdbox/app/features/cli/cmdbox_web_user_del.py +1 -1
  28. cmdbox/app/features/cli/cmdbox_web_user_edit.py +1 -1
  29. cmdbox/app/features/cli/cmdbox_web_user_list.py +1 -1
  30. cmdbox/app/features/web/cmdbox_web_agent.py +250 -0
  31. cmdbox/app/features/web/cmdbox_web_exec_cmd.py +8 -3
  32. cmdbox/app/features/web/cmdbox_web_signin.py +3 -3
  33. cmdbox/app/features/web/cmdbox_web_users.py +2 -0
  34. cmdbox/app/options.py +55 -2
  35. cmdbox/app/web.py +142 -15
  36. cmdbox/extensions/features.yml +18 -0
  37. cmdbox/extensions/sample_project/sample/app/features/cli/__init__.py +0 -0
  38. cmdbox/extensions/sample_project/sample/app/features/web/__init__.py +0 -0
  39. cmdbox/extensions/user_list.yml +1 -0
  40. cmdbox/licenses/LICENSE.Authlib.1.5.2(BSD License).txt +29 -0
  41. cmdbox/licenses/LICENSE.Deprecated.1.2.18(MIT License).txt +21 -0
  42. cmdbox/licenses/LICENSE.SQLAlchemy.2.0.40(MIT License).txt +19 -0
  43. cmdbox/licenses/LICENSE.aiohappyeyeballs.2.6.1(Python Software Foundation License).txt +279 -0
  44. cmdbox/licenses/LICENSE.aiohttp.3.11.18(Apache Software License).txt +13 -0
  45. cmdbox/licenses/LICENSE.aiosignal.1.3.2(Apache Software License).txt +201 -0
  46. cmdbox/licenses/LICENSE.attrs.25.3.0(UNKNOWN).txt +21 -0
  47. cmdbox/licenses/LICENSE.cachetools.5.5.2(MIT License).txt +20 -0
  48. cmdbox/licenses/LICENSE.distro.1.9.0(Apache Software License).txt +202 -0
  49. cmdbox/licenses/LICENSE.docstring_parser.0.16(MIT License).txt +21 -0
  50. cmdbox/licenses/LICENSE.filelock.3.18.0(The Unlicense (Unlicense)).txt +24 -0
  51. cmdbox/licenses/LICENSE.frozenlist.1.6.0(Apache-2.0).txt +201 -0
  52. cmdbox/licenses/LICENSE.fsspec.2025.3.2(BSD License).txt +29 -0
  53. cmdbox/licenses/LICENSE.google-adk.0.5.0(Apache Software License).txt +202 -0
  54. cmdbox/licenses/LICENSE.google-api-python-client.2.169.0(Apache Software License).txt +201 -0
  55. cmdbox/licenses/LICENSE.google-auth-httplib2.0.2.0(Apache Software License).txt +201 -0
  56. cmdbox/licenses/LICENSE.google-auth.2.40.1(Apache Software License).txt +201 -0
  57. cmdbox/licenses/LICENSE.google-cloud-aiplatform.1.92.0(Apache 2.0).txt +202 -0
  58. cmdbox/licenses/LICENSE.google-cloud-bigquery.3.31.0(Apache Software License).txt +202 -0
  59. cmdbox/licenses/LICENSE.google-cloud-core.2.4.3(Apache Software License).txt +202 -0
  60. cmdbox/licenses/LICENSE.google-cloud-resource-manager.1.14.2(Apache Software License).txt +202 -0
  61. cmdbox/licenses/LICENSE.google-cloud-secret-manager.2.23.3(Apache Software License).txt +202 -0
  62. cmdbox/licenses/LICENSE.google-cloud-speech.2.32.0(Apache Software License).txt +202 -0
  63. cmdbox/licenses/LICENSE.google-cloud-storage.2.19.0(Apache Software License).txt +202 -0
  64. cmdbox/licenses/LICENSE.google-cloud-trace.1.16.1(Apache Software License).txt +202 -0
  65. cmdbox/licenses/LICENSE.google-crc32c.1.7.1(Apache 2.0).txt +202 -0
  66. cmdbox/licenses/LICENSE.google-genai.1.14.0(Apache Software License).txt +202 -0
  67. cmdbox/licenses/LICENSE.google-resumable-media.2.7.2(Apache Software License).txt +202 -0
  68. cmdbox/licenses/LICENSE.googleapis-common-protos.1.70.0(Apache Software License).txt +202 -0
  69. cmdbox/licenses/LICENSE.graphviz.0.20.3(MIT License).txt +21 -0
  70. cmdbox/licenses/LICENSE.grpc-google-iam-v1.0.14.2(Apache Software License).txt +202 -0
  71. cmdbox/licenses/LICENSE.grpcio-status.1.71.0(Apache Software License).txt +610 -0
  72. cmdbox/licenses/LICENSE.grpcio.1.71.0(Apache Software License).txt +610 -0
  73. cmdbox/licenses/LICENSE.httpcore.1.0.9(BSD License).txt +27 -0
  74. cmdbox/licenses/LICENSE.httplib2.0.22.0(MIT License).txt +23 -0
  75. cmdbox/licenses/LICENSE.httpx-sse.0.4.0(MIT).txt +21 -0
  76. cmdbox/licenses/LICENSE.httpx.0.28.1(BSD License).txt +12 -0
  77. cmdbox/licenses/LICENSE.huggingface-hub.0.31.1(Apache Software License).txt +201 -0
  78. cmdbox/licenses/LICENSE.importlib_metadata.8.6.1(Apache Software License).txt +202 -0
  79. cmdbox/licenses/LICENSE.jiter.0.9.0(MIT License).txt +1 -0
  80. cmdbox/licenses/LICENSE.jsonschema-specifications.2025.4.1(UNKNOWN).txt +19 -0
  81. cmdbox/licenses/LICENSE.jsonschema.4.23.0(MIT License).txt +19 -0
  82. cmdbox/licenses/LICENSE.litellm.1.69.0(MIT License).txt +26 -0
  83. cmdbox/licenses/LICENSE.mcp.1.8.0(MIT License).txt +21 -0
  84. cmdbox/licenses/LICENSE.multidict.6.4.3(Apache Software License).txt +13 -0
  85. cmdbox/licenses/LICENSE.openai.1.75.0(Apache Software License).txt +201 -0
  86. cmdbox/licenses/LICENSE.opentelemetry-api.1.33.0(Apache Software License).txt +201 -0
  87. cmdbox/licenses/LICENSE.opentelemetry-exporter-gcp-trace.1.9.0(Apache Software License).txt +201 -0
  88. cmdbox/licenses/LICENSE.opentelemetry-resourcedetector-gcp.1.9.0a0(Apache Software License).txt +201 -0
  89. cmdbox/licenses/LICENSE.opentelemetry-sdk.1.33.0(Apache Software License).txt +201 -0
  90. cmdbox/licenses/LICENSE.opentelemetry-semantic-conventions.0.54b0(Apache Software License).txt +201 -0
  91. cmdbox/licenses/LICENSE.propcache.0.3.1(Apache Software License).txt +202 -0
  92. cmdbox/licenses/LICENSE.proto-plus.1.26.1(Apache Software License).txt +202 -0
  93. cmdbox/licenses/LICENSE.protobuf.5.29.4(3-Clause BSD License).txt +32 -0
  94. cmdbox/licenses/LICENSE.pyasn1.0.6.1(BSD License).txt +24 -0
  95. cmdbox/licenses/LICENSE.pyasn1_modules.0.4.2(BSD License).txt +24 -0
  96. cmdbox/licenses/LICENSE.pydantic-settings.2.9.1(MIT License).txt +21 -0
  97. cmdbox/licenses/LICENSE.pyparsing.3.2.3(MIT License).txt +18 -0
  98. cmdbox/licenses/LICENSE.python-dateutil.2.9.0.post0(Apache Software License; BSD License).txt +54 -0
  99. cmdbox/licenses/LICENSE.referencing.0.36.2(UNKNOWN).txt +19 -0
  100. cmdbox/licenses/LICENSE.regex.2024.11.6(Apache Software License).txt +208 -0
  101. cmdbox/licenses/LICENSE.rpds-py.0.24.0(MIT).txt +19 -0
  102. cmdbox/licenses/LICENSE.rsa.4.9.1(Apache Software License).txt +13 -0
  103. cmdbox/licenses/LICENSE.shapely.2.1.0(BSD License).txt +29 -0
  104. cmdbox/licenses/LICENSE.sse-starlette.2.3.4(BSD License).txt +27 -0
  105. cmdbox/licenses/LICENSE.tiktoken.0.9.0(MIT License).txt +21 -0
  106. cmdbox/licenses/LICENSE.tokenizers.0.21.1(Apache Software License).txt +1 -0
  107. cmdbox/licenses/LICENSE.tqdm.4.67.1(MIT License; Mozilla Public License 2.0 (MPL 2.0)).txt +49 -0
  108. cmdbox/licenses/LICENSE.tzlocal.5.3.1(MIT License).txt +19 -0
  109. cmdbox/licenses/LICENSE.uritemplate.4.1.1(Apache Software License; BSD License).txt +3 -0
  110. cmdbox/licenses/LICENSE.wrapt.1.17.2(BSD License).txt +24 -0
  111. cmdbox/licenses/LICENSE.yarl.1.20.0(Apache Software License).txt +202 -0
  112. cmdbox/licenses/files.txt +104 -11
  113. cmdbox/logconf_agent.yml +38 -0
  114. cmdbox/logconf_audit.yml +13 -5
  115. cmdbox/logconf_client.yml +13 -5
  116. cmdbox/logconf_cmdbox.yml +13 -5
  117. cmdbox/logconf_edge.yml +13 -5
  118. cmdbox/logconf_gui.yml +13 -5
  119. cmdbox/logconf_server.yml +13 -5
  120. cmdbox/logconf_web.yml +13 -5
  121. cmdbox/version.py +3 -2
  122. cmdbox/web/agent.html +263 -0
  123. cmdbox/web/assets/cmdbox/agent.js +335 -0
  124. cmdbox/web/assets/cmdbox/common.js +1111 -1020
  125. cmdbox/web/assets/cmdbox/signin.js +4 -4
  126. cmdbox/web/assets/filer/filer.js +4 -2
  127. {cmdbox-0.5.4.dist-info → cmdbox-0.6.0.dist-info}/METADATA +69 -26
  128. {cmdbox-0.5.4.dist-info → cmdbox-0.6.0.dist-info}/RECORD +143 -61
  129. /cmdbox/licenses/{LICENSE.charset-normalizer.3.4.1(MIT License).txt → LICENSE.charset-normalizer.3.4.2(MIT License).txt} +0 -0
  130. /cmdbox/licenses/{LICENSE.click.8.1.8(BSD License).txt → LICENSE.click.8.2.0(UNKNOWN).txt} +0 -0
  131. /cmdbox/licenses/{LICENSE.cryptography.44.0.2(Apache Software License; BSD License).txt → LICENSE.cryptography.44.0.3(Apache Software License; BSD License).txt} +0 -0
  132. /cmdbox/licenses/{LICENSE.importlib_metadata.8.7.0(Apache Software License).txt → LICENSE.google-api-core.2.24.2(Apache Software License).txt} +0 -0
  133. /cmdbox/licenses/{LICENSE.greenlet.3.2.1(MIT AND Python-2.0).txt → LICENSE.greenlet.3.2.2(MIT AND Python-2.0).txt} +0 -0
  134. /cmdbox/licenses/{LICENSE.psycopg-binary.3.2.6(GNU Lesser General Public License v3 (LGPLv3)).txt → LICENSE.psycopg-binary.3.2.7(GNU Lesser General Public License v3 (LGPLv3)).txt} +0 -0
  135. /cmdbox/licenses/{LICENSE.psycopg.3.2.6(GNU Lesser General Public License v3 (LGPLv3)).txt → LICENSE.psycopg.3.2.7(GNU Lesser General Public License v3 (LGPLv3)).txt} +0 -0
  136. /cmdbox/licenses/{LICENSE.pydantic.2.11.3(MIT License).txt → LICENSE.pydantic.2.11.4(MIT License).txt} +0 -0
  137. /cmdbox/licenses/{LICENSE.pydantic_core.2.33.1(MIT License).txt → LICENSE.pydantic_core.2.33.2(MIT License).txt} +0 -0
  138. /cmdbox/licenses/{LICENSE.redis.5.2.1(MIT License).txt → LICENSE.redis.6.0.0(MIT License).txt} +0 -0
  139. /cmdbox/licenses/{LICENSE.snowballstemmer.2.2.0(BSD License).txt → LICENSE.snowballstemmer.3.0.1(BSD License).txt} +0 -0
  140. {cmdbox-0.5.4.dist-info → cmdbox-0.6.0.dist-info}/LICENSE +0 -0
  141. {cmdbox-0.5.4.dist-info → cmdbox-0.6.0.dist-info}/WHEEL +0 -0
  142. {cmdbox-0.5.4.dist-info → cmdbox-0.6.0.dist-info}/entry_points.txt +0 -0
  143. {cmdbox-0.5.4.dist-info → cmdbox-0.6.0.dist-info}/top_level.txt +0 -0
@@ -4,6 +4,7 @@ from cmdbox.app.features.cli import cmdbox_audit_search, cmdbox_audit_write
4
4
  from cmdbox.app.features.web import cmdbox_web_load_cmd
5
5
  from cmdbox.app.web import Web
6
6
  from fastapi import FastAPI, Request, Response, HTTPException
7
+ from fastapi.responses import PlainTextResponse
7
8
  from starlette.datastructures import UploadFile
8
9
  from typing import Dict, Any, List
9
10
  import html
@@ -178,8 +179,10 @@ class ExecCmd(cmdbox_web_load_cmd.LoadCmd):
178
179
  sys.stdout = captured_output = io.StringIO()
179
180
  ret_main = {}
180
181
  logsize = 1024
182
+ console = common.create_console(file=old_stdout)
183
+
181
184
  try:
182
- old_stdout.write(loghandler.colorize_msg(f'EXEC: {opt_list}\n'[:logsize]))
185
+ console.log(f'EXEC: {opt_list}\n'[:logsize])
183
186
  status, ret_main, obj = cmdbox_app.main(args_list=[common.chopdq(o) for o in opt_list], file_dict=file_dict, webcall=True)
184
187
  if isinstance(obj, server.Server):
185
188
  cmdbox_app.sv = obj
@@ -206,11 +209,11 @@ class ExecCmd(cmdbox_web_load_cmd.LoadCmd):
206
209
  output = [dict(warn=f'The captured stdout was discarded because its size was larger than {capture_maxsize} bytes.')]
207
210
  else:
208
211
  output = [dict(warn='capture_stdout is off.')]
209
- old_stdout.write(loghandler.colorize_msg(f'EXEC: {output}'[:logsize])+'\n')
212
+ console.log(f'EXEC: {output}'[:logsize])
210
213
  except Exception as e:
211
214
  web.logger.disabled = False # ログ出力を有効にする
212
215
  msg = f'exec_cmd error. {traceback.format_exc()}'
213
- old_stdout.write(loghandler.colorize_msg(f'EXEC: {msg}'[:logsize])+'\n')
216
+ console.log(f'EXEC: {msg}'[:logsize])
214
217
  web.logger.warning(msg)
215
218
  output = [dict(warn=f'<pre>{html.escape(traceback.format_exc())}</pre>')]
216
219
  sys.stdout = old_stdout
@@ -234,6 +237,8 @@ class ExecCmd(cmdbox_web_load_cmd.LoadCmd):
234
237
  except:
235
238
  ret = ret_main
236
239
  if nothread:
240
+ if isinstance(ret, str):
241
+ return PlainTextResponse(ret, media_type='text/plain')
237
242
  return ret
238
243
  self.callback_return_cmd_exec_func(web, title, ret)
239
244
  except:
@@ -1,4 +1,5 @@
1
1
  from cmdbox.app import feature
2
+ from cmdbox.app.auth import signin
2
3
  from cmdbox.app.web import Web
3
4
  from fastapi import FastAPI, Request, Response, HTTPException
4
5
  from fastapi.responses import HTMLResponse, RedirectResponse
@@ -22,10 +23,9 @@ class Signin(feature.WebFeature):
22
23
  with open(web.signin_html, 'r', encoding='utf-8') as f:
23
24
  web.signin_html_data = f.read()
24
25
 
25
- @app.get('/signin/{next}', response_class=HTMLResponse)
26
- @app.post('/signin/{next}', response_class=HTMLResponse)
26
+ @app.api_route('/signin/{next}', methods=['GET', 'POST'], response_class=HTMLResponse)
27
27
  async def _signin(next:str, req:Request, res:Response):
28
- web.signin.enable_cors(req, res)
28
+ signin.Signin._enable_cors(req, res)
29
29
  res.headers['Access-Control-Allow-Origin'] = '*'
30
30
  return web.signin_html_data
31
31
 
@@ -88,6 +88,7 @@ class Users(feature.WebFeature):
88
88
  except Exception as e:
89
89
  return dict(error=str(e))
90
90
 
91
+ @app.post('/gui/apikey/add')
91
92
  @app.post('/users/apikey/add')
92
93
  async def users_apikey_add(req:Request, res:Response):
93
94
  signin = web.signin.check_signin(req, res)
@@ -103,6 +104,7 @@ class Users(feature.WebFeature):
103
104
  except Exception as e:
104
105
  return dict(error=str(e))
105
106
 
107
+ @app.post('/gui/apikey/del')
106
108
  @app.post('/users/apikey/del')
107
109
  async def users_apikey_del(req:Request, res:Response):
108
110
  signin = web.signin.check_signin(req, res)
cmdbox/app/options.py CHANGED
@@ -1,6 +1,6 @@
1
1
  from cmdbox.app import common, feature, web
2
2
  from cmdbox.app.commons import module
3
- from fastapi import Request
3
+ from fastapi import Request, WebSocket
4
4
  from fastapi.routing import APIRoute
5
5
  from datetime import datetime
6
6
  from pathlib import Path
@@ -49,6 +49,7 @@ class Options:
49
49
  self.aliases_loaded_cli = False
50
50
  self.aliases_loaded_web = False
51
51
  self.audit_loaded = False
52
+ self.agentrule_loaded = False
52
53
  self.init_options()
53
54
 
54
55
  def get_mode_keys(self) -> List[str]:
@@ -345,6 +346,7 @@ class Options:
345
346
  svcmd = fobj.get_svcmd()
346
347
  if svcmd is not None:
347
348
  self._options["svcmd"][svcmd] = fobj
349
+ opt['use_agent'] = self.check_agentrule(mode, cmd, logger)
348
350
  self.init_debugoption()
349
351
 
350
352
  def is_features_loaded(self, ftype:str) -> bool:
@@ -396,6 +398,8 @@ class Options:
396
398
  return
397
399
  if type(yml['features'][ftype]) is not list:
398
400
  raise Exception(f'features.yml is invalid. (The “features.{ftype} element must be a list. {ftype}={yml["features"][ftype]})')
401
+ # featureモジュール読込みの前にagentruleの読み込み
402
+ self.load_features_agentrule(logger)
399
403
  for data in yml['features'][ftype]:
400
404
  if type(data) is not dict:
401
405
  raise Exception(f'features.yml is invalid. (The “features.{ftype}” element must be a list element must be a dictionary. data={data})')
@@ -415,6 +419,7 @@ class Options:
415
419
  func(data['package'], data['prefix'], exclude_modules, appcls, ver, logger, self.is_features_loaded(ftype))
416
420
  self.features_loaded[ftype] = True
417
421
 
422
+
418
423
  def load_features_args(self, args_dict:Dict[str, Any]):
419
424
  yml = self.features_yml_data
420
425
  if yml is None:
@@ -658,6 +663,54 @@ class Options:
658
663
  self.audit_search_args['cmd'] = cmd
659
664
  self.audit_loaded = True
660
665
 
666
+ def load_features_agentrule(self, logger:logging.Logger):
667
+ yml = self.features_yml_data
668
+ if yml is None: return
669
+ if self.agentrule_loaded: return
670
+ if 'agentrule' not in yml: return
671
+ if 'policy' not in yml['agentrule']:
672
+ raise Exception('features.yml is invalid. (The agentrule element must have "policy" specified.)')
673
+ if yml['agentrule']['policy'] not in ['allow', 'deny']:
674
+ raise Exception('features.yml is invalid. (The policy element must specify allow or deny.)')
675
+ if 'rules' not in yml['agentrule']:
676
+ raise Exception('features.yml is invalid. (The agentrule element must have "rules" specified.)')
677
+ for rule in yml['agentrule']['rules']:
678
+ if 'mode' not in rule:
679
+ rule['mode'] = None
680
+ if 'cmds' not in rule:
681
+ rule['cmds'] = []
682
+ if rule['mode'] is None and len(rule['cmds']) > 0:
683
+ raise Exception('features.yml is invalid. (When “cmds” is specified, “mode” must be specified.)')
684
+ if 'rule' not in rule:
685
+ raise Exception('features.yml is invalid. (The agentrule.rules element must have "rule" specified.)')
686
+ self.agentrule_loaded = True
687
+
688
+ def check_agentrule(self, mode:str, cmd:str, logger:logging.Logger) -> bool:
689
+ """
690
+ エージェントが使用してよいコマンドかどうかをチェックします
691
+
692
+ Args:
693
+ mode (str): モード
694
+ cmd (str): コマンド
695
+
696
+ Returns:
697
+ bool: 認可されたかどうか
698
+ """
699
+ if not self.agentrule_loaded:
700
+ return False
701
+ # コマンドチェック
702
+ jadge = self.features_yml_data['agentrule']['policy']
703
+ for rule in self.features_yml_data['agentrule']['rules']:
704
+ if rule['mode'] is not None:
705
+ if rule['mode'] != mode:
706
+ continue
707
+ if len([c for c in rule['cmds'] if cmd == c]) <= 0:
708
+ continue
709
+ jadge = rule['rule']
710
+ if logger.level == logging.DEBUG:
711
+ logger.debug(f"agent rule: mode={mode}, cmd={cmd}: {jadge}")
712
+ return jadge == 'allow'
713
+
661
714
  AT_USER = 'user'
662
715
  AT_ADMIN = 'admin'
663
716
  AT_SYSTEM = 'system'
@@ -779,7 +832,7 @@ class Options:
779
832
  elif isinstance(arg, feature.Feature):
780
833
  func_feature = arg
781
834
  opt['clmsg_src'] = func_feature.__class__.__name__
782
- elif isinstance(arg, Request):
835
+ elif isinstance(arg, Request) or isinstance(arg, WebSocket):
783
836
  if 'signin' in arg.session and arg.session['signin'] is not None and 'name' in arg.session['signin']:
784
837
  opt['clmsg_user'] = arg.session['signin']['name']
785
838
  if opt['audit_type'] is None:
cmdbox/app/web.py CHANGED
@@ -1,11 +1,12 @@
1
1
  from cmdbox.app import common, options
2
2
  from cmdbox.app.auth import signin, signin_saml
3
3
  from cmdbox.app.commons import module
4
- from fastapi import FastAPI, Request, Response, HTTPException
5
- from fastapi.responses import RedirectResponse
4
+ from fastapi import FastAPI, Request, Response
6
5
  from pathlib import Path
6
+ from starlette.applications import Starlette
7
7
  from starlette.middleware.sessions import SessionMiddleware
8
- from typing import Any, Dict, List, Tuple
8
+ from starlette.routing import Mount
9
+ from typing import Any, Dict, List
9
10
  from uvicorn.config import Config
10
11
  import asyncio
11
12
  import copy
@@ -31,7 +32,7 @@ class Web:
31
32
  def __init__(self, logger:logging.Logger, data:Path, appcls=None, ver=None,
32
33
  redis_host:str = "localhost", redis_port:int = 6379, redis_password:str = None, svname:str = 'server',
33
34
  client_only:bool=False, doc_root:Path=None, gui_html:str=None, filer_html:str=None, result_html:str=None, users_html:str=None,
34
- audit_html:str=None, assets:List[str]=None, signin_html:str=None, signin_file:str=None, gui_mode:bool=False,
35
+ audit_html:str=None, agent_html:str=None, assets:List[str]=None, signin_html:str=None, signin_file:str=None, gui_mode:bool=False,
35
36
  web_features_packages:List[str]=None, web_features_prefix:List[str]=None):
36
37
  """
37
38
  cmdboxクライアント側のwebapiサービス
@@ -52,6 +53,7 @@ class Web:
52
53
  result_html (str, optional): 結果のHTMLファイル. Defaults to None.
53
54
  users_html (str, optional): ユーザーのHTMLファイル. Defaults to None.
54
55
  audit_html (str, optional): 監査のHTMLファイル. Defaults to None.
56
+ agent_html (str, optional): エージェントのHTMLファイル. Defaults to None.
55
57
  assets (List[str], optional): 静的ファイルのリスト. Defaults to None.
56
58
  signin_html (str, optional): ログイン画面のHTMLファイル. Defaults to None.
57
59
  signin_file (str, optional): ログイン情報のファイル. Defaults to args.signin_file.
@@ -78,6 +80,7 @@ class Web:
78
80
  self.result_html = Path(result_html) if result_html is not None else Path(__file__).parent.parent / 'web' / 'result.html'
79
81
  self.users_html = Path(users_html) if users_html is not None else Path(__file__).parent.parent / 'web' / 'users.html'
80
82
  self.audit_html = Path(audit_html) if audit_html is not None else Path(__file__).parent.parent / 'web' / 'audit.html'
83
+ self.agent_html = Path(agent_html) if agent_html is not None else Path(__file__).parent.parent / 'web' / 'agent.html'
81
84
  self.assets = []
82
85
  if assets is not None:
83
86
  if not isinstance(assets, list):
@@ -97,6 +100,7 @@ class Web:
97
100
  self.result_html_data = None
98
101
  self.users_html_data = None
99
102
  self.audit_html_data = None
103
+ self.agent_html_data = None
100
104
  self.assets_data = None
101
105
  self.signin_html_data = None
102
106
  self.gui_mode = gui_mode
@@ -106,11 +110,13 @@ class Web:
106
110
  self.pipes_path = self.data / ".pipes"
107
111
  self.users_path = self.data / ".users"
108
112
  self.audit_path = self.data / '.audit'
113
+ self.agent_path = self.data / '.agent'
109
114
  self.static_root = Path(__file__).parent.parent / 'web'
110
115
  common.mkdirs(self.cmds_path)
111
116
  common.mkdirs(self.pipes_path)
112
117
  common.mkdirs(self.users_path)
113
118
  common.mkdirs(self.audit_path)
119
+ common.mkdirs(self.agent_path)
114
120
  self.pipe_th = None
115
121
  self.img_queue = queue.Queue(1000)
116
122
  self.cb_queue = queue.Queue(1000)
@@ -132,6 +138,7 @@ class Web:
132
138
  self.logger.debug(f"web init parameter: result_html={self.result_html} -> {self.result_html.absolute() if self.result_html is not None else None}")
133
139
  self.logger.debug(f"web init parameter: users_html={self.users_html} -> {self.users_html.absolute() if self.users_html is not None else None}")
134
140
  self.logger.debug(f"web init parameter: audit_html={self.audit_html} -> {self.audit_html.absolute() if self.audit_html is not None else None}")
141
+ self.logger.debug(f"web init parameter: agent_html={self.agent_html} -> {self.agent_html.absolute() if self.agent_html is not None else None}")
135
142
  self.logger.debug(f"web init parameter: assets={self.assets} -> {[a.absolute() for a in self.assets] if self.assets is not None else None}")
136
143
  self.logger.debug(f"web init parameter: signin_html={self.signin_html} -> {self.signin_html.absolute() if self.signin_html is not None else None}")
137
144
  self.logger.debug(f"web init parameter: signin_file={self.signin_file} -> {self.signin_file.absolute() if self.signin_file is not None else None}")
@@ -141,6 +148,8 @@ class Web:
141
148
  self.logger.debug(f"web init parameter: cmds_path={self.cmds_path} -> {self.cmds_path.absolute() if self.cmds_path is not None else None}")
142
149
  self.logger.debug(f"web init parameter: pipes_path={self.pipes_path} -> {self.pipes_path.absolute() if self.pipes_path is not None else None}")
143
150
  self.logger.debug(f"web init parameter: users_path={self.users_path} -> {self.users_path.absolute() if self.users_path is not None else None}")
151
+ self.logger.debug(f"web init parameter: audit_path={self.audit_path} -> {self.audit_path.absolute() if self.audit_path is not None else None}")
152
+ self.logger.debug(f"web init parameter: agent_path={self.agent_path} -> {self.agent_path.absolute() if self.agent_path is not None else None}")
144
153
 
145
154
  def init_webfeatures(self, app:FastAPI):
146
155
  self.filemenu = dict()
@@ -150,7 +159,10 @@ class Web:
150
159
  if self.options.is_features_loaded('web'):
151
160
  return
152
161
  # webfeatureの読込み
162
+ self.wf_dep = []
153
163
  def wf_route(pk, prefix, excludes, w, app, appcls, ver, logger):
164
+ if pk in w.wf_dep: return
165
+ w.wf_dep.append(pk)
154
166
  for wf in module.load_webfeatures(pk, prefix, excludes, appcls=appcls, ver=ver, logger=logger):
155
167
  wf.route(self, app)
156
168
  self.filemenu = {**self.filemenu, **wf.filemenu(w)}
@@ -666,7 +678,8 @@ class Web:
666
678
  def start(self, allow_host:str="0.0.0.0", listen_port:int=8081, ssl_listen_port:int=8443,
667
679
  ssl_cert:Path=None, ssl_key:Path=None, ssl_keypass:str=None, ssl_ca_certs:Path=None,
668
680
  session_domain:str=None, session_path:str='/', session_secure:bool=False, session_timeout:int=900, outputs_key:List[str]=[],
669
- guvicorn_workers:int=-1, guvicorn_timeout:int=30):
681
+ guvicorn_workers:int=-1, guvicorn_timeout:int=30,
682
+ agent_runner=None, mcp=None, mcp_listen_port=9081, mcp_ssl_listen_port=9443):
670
683
  """
671
684
  Webサーバを起動する
672
685
 
@@ -685,6 +698,10 @@ class Web:
685
698
  outputs_key (list, optional): 出力キー. Defaults to [].
686
699
  guvicorn_workers (int, optional): Gunicornワーカー数. Defaults to -1.
687
700
  guvicorn_timeout (int, optional): Gunicornタイムアウト. Defaults to 30.
701
+ agent_runner (Runner, optional): エージェントランナー. Defaults to None.
702
+ mcp (MCP, optional): MCP. Defaults to None.
703
+ mcp_listen_port (int, optional): MCPリスンポート. Defaults to 9081.
704
+ mcp_ssl_listen_port (int, optional): MCP SSLリスンポート. Defaults to 9443.
688
705
  """
689
706
  self.allow_host = allow_host
690
707
  self.listen_port = listen_port
@@ -700,6 +717,10 @@ class Web:
700
717
  self.session_timeout = session_timeout
701
718
  self.guvicorn_workers = guvicorn_workers
702
719
  self.guvicorn_timeout = guvicorn_timeout
720
+ self.agent_runner = agent_runner
721
+ self.mcp = mcp
722
+ self.mcp_listen_port = mcp_listen_port
723
+ self.mcp_ssl_listen_port = mcp_ssl_listen_port
703
724
  if self.logger.level == logging.DEBUG:
704
725
  self.logger.debug(f"web start parameter: allow_host={self.allow_host}")
705
726
  self.logger.debug(f"web start parameter: listen_port={self.listen_port}")
@@ -715,7 +736,81 @@ class Web:
715
736
  self.logger.debug(f"web start parameter: session_timeout={self.session_timeout}")
716
737
  self.logger.debug(f"web start parameter: guvicorn_worker={self.guvicorn_workers}")
717
738
  self.logger.debug(f"web start parameter: guvicorn_timeout={self.guvicorn_timeout}")
739
+ self.logger.debug(f"web start parameter: agent_runner={self.agent_runner}")
740
+ self.logger.debug(f"web start parameter: mcp={self.mcp}")
741
+ self.logger.debug(f"web start parameter: mcp_listen_port={self.mcp_listen_port}")
742
+ self.logger.debug(f"web start parameter: mcp_ssl_listen_port={self.mcp_ssl_listen_port}")
743
+
744
+ if self.agent_runner is not None:
745
+ # google.adkが大きいので必要な時にだけ読込む
746
+ from google.adk.sessions import BaseSessionService, Session
747
+ async def create_agent_session(session_service:BaseSessionService, user_id:str, session_id:str=None) -> Session:
748
+ """
749
+ セッションを作成します
750
+
751
+ Args:
752
+ session_service (BaseSessionService): セッションサービス
753
+ user_id (str): ユーザーID
754
+ session_id (str): セッションID
755
+
756
+ Returns:
757
+ Any: セッション
758
+ """
759
+ if session_id is None:
760
+ session_id = common.random_string(32)
761
+ session = await session_service.get_session(app_name=self.ver.__appid__, user_id=user_id, session_id=session_id)
762
+ if session is None:
763
+ session = await session_service.create_session(app_name=self.ver.__appid__, user_id=user_id, session_id=session_id)
764
+ return session
765
+ self.create_agent_session = create_agent_session
766
+ async def list_agent_sessions(session_service:BaseSessionService, user_id:str, session_id:str=None) -> List[Session]:
767
+ """
768
+ セッションをリストします
769
+
770
+ Args:
771
+ session_service (BaseSessionService): セッションサービス
772
+ user_id (str): ユーザーID
773
+
774
+ Returns:
775
+ List[Session]: セッションリスト
776
+ """
777
+ if session_id is None:
778
+ sessions = await session_service.list_sessions(app_name=self.ver.__appid__, user_id=user_id)
779
+ ret = []
780
+ for s in sessions.sessions:
781
+ ret.append(await session_service.get_session(app_name=self.ver.__appid__, user_id=user_id, session_id=s.id))
782
+ return ret
783
+ else:
784
+ session = await session_service.get_session(app_name=self.ver.__appid__, user_id=user_id, session_id=session_id)
785
+ if session is None:
786
+ return []
787
+ return [session]
788
+ self.list_agent_sessions = list_agent_sessions
789
+ async def delete_agent_session(session_service:BaseSessionService, user_id:str, session_id:str) -> bool:
790
+ """
791
+ セッションを削除します
792
+
793
+ Args:
794
+ session_service (BaseSessionService): セッションサービス
795
+ user_id (str): ユーザーID
796
+ session_id (str): セッションID
797
+
798
+ Returns:
799
+ bool: 成功した場合はTrue, 失敗した場合はFalse
800
+ """
801
+ return await session_service.delete_session(app_name=self.ver.__appid__, user_id=user_id, session_id=session_id)
802
+ self.delete_agent_session = delete_agent_session
718
803
 
804
+ """
805
+ if self.mcp is not None:
806
+ # MCPをFastAPIにマウント
807
+ mcp_app:Starlette = self.mcp.streamable_http_app()
808
+ app = FastAPI(lifespan=self.mcp.settings.lifespan)
809
+ app.mount("/mcp", mcp_app)
810
+ #app.include_router(mcp_app.router)
811
+ else:
812
+ app = FastAPI()
813
+ """
719
814
  app = FastAPI()
720
815
  @app.middleware("http")
721
816
  async def set_context_cookie(req:Request, call_next):
@@ -733,19 +828,32 @@ class Web:
733
828
 
734
829
  self.is_running = True
735
830
  #uvicorn.run(app, host=self.allow_host, port=self.listen_port, workers=2)
736
- th = ThreadedUvicorn(self.logger, config=Config(app=app, host=self.allow_host, port=self.listen_port),
831
+ http_config = Config(app=app, host=self.allow_host, port=self.listen_port)
832
+ th = ThreadedUvicorn(self.logger, config=http_config,
737
833
  guvicorn_config=dict(workers=self.guvicorn_workers, timeout=self.guvicorn_timeout))
738
834
  th.start()
835
+ if self.mcp is not None and self.ssl_cert is None and self.ssl_key is None:
836
+ mcp_app:Starlette = self.mcp.streamable_http_app()
837
+ http_config = Config(app=mcp_app, host=self.allow_host, port=self.mcp_listen_port)
838
+ mcp_th = ThreadedUvicorn(self.logger, config=http_config, force_uvicorn=True)
839
+ mcp_th.start()
739
840
  browser_port = self.listen_port
740
841
  th_ssl = None
741
842
  if self.ssl_cert is not None and self.ssl_key is not None:
742
- th_ssl = ThreadedUvicorn(self.logger,
743
- config=Config(app=app, host=self.allow_host, port=self.ssl_listen_port,
744
- ssl_certfile=self.ssl_cert, ssl_keyfile=self.ssl_key,
745
- ssl_keyfile_password=self.ssl_keypass, ssl_ca_certs=self.ssl_ca_certs),
843
+ https_config = Config(app=app, host=self.allow_host, port=self.ssl_listen_port,
844
+ ssl_certfile=self.ssl_cert, ssl_keyfile=self.ssl_key,
845
+ ssl_keyfile_password=self.ssl_keypass, ssl_ca_certs=self.ssl_ca_certs)
846
+ th_ssl = ThreadedUvicorn(self.logger, config=https_config,
746
847
  guvicorn_config=dict(workers=self.guvicorn_workers, timeout=self.guvicorn_timeout))
747
848
  th_ssl.start()
748
849
  browser_port = self.ssl_listen_port
850
+ if self.mcp is not None:
851
+ mcp_app:Starlette = self.mcp.streamable_http_app()
852
+ https_config = Config(app=mcp_app, host=self.allow_host, port=self.mcp_ssl_listen_port,
853
+ ssl_certfile=self.ssl_cert, ssl_keyfile=self.ssl_key,
854
+ ssl_keyfile_password=self.ssl_keypass, ssl_ca_certs=self.ssl_ca_certs)
855
+ mcp_th_ssl = ThreadedUvicorn(self.logger, config=https_config, force_uvicorn=True)
856
+ mcp_th_ssl.start()
749
857
  try:
750
858
  if self.gui_mode:
751
859
  webbrowser.open(f'http://localhost:{browser_port}/gui')
@@ -754,12 +862,20 @@ class Web:
754
862
  while self.is_running:
755
863
  gevent.sleep(1)
756
864
  th.stop()
865
+ if self.mcp is not None:
866
+ mcp_th.stop()
757
867
  if th_ssl is not None:
758
868
  th_ssl.stop()
869
+ if self.mcp is not None:
870
+ mcp_th_ssl.stop()
759
871
  except KeyboardInterrupt:
760
872
  th.stop()
873
+ if self.mcp is not None:
874
+ mcp_th.stop()
761
875
  if th_ssl is not None:
762
876
  th_ssl.stop()
877
+ if self.mcp is not None:
878
+ mcp_th_ssl.stop()
763
879
 
764
880
  def stop(self):
765
881
  """
@@ -780,13 +896,24 @@ class Web:
780
896
  self.logger.info(f"Exit web.")
781
897
 
782
898
  class ThreadedUvicorn:
783
- def __init__(self, logger:logging.Logger, config:Config, guvicorn_config:Dict[str, Any]):
899
+ def __init__(self, logger:logging.Logger, config:Config, guvicorn_config:Dict[str, Any]=None, force_uvicorn:bool=False):
784
900
  self.logger = logger
785
901
  self.guvicorn_config = guvicorn_config
786
- if platform.system() == "Windows":
902
+ self.force_uvicorn = True if platform.system() == "Windows" else force_uvicorn
903
+ stderr_handler = common.create_log_handler(stderr=True)
904
+ stdout_handler = common.create_log_handler(stderr=False)
905
+ if self.force_uvicorn:
906
+ # loggerの設定
907
+ common.reset_logger("uvicorn")
908
+ common.reset_logger("uvicorn.error")
909
+ common.reset_logger("uvicorn.access")
787
910
  self.server = uvicorn.Server(config)
788
911
  self.thread = RaiseThread(daemon=True, target=self.server.run)
789
912
  else:
913
+ # loggerの設定
914
+ common.reset_logger("gunicorn.error")
915
+ common.reset_logger("gunicorn.access")
916
+
790
917
  from gunicorn.app.wsgiapp import WSGIApplication
791
918
  class App(WSGIApplication):
792
919
  def __init__(self, app, options):
@@ -823,7 +950,7 @@ class ThreadedUvicorn:
823
950
  #self.thread = RaiseThread(daemon=True, target=self.server.run)
824
951
 
825
952
  def start(self):
826
- if platform.system() == "Windows":
953
+ if self.force_uvicorn:
827
954
  asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
828
955
  self.thread.start()
829
956
  asyncio.run(self.wait_for_started())
@@ -837,7 +964,7 @@ class ThreadedUvicorn:
837
964
  await asyncio.sleep(0.1)
838
965
 
839
966
  def stop(self):
840
- if platform.system() == "Windows":
967
+ if self.force_uvicorn:
841
968
  if self.thread.is_alive():
842
969
  self.server.should_exit = True
843
970
  self.thread.raise_exception()
@@ -847,7 +974,7 @@ class ThreadedUvicorn:
847
974
  self.server.started = False
848
975
 
849
976
  def is_alive(self):
850
- if platform.system() == "Windows":
977
+ if self.force_uvicorn:
851
978
  return self.thread.is_alive()
852
979
  else:
853
980
  return self.server.started
@@ -37,6 +37,24 @@ aliases: # Specify the alias for the specified co
37
37
  # e.g. /{1}_exec
38
38
  move: # Specify whether to move the regular expression group of the source to the target.
39
39
  # e.g. true
40
+ agentrule: # Specifies a list of rules that determine which commands the agent can execute.
41
+ policy: deny # Specify the default policy for the rule. The value can be allow or deny.
42
+ rules: # Specify the rules for the commands that the agent can execute according to the group to which the user belongs.
43
+ - mode: audit # Specify the "mode" as the condition for applying the rule.
44
+ cmds: [search, write] # Specify the "cmd" to which the rule applies. Multiple items can be specified in a list.
45
+ rule: allow # Specifies whether the specified command is allowed or not. Values are allow or deny.
46
+ - mode: client
47
+ cmds: [file_copy, file_download, file_list, file_mkdir, file_move, file_remove, file_rmdir, file_upload, server_info]
48
+ rule: allow
49
+ - mode: cmd
50
+ cmds: [list, load]
51
+ rule: allow
52
+ - mode: server
53
+ cmds: [list]
54
+ rule: allow
55
+ - mode: web
56
+ cmds: [gencert, genpass, group_list, user_list]
57
+ rule: allow
40
58
  audit:
41
59
  enabled: true # Specify whether to enable the audit function.
42
60
  write:
@@ -74,6 +74,7 @@ pathrule: # List of RESTAPI rules, rules that determine whe
74
74
  - groups: [user]
75
75
  paths: [/signin, /assets, /bbforce_cmd, /copyright, /dosignin, /dosignout, /password/change,
76
76
  /gui/user_data/load, /gui/user_data/save, /gui/user_data/delete,
77
+ /agent, /mcp,
77
78
  /exec_cmd, /exec_pipe, /filer, /result, /gui, /get_server_opt, /usesignout, /versions_cmdbox, /versions_used]
78
79
  rule: allow
79
80
  - groups: [readonly]
@@ -0,0 +1,29 @@
1
+ BSD 3-Clause License
2
+
3
+ Copyright (c) 2017, Hsiaoming Yang
4
+ All rights reserved.
5
+
6
+ Redistribution and use in source and binary forms, with or without
7
+ modification, are permitted provided that the following conditions are met:
8
+
9
+ * Redistributions of source code must retain the above copyright notice, this
10
+ list of conditions and the following disclaimer.
11
+
12
+ * Redistributions in binary form must reproduce the above copyright notice,
13
+ this list of conditions and the following disclaimer in the documentation
14
+ and/or other materials provided with the distribution.
15
+
16
+ * Neither the name of the copyright holder nor the names of its
17
+ contributors may be used to endorse or promote products derived from
18
+ this software without specific prior written permission.
19
+
20
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
24
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
26
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
27
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2017 Laurent LAPORTE
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,19 @@
1
+ Copyright 2005-2025 SQLAlchemy authors and contributors <see AUTHORS file>.
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
4
+ this software and associated documentation files (the "Software"), to deal in
5
+ the Software without restriction, including without limitation the rights to
6
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
7
+ of the Software, and to permit persons to whom the Software is furnished to do
8
+ so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in all
11
+ copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19
+ SOFTWARE.