cmdbox 0.5.4__py3-none-any.whl → 0.6.0.1__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.
- cmdbox/app/auth/signin.py +463 -303
- cmdbox/app/common.py +51 -3
- cmdbox/app/commons/loghandler.py +62 -13
- cmdbox/app/edge.py +5 -173
- cmdbox/app/edge_tool.py +177 -0
- cmdbox/app/feature.py +10 -9
- cmdbox/app/features/cli/agent_base.py +479 -0
- cmdbox/app/features/cli/audit_base.py +17 -5
- cmdbox/app/features/cli/cmdbox_audit_search.py +24 -1
- cmdbox/app/features/cli/cmdbox_client_file_download.py +1 -1
- cmdbox/app/features/cli/cmdbox_cmd_list.py +105 -0
- cmdbox/app/features/cli/cmdbox_cmd_load.py +104 -0
- cmdbox/app/features/cli/cmdbox_edge_config.py +2 -2
- cmdbox/app/features/cli/cmdbox_edge_start.py +1 -1
- cmdbox/app/features/cli/cmdbox_gui_start.py +9 -132
- cmdbox/app/features/cli/cmdbox_gui_stop.py +4 -21
- cmdbox/app/features/cli/cmdbox_server_start.py +1 -1
- cmdbox/app/features/cli/cmdbox_web_apikey_add.py +1 -1
- cmdbox/app/features/cli/cmdbox_web_apikey_del.py +1 -1
- cmdbox/app/features/cli/cmdbox_web_genpass.py +0 -3
- cmdbox/app/features/cli/cmdbox_web_group_add.py +1 -1
- cmdbox/app/features/cli/cmdbox_web_group_del.py +1 -1
- cmdbox/app/features/cli/cmdbox_web_group_edit.py +1 -1
- cmdbox/app/features/cli/cmdbox_web_group_list.py +1 -1
- cmdbox/app/features/cli/cmdbox_web_start.py +120 -104
- cmdbox/app/features/cli/cmdbox_web_stop.py +1 -1
- cmdbox/app/features/cli/cmdbox_web_user_add.py +1 -1
- cmdbox/app/features/cli/cmdbox_web_user_del.py +1 -1
- cmdbox/app/features/cli/cmdbox_web_user_edit.py +1 -1
- cmdbox/app/features/cli/cmdbox_web_user_list.py +1 -1
- cmdbox/app/features/web/cmdbox_web_agent.py +262 -0
- cmdbox/app/features/web/cmdbox_web_do_signout.py +3 -3
- cmdbox/app/features/web/cmdbox_web_exec_cmd.py +8 -3
- cmdbox/app/features/web/cmdbox_web_signin.py +5 -4
- cmdbox/app/features/web/cmdbox_web_users.py +2 -0
- cmdbox/app/options.py +62 -9
- cmdbox/app/web.py +139 -15
- cmdbox/extensions/features.yml +18 -0
- cmdbox/extensions/sample_project/sample/app/features/cli/__init__.py +0 -0
- cmdbox/extensions/sample_project/sample/app/features/web/__init__.py +0 -0
- cmdbox/extensions/sample_project/sample/extensions/features.yml +18 -0
- cmdbox/extensions/sample_project/sample/extensions/user_list.yml +2 -1
- cmdbox/extensions/sample_project/sample/logconf_sample.yml +14 -1
- cmdbox/extensions/user_list.yml +1 -0
- cmdbox/licenses/LICENSE.Authlib.1.5.2(BSD License).txt +29 -0
- cmdbox/licenses/LICENSE.Deprecated.1.2.18(MIT License).txt +21 -0
- cmdbox/licenses/LICENSE.SQLAlchemy.2.0.40(MIT License).txt +19 -0
- cmdbox/licenses/LICENSE.aiohappyeyeballs.2.6.1(Python Software Foundation License).txt +279 -0
- cmdbox/licenses/LICENSE.aiohttp.3.11.18(Apache Software License).txt +13 -0
- cmdbox/licenses/LICENSE.aiosignal.1.3.2(Apache Software License).txt +201 -0
- cmdbox/licenses/LICENSE.attrs.25.3.0(UNKNOWN).txt +21 -0
- cmdbox/licenses/LICENSE.cachetools.5.5.2(MIT License).txt +20 -0
- cmdbox/licenses/LICENSE.distro.1.9.0(Apache Software License).txt +202 -0
- cmdbox/licenses/LICENSE.docstring_parser.0.16(MIT License).txt +21 -0
- cmdbox/licenses/LICENSE.filelock.3.18.0(The Unlicense (Unlicense)).txt +24 -0
- cmdbox/licenses/LICENSE.frozenlist.1.6.0(Apache-2.0).txt +201 -0
- cmdbox/licenses/LICENSE.fsspec.2025.3.2(BSD License).txt +29 -0
- cmdbox/licenses/LICENSE.google-adk.0.5.0(Apache Software License).txt +202 -0
- cmdbox/licenses/LICENSE.google-api-python-client.2.169.0(Apache Software License).txt +201 -0
- cmdbox/licenses/LICENSE.google-auth-httplib2.0.2.0(Apache Software License).txt +201 -0
- cmdbox/licenses/LICENSE.google-auth.2.40.1(Apache Software License).txt +201 -0
- cmdbox/licenses/LICENSE.google-cloud-aiplatform.1.92.0(Apache 2.0).txt +202 -0
- cmdbox/licenses/LICENSE.google-cloud-bigquery.3.31.0(Apache Software License).txt +202 -0
- cmdbox/licenses/LICENSE.google-cloud-core.2.4.3(Apache Software License).txt +202 -0
- cmdbox/licenses/LICENSE.google-cloud-resource-manager.1.14.2(Apache Software License).txt +202 -0
- cmdbox/licenses/LICENSE.google-cloud-secret-manager.2.23.3(Apache Software License).txt +202 -0
- cmdbox/licenses/LICENSE.google-cloud-speech.2.32.0(Apache Software License).txt +202 -0
- cmdbox/licenses/LICENSE.google-cloud-storage.2.19.0(Apache Software License).txt +202 -0
- cmdbox/licenses/LICENSE.google-cloud-trace.1.16.1(Apache Software License).txt +202 -0
- cmdbox/licenses/LICENSE.google-crc32c.1.7.1(Apache 2.0).txt +202 -0
- cmdbox/licenses/LICENSE.google-genai.1.14.0(Apache Software License).txt +202 -0
- cmdbox/licenses/LICENSE.google-resumable-media.2.7.2(Apache Software License).txt +202 -0
- cmdbox/licenses/LICENSE.googleapis-common-protos.1.70.0(Apache Software License).txt +202 -0
- cmdbox/licenses/LICENSE.graphviz.0.20.3(MIT License).txt +21 -0
- cmdbox/licenses/LICENSE.grpc-google-iam-v1.0.14.2(Apache Software License).txt +202 -0
- cmdbox/licenses/LICENSE.grpcio-status.1.71.0(Apache Software License).txt +610 -0
- cmdbox/licenses/LICENSE.grpcio.1.71.0(Apache Software License).txt +610 -0
- cmdbox/licenses/LICENSE.httpcore.1.0.9(BSD License).txt +27 -0
- cmdbox/licenses/LICENSE.httplib2.0.22.0(MIT License).txt +23 -0
- cmdbox/licenses/LICENSE.httpx-sse.0.4.0(MIT).txt +21 -0
- cmdbox/licenses/LICENSE.httpx.0.28.1(BSD License).txt +12 -0
- cmdbox/licenses/LICENSE.huggingface-hub.0.31.1(Apache Software License).txt +201 -0
- cmdbox/licenses/LICENSE.importlib_metadata.8.6.1(Apache Software License).txt +202 -0
- cmdbox/licenses/LICENSE.jiter.0.9.0(MIT License).txt +1 -0
- cmdbox/licenses/LICENSE.jsonschema-specifications.2025.4.1(UNKNOWN).txt +19 -0
- cmdbox/licenses/LICENSE.jsonschema.4.23.0(MIT License).txt +19 -0
- cmdbox/licenses/LICENSE.litellm.1.69.0(MIT License).txt +26 -0
- cmdbox/licenses/LICENSE.mcp.1.8.0(MIT License).txt +21 -0
- cmdbox/licenses/LICENSE.multidict.6.4.3(Apache Software License).txt +13 -0
- cmdbox/licenses/LICENSE.openai.1.75.0(Apache Software License).txt +201 -0
- cmdbox/licenses/LICENSE.opentelemetry-api.1.33.0(Apache Software License).txt +201 -0
- cmdbox/licenses/LICENSE.opentelemetry-exporter-gcp-trace.1.9.0(Apache Software License).txt +201 -0
- cmdbox/licenses/LICENSE.opentelemetry-resourcedetector-gcp.1.9.0a0(Apache Software License).txt +201 -0
- cmdbox/licenses/LICENSE.opentelemetry-sdk.1.33.0(Apache Software License).txt +201 -0
- cmdbox/licenses/LICENSE.opentelemetry-semantic-conventions.0.54b0(Apache Software License).txt +201 -0
- cmdbox/licenses/LICENSE.propcache.0.3.1(Apache Software License).txt +202 -0
- cmdbox/licenses/LICENSE.proto-plus.1.26.1(Apache Software License).txt +202 -0
- cmdbox/licenses/LICENSE.protobuf.5.29.4(3-Clause BSD License).txt +32 -0
- cmdbox/licenses/LICENSE.pyasn1.0.6.1(BSD License).txt +24 -0
- cmdbox/licenses/LICENSE.pyasn1_modules.0.4.2(BSD License).txt +24 -0
- cmdbox/licenses/LICENSE.pydantic-settings.2.9.1(MIT License).txt +21 -0
- cmdbox/licenses/LICENSE.pyparsing.3.2.3(MIT License).txt +18 -0
- cmdbox/licenses/LICENSE.python-dateutil.2.9.0.post0(Apache Software License; BSD License).txt +54 -0
- cmdbox/licenses/LICENSE.referencing.0.36.2(UNKNOWN).txt +19 -0
- cmdbox/licenses/LICENSE.regex.2024.11.6(Apache Software License).txt +208 -0
- cmdbox/licenses/LICENSE.rpds-py.0.24.0(MIT).txt +19 -0
- cmdbox/licenses/LICENSE.rsa.4.9.1(Apache Software License).txt +13 -0
- cmdbox/licenses/LICENSE.shapely.2.1.0(BSD License).txt +29 -0
- cmdbox/licenses/LICENSE.sse-starlette.2.3.4(BSD License).txt +27 -0
- cmdbox/licenses/LICENSE.tiktoken.0.9.0(MIT License).txt +21 -0
- cmdbox/licenses/LICENSE.tokenizers.0.21.1(Apache Software License).txt +1 -0
- cmdbox/licenses/LICENSE.tqdm.4.67.1(MIT License; Mozilla Public License 2.0 (MPL 2.0)).txt +49 -0
- cmdbox/licenses/LICENSE.tzlocal.5.3.1(MIT License).txt +19 -0
- cmdbox/licenses/LICENSE.uritemplate.4.1.1(Apache Software License; BSD License).txt +3 -0
- cmdbox/licenses/LICENSE.wrapt.1.17.2(BSD License).txt +24 -0
- cmdbox/licenses/LICENSE.yarl.1.20.0(Apache Software License).txt +202 -0
- cmdbox/licenses/files.txt +104 -11
- cmdbox/logconf_audit.yml +16 -3
- cmdbox/logconf_client.yml +16 -3
- cmdbox/logconf_cmdbox.yml +16 -3
- cmdbox/logconf_edge.yml +16 -3
- cmdbox/logconf_gui.yml +15 -2
- cmdbox/logconf_server.yml +15 -2
- cmdbox/logconf_web.yml +15 -2
- cmdbox/version.py +3 -2
- cmdbox/web/agent.html +263 -0
- cmdbox/web/assets/cmdbox/agent.js +338 -0
- cmdbox/web/assets/cmdbox/common.js +1111 -1020
- cmdbox/web/assets/cmdbox/main.js +17 -3
- cmdbox/web/assets/cmdbox/signin.js +4 -4
- cmdbox/web/assets/filer/filer.js +4 -2
- {cmdbox-0.5.4.dist-info → cmdbox-0.6.0.1.dist-info}/METADATA +69 -26
- {cmdbox-0.5.4.dist-info → cmdbox-0.6.0.1.dist-info}/RECORD +148 -67
- /cmdbox/licenses/{LICENSE.charset-normalizer.3.4.1(MIT License).txt → LICENSE.charset-normalizer.3.4.2(MIT License).txt} +0 -0
- /cmdbox/licenses/{LICENSE.click.8.1.8(BSD License).txt → LICENSE.click.8.2.0(UNKNOWN).txt} +0 -0
- /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
- /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
- /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
- /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
- /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
- /cmdbox/licenses/{LICENSE.pydantic.2.11.3(MIT License).txt → LICENSE.pydantic.2.11.4(MIT License).txt} +0 -0
- /cmdbox/licenses/{LICENSE.pydantic_core.2.33.1(MIT License).txt → LICENSE.pydantic_core.2.33.2(MIT License).txt} +0 -0
- /cmdbox/licenses/{LICENSE.redis.5.2.1(MIT License).txt → LICENSE.redis.6.0.0(MIT License).txt} +0 -0
- /cmdbox/licenses/{LICENSE.snowballstemmer.2.2.0(BSD License).txt → LICENSE.snowballstemmer.3.0.1(BSD License).txt} +0 -0
- {cmdbox-0.5.4.dist-info → cmdbox-0.6.0.1.dist-info}/LICENSE +0 -0
- {cmdbox-0.5.4.dist-info → cmdbox-0.6.0.1.dist-info}/WHEEL +0 -0
- {cmdbox-0.5.4.dist-info → cmdbox-0.6.0.1.dist-info}/entry_points.txt +0 -0
- {cmdbox-0.5.4.dist-info → cmdbox-0.6.0.1.dist-info}/top_level.txt +0 -0
|
@@ -13,9 +13,9 @@ class DoSignout(feature.WebFeature):
|
|
|
13
13
|
web (Web): Webオブジェクト
|
|
14
14
|
app (FastAPI): FastAPIオブジェクト
|
|
15
15
|
"""
|
|
16
|
-
@app.
|
|
17
|
-
@app.
|
|
18
|
-
async def do_signout(next, req:Request, res:Response):
|
|
16
|
+
@app.api_route('/dosignout/{next}', methods=['GET', 'POST'], response_class=HTMLResponse)
|
|
17
|
+
@app.api_route('/{full_path:path}/dosignout/{next}/', methods=['GET', 'POST'], response_class=HTMLResponse)
|
|
18
|
+
async def do_signout(next, req:Request, res:Response, full_path:str=None):
|
|
19
19
|
if 'signin' in req.session:
|
|
20
20
|
web.options.audit_exec(req, res, web, body=dict(msg='Signout.'), audit_type='auth')
|
|
21
21
|
for key in list(req.session.keys()).copy():
|
|
@@ -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
|
-
|
|
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(
|
|
212
|
+
old_stdout.write(f'EXEC OUTPUT => {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
|
-
|
|
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,10 @@ 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.
|
|
26
|
-
@app.
|
|
27
|
-
async def _signin(next:str, req:Request, res:Response):
|
|
28
|
-
|
|
26
|
+
@app.api_route('/signin/{next}', methods=['GET', 'POST'], response_class=HTMLResponse)
|
|
27
|
+
@app.api_route('/{full_path:path}/signin/{next}', methods=['GET', 'POST'], response_class=HTMLResponse)
|
|
28
|
+
async def _signin(next:str, req:Request, res:Response, full_path:str=None):
|
|
29
|
+
signin.Signin._enable_cors(req, res)
|
|
29
30
|
res.headers['Access-Control-Allow-Origin'] = '*'
|
|
30
31
|
return web.signin_html_data
|
|
31
32
|
|
|
@@ -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:
|
|
@@ -627,6 +632,11 @@ class Options:
|
|
|
627
632
|
if 'enabled' not in yml['audit']:
|
|
628
633
|
raise Exception('features.yml is invalid. (The audit element must have "enabled" specified.)')
|
|
629
634
|
if not yml['audit']['enabled']: return
|
|
635
|
+
# フューチャーのoptions
|
|
636
|
+
if 'options' not in yml['audit']:
|
|
637
|
+
raise Exception('features.yml is invalid. (The audit element must have "options" specified.)')
|
|
638
|
+
self.audit_write_args = yml['audit']['options'].copy()
|
|
639
|
+
self.audit_search_args = yml['audit']['options'].copy()
|
|
630
640
|
# writeフューチャー
|
|
631
641
|
if 'write' not in yml['audit']:
|
|
632
642
|
raise Exception('features.yml is invalid. (The audit element must have "write" specified.)')
|
|
@@ -637,6 +647,8 @@ class Options:
|
|
|
637
647
|
raise Exception('features.yml is invalid. (The audit.write element must have "cmd" specified.)')
|
|
638
648
|
cmd = yml['audit']['write']['cmd']
|
|
639
649
|
self.audit_write:feature.Feature = self.get_cmd_attr(mode, cmd, 'feature')
|
|
650
|
+
self.audit_write_args['mode'] = mode
|
|
651
|
+
self.audit_write_args['cmd'] = cmd
|
|
640
652
|
# searchフューチャー
|
|
641
653
|
if 'search' not in yml['audit']:
|
|
642
654
|
raise Exception('features.yml is invalid. (The audit element must have "search" specified.)')
|
|
@@ -647,17 +659,58 @@ class Options:
|
|
|
647
659
|
raise Exception('features.yml is invalid. (The audit.search element must have "cmd" specified.)')
|
|
648
660
|
cmd = yml['audit']['search']['cmd']
|
|
649
661
|
self.audit_search:feature.Feature = self.get_cmd_attr(mode, cmd, 'feature')
|
|
650
|
-
# フューチャーのoptions
|
|
651
|
-
if 'options' not in yml['audit']:
|
|
652
|
-
raise Exception('features.yml is invalid. (The audit element must have "options" specified.)')
|
|
653
|
-
self.audit_write_args = yml['audit']['options'].copy()
|
|
654
|
-
self.audit_write_args['mode'] = mode
|
|
655
|
-
self.audit_write_args['cmd'] = cmd
|
|
656
|
-
self.audit_search_args = yml['audit']['options'].copy()
|
|
657
662
|
self.audit_search_args['mode'] = mode
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
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,21 @@ 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
|
+
# loggerの設定
|
|
904
|
+
common.reset_logger("uvicorn")
|
|
905
|
+
common.reset_logger("uvicorn.error")
|
|
906
|
+
common.reset_logger("uvicorn.access")
|
|
907
|
+
#common.reset_logger("gunicorn.error")
|
|
908
|
+
#common.reset_logger("gunicorn.access")
|
|
909
|
+
if self.force_uvicorn:
|
|
787
910
|
self.server = uvicorn.Server(config)
|
|
788
911
|
self.thread = RaiseThread(daemon=True, target=self.server.run)
|
|
789
912
|
else:
|
|
913
|
+
|
|
790
914
|
from gunicorn.app.wsgiapp import WSGIApplication
|
|
791
915
|
class App(WSGIApplication):
|
|
792
916
|
def __init__(self, app, options):
|
|
@@ -823,7 +947,7 @@ class ThreadedUvicorn:
|
|
|
823
947
|
#self.thread = RaiseThread(daemon=True, target=self.server.run)
|
|
824
948
|
|
|
825
949
|
def start(self):
|
|
826
|
-
if
|
|
950
|
+
if self.force_uvicorn:
|
|
827
951
|
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
|
|
828
952
|
self.thread.start()
|
|
829
953
|
asyncio.run(self.wait_for_started())
|
|
@@ -837,7 +961,7 @@ class ThreadedUvicorn:
|
|
|
837
961
|
await asyncio.sleep(0.1)
|
|
838
962
|
|
|
839
963
|
def stop(self):
|
|
840
|
-
if
|
|
964
|
+
if self.force_uvicorn:
|
|
841
965
|
if self.thread.is_alive():
|
|
842
966
|
self.server.should_exit = True
|
|
843
967
|
self.thread.raise_exception()
|
|
@@ -847,7 +971,7 @@ class ThreadedUvicorn:
|
|
|
847
971
|
self.server.started = False
|
|
848
972
|
|
|
849
973
|
def is_alive(self):
|
|
850
|
-
if
|
|
974
|
+
if self.force_uvicorn:
|
|
851
975
|
return self.thread.is_alive()
|
|
852
976
|
else:
|
|
853
977
|
return self.server.started
|
cmdbox/extensions/features.yml
CHANGED
|
@@ -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:
|
|
File without changes
|
|
File without changes
|
|
@@ -46,6 +46,24 @@ aliases: # Specify the alias for the specified co
|
|
|
46
46
|
# e.g. /{1}_exec
|
|
47
47
|
move: # Specify whether to move the regular expression group of the source to the target.
|
|
48
48
|
# e.g. true
|
|
49
|
+
agentrule: # Specifies a list of rules that determine which commands the agent can execute.
|
|
50
|
+
policy: deny # Specify the default policy for the rule. The value can be allow or deny.
|
|
51
|
+
rules: # Specify the rules for the commands that the agent can execute according to the group to which the user belongs.
|
|
52
|
+
- mode: audit # Specify the "mode" as the condition for applying the rule.
|
|
53
|
+
cmds: [search, write] # Specify the "cmd" to which the rule applies. Multiple items can be specified in a list.
|
|
54
|
+
rule: allow # Specifies whether the specified command is allowed or not. Values are allow or deny.
|
|
55
|
+
- mode: client
|
|
56
|
+
cmds: [file_copy, file_download, file_list, file_mkdir, file_move, file_remove, file_rmdir, file_upload, server_info]
|
|
57
|
+
rule: allow
|
|
58
|
+
- mode: cmd
|
|
59
|
+
cmds: [list, load]
|
|
60
|
+
rule: allow
|
|
61
|
+
- mode: server
|
|
62
|
+
cmds: [list]
|
|
63
|
+
rule: allow
|
|
64
|
+
- mode: web
|
|
65
|
+
cmds: [gencert, genpass, group_list, user_list]
|
|
66
|
+
rule: allow
|
|
49
67
|
audit:
|
|
50
68
|
enabled: true # Specify whether to enable the audit function.
|
|
51
69
|
write:
|
|
@@ -74,7 +74,8 @@ 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
|
-
/
|
|
77
|
+
/agent, /mcp,
|
|
78
|
+
/exec_cmd, /exec_pipe, /filer, /result, /gui, /get_server_opt, /usesignout, /versions_cmdbox, /versions_used]
|
|
78
79
|
rule: allow
|
|
79
80
|
- groups: [readonly]
|
|
80
81
|
paths: [/gui/del_cmd, /gui/del_pipe, /gui/save_cmd, /gui/save_pipe]
|
|
@@ -2,7 +2,11 @@ version: 1
|
|
|
2
2
|
|
|
3
3
|
formatters:
|
|
4
4
|
fmt:
|
|
5
|
-
format: '%(
|
|
5
|
+
format: '[%(asctime)s] %(levelname)s - %(message)s'
|
|
6
|
+
datefmt: '%Y-%m-%d %H:%M:%S'
|
|
7
|
+
class: logging.Formatter
|
|
8
|
+
fmt_rich:
|
|
9
|
+
format: '%(message)s'
|
|
6
10
|
class: logging.Formatter
|
|
7
11
|
handlers:
|
|
8
12
|
std:
|
|
@@ -10,6 +14,14 @@ handlers:
|
|
|
10
14
|
level: INFO
|
|
11
15
|
formatter: fmt
|
|
12
16
|
stream: ext://sys.stdout
|
|
17
|
+
rich:
|
|
18
|
+
class: rich.logging.RichHandler
|
|
19
|
+
level: INFO
|
|
20
|
+
formatter: fmt_rich
|
|
21
|
+
show_path: false
|
|
22
|
+
omit_repeated_times: false
|
|
23
|
+
tracebacks_word_wrap: false
|
|
24
|
+
log_time_format: '[%Y-%m-%d %H:%M]'
|
|
13
25
|
sample:
|
|
14
26
|
class: cmdbox.app.commons.loghandler.TimedRotatingFileHandler
|
|
15
27
|
level: INFO
|
|
@@ -24,6 +36,7 @@ loggers:
|
|
|
24
36
|
handlers: [sample, std]
|
|
25
37
|
level: INFO
|
|
26
38
|
qualname: sample
|
|
39
|
+
propagate: false
|
|
27
40
|
|
|
28
41
|
#root:
|
|
29
42
|
# handlers: [sample, std]
|
cmdbox/extensions/user_list.yml
CHANGED
|
@@ -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]
|