langbot-plugin 0.3.10__tar.gz → 0.4.0__tar.gz
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.
- langbot_plugin-0.4.0/.github/workflows/test.yml +46 -0
- {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/.gitignore +1 -0
- {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/PKG-INFO +3 -1
- {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/pyproject.toml +19 -1
- {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/api/entities/builtin/command/context.py +2 -3
- {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/api/entities/builtin/command/errors.py +4 -4
- {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/api/entities/builtin/provider/message.py +4 -1
- langbot_plugin-0.4.0/src/langbot_plugin/box/__init__.py +5 -0
- langbot_plugin-0.4.0/src/langbot_plugin/box/actions.py +34 -0
- langbot_plugin-0.4.0/src/langbot_plugin/box/backend.py +411 -0
- langbot_plugin-0.4.0/src/langbot_plugin/box/client.py +377 -0
- langbot_plugin-0.4.0/src/langbot_plugin/box/e2b_backend.py +429 -0
- langbot_plugin-0.4.0/src/langbot_plugin/box/errors.py +33 -0
- langbot_plugin-0.4.0/src/langbot_plugin/box/models.py +331 -0
- langbot_plugin-0.4.0/src/langbot_plugin/box/nsjail_backend.py +552 -0
- langbot_plugin-0.4.0/src/langbot_plugin/box/runtime.py +598 -0
- langbot_plugin-0.4.0/src/langbot_plugin/box/security.py +52 -0
- langbot_plugin-0.4.0/src/langbot_plugin/box/server.py +494 -0
- langbot_plugin-0.4.0/src/langbot_plugin/box/skill_store.py +647 -0
- {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/cli/__init__.py +57 -1
- {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/cli/commands/runplugin.py +23 -3
- {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/entities/io/actions/enums.py +5 -0
- {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/runtime/app.py +5 -0
- {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/runtime/helper/pkgmgr.py +36 -24
- {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/runtime/io/handler.py +13 -5
- {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/runtime/io/handlers/control.py +18 -0
- {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/runtime/plugin/container.py +3 -4
- {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/runtime/plugin/mgr.py +21 -5
- {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/utils/discover/engine.py +3 -0
- langbot_plugin-0.4.0/src/langbot_plugin/utils/importutil.py +52 -0
- langbot_plugin-0.4.0/src/langbot_plugin/version.py +1 -0
- langbot_plugin-0.4.0/tests/__init__.py +1 -0
- langbot_plugin-0.4.0/tests/api/definition/components/test_components.py +97 -0
- langbot_plugin-0.4.0/tests/api/definition/test_manifest.py +124 -0
- langbot_plugin-0.4.0/tests/api/entities/builtin/test_command_context.py +77 -0
- langbot_plugin-0.4.0/tests/api/entities/builtin/test_platform_logger.py +23 -0
- langbot_plugin-0.4.0/tests/api/entities/builtin/test_provider_message.py +107 -0
- langbot_plugin-0.4.0/tests/api/entities/builtin/test_rag_models.py +133 -0
- langbot_plugin-0.4.0/tests/api/entities/test_context.py +163 -0
- langbot_plugin-0.4.0/tests/api/proxies/test_base.py +11 -0
- langbot_plugin-0.4.0/tests/api/proxies/test_langbot_api.py +218 -0
- langbot_plugin-0.4.0/tests/api/proxies/test_query_based_api.py +96 -0
- langbot_plugin-0.4.0/tests/box/__init__.py +0 -0
- langbot_plugin-0.4.0/tests/box/test_backend.py +865 -0
- langbot_plugin-0.4.0/tests/box/test_backend_selection.py +275 -0
- langbot_plugin-0.4.0/tests/box/test_client.py +658 -0
- langbot_plugin-0.4.0/tests/box/test_e2b_backend.py +482 -0
- langbot_plugin-0.4.0/tests/box/test_nsjail_backend.py +445 -0
- langbot_plugin-0.4.0/tests/box/test_runtime.py +805 -0
- langbot_plugin-0.4.0/tests/box/test_server.py +859 -0
- langbot_plugin-0.4.0/tests/box/test_skill_store.py +914 -0
- langbot_plugin-0.4.0/tests/cli/run/test_controller.py +172 -0
- langbot_plugin-0.4.0/tests/cli/run/test_runtime_handler.py +338 -0
- langbot_plugin-0.4.0/tests/cli/test_buildplugin.py +81 -0
- langbot_plugin-0.4.0/tests/cli/test_gencomponent.py +90 -0
- langbot_plugin-0.4.0/tests/cli/test_i18n_form.py +63 -0
- langbot_plugin-0.4.0/tests/cli/test_initplugin.py +102 -0
- langbot_plugin-0.4.0/tests/cli/test_login.py +516 -0
- langbot_plugin-0.4.0/tests/cli/test_logout_publish.py +184 -0
- langbot_plugin-0.4.0/tests/cli/test_page_components.py +63 -0
- langbot_plugin-0.4.0/tests/cli/test_renderer.py +64 -0
- langbot_plugin-0.4.0/tests/cli/test_runplugin.py +257 -0
- langbot_plugin-0.4.0/tests/entities/io/test_protocol.py +88 -0
- langbot_plugin-0.4.0/tests/helpers/__init__.py +1 -0
- langbot_plugin-0.4.0/tests/helpers/protocol.py +120 -0
- langbot_plugin-0.4.0/tests/runtime/helper/test_marketplace.py +131 -0
- langbot_plugin-0.4.0/tests/runtime/helper/test_pkgmgr.py +109 -0
- langbot_plugin-0.4.0/tests/runtime/io/handlers/test_control_handler.py +452 -0
- langbot_plugin-0.4.0/tests/runtime/io/handlers/test_import_contracts.py +24 -0
- langbot_plugin-0.4.0/tests/runtime/io/handlers/test_plugin_handler.py +317 -0
- langbot_plugin-0.4.0/tests/runtime/io/test_connections.py +148 -0
- langbot_plugin-0.4.0/tests/runtime/io/test_controllers.py +193 -0
- langbot_plugin-0.4.0/tests/runtime/io/test_handler.py +228 -0
- langbot_plugin-0.4.0/tests/runtime/plugin/test_container.py +124 -0
- langbot_plugin-0.4.0/tests/runtime/plugin/test_manager.py +536 -0
- langbot_plugin-0.4.0/tests/runtime/test_app.py +389 -0
- langbot_plugin-0.4.0/tests/utils/test_discovery.py +120 -0
- langbot_plugin-0.4.0/tests/utils/test_importutil.py +54 -0
- langbot_plugin-0.4.0/tests/utils/test_platform.py +9 -0
- langbot_plugin-0.3.10/src/langbot_plugin/utils/importutil.py +0 -38
- langbot_plugin-0.3.10/src/langbot_plugin/version.py +0 -1
- {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/.github/workflows/publish-pypi.yaml +0 -0
- {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/.python-version +0 -0
- {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/AGENTS.md +0 -0
- {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/CLAUDE.md +0 -0
- {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/LICENSE +0 -0
- {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/README.md +0 -0
- {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/data/.env.example +0 -0
- {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/docs/Message.md +0 -0
- {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/docs/PluginPages.md +0 -0
- {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/docs/communication/apis/lb2rt.md +0 -0
- {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/docs/communication/runtime_plugin.md +0 -0
- {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/docs/dependency-management.md +0 -0
- {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/docs/langbot-plugin-social.png +0 -0
- {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/__init__.py +0 -0
- {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/api/__init__.py +0 -0
- {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/api/definition/__init__.py +0 -0
- {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/api/definition/abstract/__init__.py +0 -0
- {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/api/definition/abstract/platform/__init__.py +0 -0
- {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/api/definition/abstract/platform/adapter.py +0 -0
- {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/api/definition/abstract/platform/event_logger.py +0 -0
- {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/api/definition/components/__init__.py +0 -0
- {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/api/definition/components/base.py +0 -0
- {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/api/definition/components/command/__init__.py +0 -0
- {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/api/definition/components/command/command.py +0 -0
- {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/api/definition/components/common/__init__.py +0 -0
- {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/api/definition/components/common/event_listener.py +0 -0
- {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/api/definition/components/knowledge_engine/__init__.py +0 -0
- {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/api/definition/components/knowledge_engine/engine.py +0 -0
- {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/api/definition/components/manifest.py +0 -0
- {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/api/definition/components/page/__init__.py +0 -0
- {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/api/definition/components/parser/__init__.py +0 -0
- {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/api/definition/components/parser/parser.py +0 -0
- {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/api/definition/components/tool/__init__.py +0 -0
- {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/api/definition/components/tool/tool.py +0 -0
- {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/api/definition/plugin.py +0 -0
- {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/api/entities/__init__.py +0 -0
- {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/api/entities/builtin/__init__.py +0 -0
- {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/api/entities/builtin/command/__init__.py +0 -0
- {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/api/entities/builtin/pipeline/__init__.py +0 -0
- {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/api/entities/builtin/pipeline/query.py +0 -0
- {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/api/entities/builtin/platform/__init__.py +0 -0
- {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/api/entities/builtin/platform/entities.py +0 -0
- {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/api/entities/builtin/platform/events.py +0 -0
- {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/api/entities/builtin/platform/logger.py +0 -0
- {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/api/entities/builtin/platform/message.py +0 -0
- {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/api/entities/builtin/provider/__init__.py +0 -0
- {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/api/entities/builtin/provider/prompt.py +0 -0
- {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/api/entities/builtin/provider/session.py +0 -0
- {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/api/entities/builtin/rag/__init__.py +0 -0
- {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/api/entities/builtin/rag/context.py +0 -0
- {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/api/entities/builtin/rag/enums.py +0 -0
- {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/api/entities/builtin/rag/errors.py +0 -0
- {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/api/entities/builtin/rag/models.py +0 -0
- {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/api/entities/builtin/resource/__init__.py +0 -0
- {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/api/entities/builtin/resource/tool.py +0 -0
- {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/api/entities/context.py +0 -0
- {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/api/entities/events.py +0 -0
- {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/api/proxies/__init__.py +0 -0
- {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/api/proxies/base.py +0 -0
- {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/api/proxies/event_context.py +0 -0
- {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/api/proxies/execute_context.py +0 -0
- {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/api/proxies/langbot_api.py +0 -0
- {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/api/proxies/query_based_api.py +0 -0
- {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/assets/__init__.py +0 -0
- {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/assets/langbot-page-sdk.js +0 -0
- {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/assets/templates/.env.example.example +0 -0
- {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/assets/templates/.gitignore.example +0 -0
- {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/assets/templates/.vscode/launch.json.example +0 -0
- {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/assets/templates/README.md.example +0 -0
- {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/assets/templates/__init__.py +0 -0
- {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/assets/templates/assets/icon.svg.example +0 -0
- {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/assets/templates/components/__init__.py +0 -0
- {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/assets/templates/components/commands/__init__.py +0 -0
- {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/assets/templates/components/commands/{cmd_name}.py.example +0 -0
- {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/assets/templates/components/commands/{cmd_name}.yaml.example +0 -0
- {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/assets/templates/components/event_listener/__init__.py +0 -0
- {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/assets/templates/components/event_listener/default.py.example +0 -0
- {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/assets/templates/components/event_listener/default.yaml.example +0 -0
- {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/assets/templates/components/knowledge_engine/__init__.py +0 -0
- {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/assets/templates/components/knowledge_engine/{knowledge_engine_name}.py.example +0 -0
- {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/assets/templates/components/knowledge_engine/{knowledge_engine_name}.yaml.example +0 -0
- {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/assets/templates/components/pages/{page_name}.html.example +0 -0
- {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/assets/templates/components/pages/{page_name}.yaml.example +0 -0
- {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/assets/templates/components/parser/__init__.py +0 -0
- {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/assets/templates/components/parser/{parser_name}.py.example +0 -0
- {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/assets/templates/components/parser/{parser_name}.yaml.example +0 -0
- {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/assets/templates/components/tools/__init__.py +0 -0
- {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/assets/templates/components/tools/{tool_name}.py.example +0 -0
- {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/assets/templates/components/tools/{tool_name}.yaml.example +0 -0
- {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/assets/templates/main.py.example +0 -0
- {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/assets/templates/manifest.yaml.example +0 -0
- {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/assets/templates/requirements.txt.example +0 -0
- {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/cli/__main__.py +0 -0
- {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/cli/commands/__init__.py +0 -0
- {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/cli/commands/buildplugin.py +0 -0
- {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/cli/commands/gencomponent.py +0 -0
- {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/cli/commands/initplugin.py +0 -0
- {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/cli/commands/login.py +0 -0
- {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/cli/commands/logout.py +0 -0
- {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/cli/commands/publish.py +0 -0
- {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/cli/gen/__init__.py +0 -0
- {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/cli/gen/renderer.py +0 -0
- {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/cli/i18n.py +0 -0
- {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/cli/locales/__init__.py +0 -0
- {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/cli/locales/en_US.py +0 -0
- {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/cli/locales/es_ES.py +0 -0
- {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/cli/locales/ja_JP.py +0 -0
- {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/cli/locales/th_TH.py +0 -0
- {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/cli/locales/vi_VN.py +0 -0
- {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/cli/locales/zh_Hans.py +0 -0
- {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/cli/locales/zh_Hant.py +0 -0
- {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/cli/run/__init__.py +0 -0
- {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/cli/run/controller.py +0 -0
- {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/cli/run/handler.py +0 -0
- {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/cli/run/hotreload.py +0 -0
- {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/cli/utils/__init__.py +0 -0
- {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/cli/utils/cloudsv.py +0 -0
- {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/cli/utils/form.py +0 -0
- {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/cli/utils/page_components.py +0 -0
- {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/entities/__init__.py +0 -0
- {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/entities/io/__init__.py +0 -0
- {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/entities/io/actions/__init__.py +0 -0
- {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/entities/io/errors.py +0 -0
- {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/entities/io/req.py +0 -0
- {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/entities/io/resp.py +0 -0
- {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/entities/marketplace.py +0 -0
- {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/runtime/LICENSE +0 -0
- {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/runtime/README.md +0 -0
- {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/runtime/__init__.py +0 -0
- {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/runtime/context.py +0 -0
- {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/runtime/helper/__init__.py +0 -0
- {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/runtime/helper/marketplace.py +0 -0
- {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/runtime/io/__init__.py +0 -0
- {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/runtime/io/connection.py +0 -0
- {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/runtime/io/connections/__init__.py +0 -0
- {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/runtime/io/connections/stdio.py +0 -0
- {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/runtime/io/connections/ws.py +0 -0
- {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/runtime/io/controller.py +0 -0
- {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/runtime/io/controllers/__init__.py +0 -0
- {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/runtime/io/controllers/stdio/__init__.py +0 -0
- {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/runtime/io/controllers/stdio/client.py +0 -0
- {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/runtime/io/controllers/stdio/server.py +0 -0
- {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/runtime/io/controllers/ws/__init__.py +0 -0
- {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/runtime/io/controllers/ws/client.py +0 -0
- {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/runtime/io/controllers/ws/server.py +0 -0
- {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/runtime/io/handlers/__init__.py +0 -0
- {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/runtime/io/handlers/plugin.py +0 -0
- {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/runtime/plugin/__init__.py +0 -0
- {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/runtime/settings.py +0 -0
- {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/utils/__init__.py +0 -0
- {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/utils/discover/__init__.py +0 -0
- {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/utils/log.py +0 -0
- {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/utils/platform.py +0 -0
- {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/tests/api/entities/test_events.py +0 -0
- {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/tests/test_log.py +0 -0
- {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/tests/test_message.py +0 -0
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
name: Tests
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches:
|
|
6
|
+
- main
|
|
7
|
+
- test-build
|
|
8
|
+
pull_request:
|
|
9
|
+
|
|
10
|
+
permissions:
|
|
11
|
+
contents: read
|
|
12
|
+
|
|
13
|
+
jobs:
|
|
14
|
+
unit-tests:
|
|
15
|
+
name: Unit Tests
|
|
16
|
+
runs-on: ubuntu-latest
|
|
17
|
+
strategy:
|
|
18
|
+
fail-fast: false
|
|
19
|
+
matrix:
|
|
20
|
+
python-version: ["3.10", "3.11", "3.12"]
|
|
21
|
+
steps:
|
|
22
|
+
- uses: actions/checkout@v4
|
|
23
|
+
with:
|
|
24
|
+
persist-credentials: false
|
|
25
|
+
- name: Install uv
|
|
26
|
+
uses: astral-sh/setup-uv@v6
|
|
27
|
+
- name: Set up Python
|
|
28
|
+
run: uv python install ${{ matrix.python-version }}
|
|
29
|
+
- name: Run tests with coverage
|
|
30
|
+
run: |
|
|
31
|
+
uv run --python ${{ matrix.python-version }} pytest \
|
|
32
|
+
--cov=langbot_plugin \
|
|
33
|
+
--cov-report=term-missing \
|
|
34
|
+
--cov-fail-under=75
|
|
35
|
+
|
|
36
|
+
test-lint:
|
|
37
|
+
name: Test Lint
|
|
38
|
+
runs-on: ubuntu-latest
|
|
39
|
+
steps:
|
|
40
|
+
- uses: actions/checkout@v4
|
|
41
|
+
with:
|
|
42
|
+
persist-credentials: false
|
|
43
|
+
- name: Install uv
|
|
44
|
+
uses: astral-sh/setup-uv@v6
|
|
45
|
+
- name: Ruff tests
|
|
46
|
+
run: uv run ruff check tests --output-format=concise
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: langbot-plugin
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: This package contains the SDK, CLI for building plugins for LangBot, plus the runtime for hosting LangBot plugins
|
|
5
5
|
Project-URL: Homepage, https://langbot.app
|
|
6
6
|
Project-URL: Repository, https://github.com/langbot-app/langbot-plugin-sdk
|
|
@@ -9,7 +9,9 @@ Author-email: Junyan Qin <rockchinq@gmail.com>
|
|
|
9
9
|
License-File: LICENSE
|
|
10
10
|
Requires-Python: >=3.10
|
|
11
11
|
Requires-Dist: aiofiles>=24.1.0
|
|
12
|
+
Requires-Dist: aiohttp>=3.9.0
|
|
12
13
|
Requires-Dist: dotenv>=0.9.9
|
|
14
|
+
Requires-Dist: e2b>=2.15
|
|
13
15
|
Requires-Dist: httpx>=0.28.1
|
|
14
16
|
Requires-Dist: jinja2>=3.1.6
|
|
15
17
|
Requires-Dist: pip>=25.2
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "langbot-plugin"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.4.0"
|
|
4
4
|
description = "This package contains the SDK, CLI for building plugins for LangBot, plus the runtime for hosting LangBot plugins"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
authors = [
|
|
@@ -9,7 +9,9 @@ authors = [
|
|
|
9
9
|
requires-python = ">=3.10"
|
|
10
10
|
dependencies = [
|
|
11
11
|
"aiofiles>=24.1.0",
|
|
12
|
+
"aiohttp>=3.9.0",
|
|
12
13
|
"dotenv>=0.9.9",
|
|
14
|
+
"e2b>=2.15",
|
|
13
15
|
"httpx>=0.28.1",
|
|
14
16
|
"jinja2>=3.1.6",
|
|
15
17
|
"pip>=25.2",
|
|
@@ -27,6 +29,8 @@ dependencies = [
|
|
|
27
29
|
[dependency-groups]
|
|
28
30
|
dev = [
|
|
29
31
|
"mypy>=1.16.0",
|
|
32
|
+
"pytest-asyncio>=1.3.0",
|
|
33
|
+
"pytest-cov>=7.0.0",
|
|
30
34
|
"ruff>=0.11.12",
|
|
31
35
|
]
|
|
32
36
|
|
|
@@ -44,3 +48,17 @@ Issues = "https://github.com/langbot-app/langbot-plugin-sdk/issues"
|
|
|
44
48
|
|
|
45
49
|
[tool.setuptools]
|
|
46
50
|
package-data = { "langbot_plugin" = ["assets/templates/*", "assets/*.js"] }
|
|
51
|
+
|
|
52
|
+
[tool.pytest.ini_options]
|
|
53
|
+
addopts = "-ra"
|
|
54
|
+
testpaths = ["tests"]
|
|
55
|
+
asyncio_mode = "auto"
|
|
56
|
+
pythonpath = ["."]
|
|
57
|
+
|
|
58
|
+
[tool.coverage.run]
|
|
59
|
+
branch = true
|
|
60
|
+
source = ["langbot_plugin"]
|
|
61
|
+
|
|
62
|
+
[tool.coverage.report]
|
|
63
|
+
show_missing = true
|
|
64
|
+
skip_covered = true
|
|
@@ -37,8 +37,8 @@ class CommandReturn(pydantic.BaseModel):
|
|
|
37
37
|
"""错误,保留供系统使用,插件逻辑报错请自行使用 text 传递
|
|
38
38
|
"""
|
|
39
39
|
|
|
40
|
-
@classmethod
|
|
41
40
|
@pydantic.field_validator("error", mode="before")
|
|
41
|
+
@classmethod
|
|
42
42
|
def _validate_error(
|
|
43
43
|
cls, v: Optional[errors.CommandError]
|
|
44
44
|
) -> Optional[errors.CommandError]:
|
|
@@ -46,9 +46,8 @@ class CommandReturn(pydantic.BaseModel):
|
|
|
46
46
|
return errors.CommandError(message=v.message)
|
|
47
47
|
return v
|
|
48
48
|
|
|
49
|
-
@classmethod
|
|
50
49
|
@pydantic.field_serializer("error")
|
|
51
|
-
def _serialize_error(
|
|
50
|
+
def _serialize_error(self, v: Optional[errors.CommandError]) -> Optional[str]:
|
|
52
51
|
if v is not None:
|
|
53
52
|
return v.message
|
|
54
53
|
return v
|
|
@@ -13,20 +13,20 @@ class CommandError(pydantic.BaseModel):
|
|
|
13
13
|
|
|
14
14
|
|
|
15
15
|
class CommandNotFoundError(CommandError):
|
|
16
|
-
def __init__(self, message: str =
|
|
16
|
+
def __init__(self, message: str = ""):
|
|
17
17
|
super().__init__("未知命令: " + message)
|
|
18
18
|
|
|
19
19
|
|
|
20
20
|
class CommandPrivilegeError(CommandError):
|
|
21
|
-
def __init__(self, message: str =
|
|
21
|
+
def __init__(self, message: str = ""):
|
|
22
22
|
super().__init__("权限不足: " + message)
|
|
23
23
|
|
|
24
24
|
|
|
25
25
|
class ParamNotEnoughError(CommandError):
|
|
26
|
-
def __init__(self, message: str =
|
|
26
|
+
def __init__(self, message: str = ""):
|
|
27
27
|
super().__init__("参数不足: " + message)
|
|
28
28
|
|
|
29
29
|
|
|
30
30
|
class CommandOperationError(CommandError):
|
|
31
|
-
def __init__(self, message: str =
|
|
31
|
+
def __init__(self, message: str = ""):
|
|
32
32
|
super().__init__("操作失败: " + message)
|
|
@@ -132,7 +132,8 @@ class Message(pydantic.BaseModel):
|
|
|
132
132
|
platform_message.File(url=ce.file_url, name=ce.file_name)
|
|
133
133
|
)
|
|
134
134
|
elif ce.type == "image_url":
|
|
135
|
-
|
|
135
|
+
if ce.image_url is None:
|
|
136
|
+
raise ValueError("image_url content requires image_url payload")
|
|
136
137
|
if ce.image_url.url.startswith("http"):
|
|
137
138
|
mc.append(platform_message.Image(url=ce.image_url.url))
|
|
138
139
|
# else: # base64, for backward compatibility
|
|
@@ -223,6 +224,8 @@ class MessageChunk(pydantic.BaseModel):
|
|
|
223
224
|
platform_message.File(url=ce.file_url, name=ce.file_name)
|
|
224
225
|
)
|
|
225
226
|
elif ce.type == "image_url":
|
|
227
|
+
if ce.image_url is None:
|
|
228
|
+
raise ValueError("image_url content requires image_url payload")
|
|
226
229
|
if ce.image_url.url.startswith("http"):
|
|
227
230
|
mc.append(platform_message.Image(url=ce.image_url.url))
|
|
228
231
|
# else: # base64
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""Box-specific action types for the action RPC protocol."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from langbot_plugin.entities.io.actions.enums import ActionType
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class LangBotToBoxAction(ActionType):
|
|
9
|
+
"""Actions sent from LangBot to the Box runtime."""
|
|
10
|
+
|
|
11
|
+
INIT = "box_init" # Initialize with full box config (highest priority)
|
|
12
|
+
HEALTH = "box_health"
|
|
13
|
+
STATUS = "box_status"
|
|
14
|
+
EXEC = "box_exec"
|
|
15
|
+
CREATE_SESSION = "box_create_session"
|
|
16
|
+
GET_SESSION = "box_get_session"
|
|
17
|
+
GET_SESSIONS = "box_get_sessions"
|
|
18
|
+
DELETE_SESSION = "box_delete_session"
|
|
19
|
+
START_MANAGED_PROCESS = "box_start_managed_process"
|
|
20
|
+
GET_MANAGED_PROCESS = "box_get_managed_process"
|
|
21
|
+
STOP_MANAGED_PROCESS = "box_stop_managed_process"
|
|
22
|
+
GET_BACKEND_INFO = "box_get_backend_info"
|
|
23
|
+
LIST_SKILLS = "box_list_skills"
|
|
24
|
+
GET_SKILL = "box_get_skill"
|
|
25
|
+
CREATE_SKILL = "box_create_skill"
|
|
26
|
+
UPDATE_SKILL = "box_update_skill"
|
|
27
|
+
DELETE_SKILL = "box_delete_skill"
|
|
28
|
+
SCAN_SKILL_DIRECTORY = "box_scan_skill_directory"
|
|
29
|
+
LIST_SKILL_FILES = "box_list_skill_files"
|
|
30
|
+
READ_SKILL_FILE = "box_read_skill_file"
|
|
31
|
+
WRITE_SKILL_FILE = "box_write_skill_file"
|
|
32
|
+
PREVIEW_SKILL_ZIP = "box_preview_skill_zip"
|
|
33
|
+
INSTALL_SKILL_ZIP = "box_install_skill_zip"
|
|
34
|
+
SHUTDOWN = "box_shutdown"
|
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import abc
|
|
4
|
+
import asyncio
|
|
5
|
+
import dataclasses
|
|
6
|
+
import datetime as dt
|
|
7
|
+
import logging
|
|
8
|
+
import re
|
|
9
|
+
import shlex
|
|
10
|
+
import shutil
|
|
11
|
+
import uuid
|
|
12
|
+
|
|
13
|
+
from .errors import BoxError
|
|
14
|
+
from .models import (
|
|
15
|
+
BoxExecutionResult,
|
|
16
|
+
BoxExecutionStatus,
|
|
17
|
+
BoxHostMountMode,
|
|
18
|
+
BoxNetworkMode,
|
|
19
|
+
BoxSessionInfo,
|
|
20
|
+
BoxSpec,
|
|
21
|
+
)
|
|
22
|
+
from .security import validate_sandbox_security
|
|
23
|
+
|
|
24
|
+
# Hard cap on raw subprocess output to prevent unbounded memory usage.
|
|
25
|
+
# Container timeout already bounds duration, but fast commands can still
|
|
26
|
+
# produce large output within the time limit. After this many bytes the
|
|
27
|
+
# remaining output is discarded before decoding.
|
|
28
|
+
_MAX_RAW_OUTPUT_BYTES = 1_048_576 # 1 MB per stream
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclasses.dataclass(slots=True)
|
|
32
|
+
class _CommandResult:
|
|
33
|
+
return_code: int
|
|
34
|
+
stdout: str
|
|
35
|
+
stderr: str
|
|
36
|
+
timed_out: bool = False
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class BaseSandboxBackend(abc.ABC):
|
|
40
|
+
name: str
|
|
41
|
+
instance_id: str = ''
|
|
42
|
+
|
|
43
|
+
def __init__(self, logger: logging.Logger):
|
|
44
|
+
self.logger = logger
|
|
45
|
+
|
|
46
|
+
async def initialize(self):
|
|
47
|
+
return None
|
|
48
|
+
|
|
49
|
+
@abc.abstractmethod
|
|
50
|
+
async def is_available(self) -> bool:
|
|
51
|
+
pass
|
|
52
|
+
|
|
53
|
+
@abc.abstractmethod
|
|
54
|
+
async def start_session(self, spec: BoxSpec) -> BoxSessionInfo:
|
|
55
|
+
pass
|
|
56
|
+
|
|
57
|
+
@abc.abstractmethod
|
|
58
|
+
async def exec(self, session: BoxSessionInfo, spec: BoxSpec) -> BoxExecutionResult:
|
|
59
|
+
pass
|
|
60
|
+
|
|
61
|
+
@abc.abstractmethod
|
|
62
|
+
async def stop_session(self, session: BoxSessionInfo):
|
|
63
|
+
pass
|
|
64
|
+
|
|
65
|
+
async def is_session_alive(self, session: BoxSessionInfo) -> bool:
|
|
66
|
+
return True
|
|
67
|
+
|
|
68
|
+
async def start_managed_process(self, session: BoxSessionInfo, spec):
|
|
69
|
+
raise BoxError(f'{self.name} backend does not support managed processes')
|
|
70
|
+
|
|
71
|
+
async def cleanup_orphaned_containers(self, current_instance_id: str = ''):
|
|
72
|
+
"""Remove lingering containers from previous runs. No-op by default."""
|
|
73
|
+
pass
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class CLISandboxBackend(BaseSandboxBackend):
|
|
77
|
+
command: str
|
|
78
|
+
|
|
79
|
+
def __init__(self, logger: logging.Logger, command: str, backend_name: str):
|
|
80
|
+
super().__init__(logger)
|
|
81
|
+
self.command = command
|
|
82
|
+
self.name = backend_name
|
|
83
|
+
|
|
84
|
+
async def is_available(self) -> bool:
|
|
85
|
+
if shutil.which(self.command) is None:
|
|
86
|
+
return False
|
|
87
|
+
|
|
88
|
+
result = await self._run_command([self.command, 'info'], timeout_sec=5, check=False)
|
|
89
|
+
return result.return_code == 0 and not result.timed_out
|
|
90
|
+
|
|
91
|
+
async def start_session(self, spec: BoxSpec) -> BoxSessionInfo:
|
|
92
|
+
validate_sandbox_security(spec)
|
|
93
|
+
|
|
94
|
+
now = dt.datetime.now(dt.timezone.utc)
|
|
95
|
+
container_name = self._build_container_name(spec.session_id)
|
|
96
|
+
|
|
97
|
+
args = [
|
|
98
|
+
self.command,
|
|
99
|
+
'run',
|
|
100
|
+
'-d',
|
|
101
|
+
]
|
|
102
|
+
|
|
103
|
+
if not spec.persistent:
|
|
104
|
+
args.append('--rm')
|
|
105
|
+
|
|
106
|
+
args.extend([
|
|
107
|
+
'--name',
|
|
108
|
+
container_name,
|
|
109
|
+
'--label',
|
|
110
|
+
'langbot.box=true',
|
|
111
|
+
'--label',
|
|
112
|
+
f'langbot.session_id={spec.session_id}',
|
|
113
|
+
'--label',
|
|
114
|
+
f'langbot.box.instance_id={self.instance_id}',
|
|
115
|
+
])
|
|
116
|
+
|
|
117
|
+
if spec.network == BoxNetworkMode.OFF:
|
|
118
|
+
args.extend(['--network', 'none'])
|
|
119
|
+
|
|
120
|
+
# Resource limits
|
|
121
|
+
args.extend(['--cpus', str(spec.cpus)])
|
|
122
|
+
args.extend(['--memory', f'{spec.memory_mb}m'])
|
|
123
|
+
args.extend(['--pids-limit', str(spec.pids_limit)])
|
|
124
|
+
|
|
125
|
+
if spec.read_only_rootfs:
|
|
126
|
+
args.append('--read-only')
|
|
127
|
+
args.extend(['--tmpfs', '/tmp:size=64m'])
|
|
128
|
+
|
|
129
|
+
if spec.host_path is not None and spec.host_path_mode != BoxHostMountMode.NONE:
|
|
130
|
+
mount_spec = f'{spec.host_path}:{spec.mount_path}:{spec.host_path_mode.value}'
|
|
131
|
+
args.extend(['-v', mount_spec])
|
|
132
|
+
|
|
133
|
+
for mount in spec.extra_mounts:
|
|
134
|
+
if mount.mode != BoxHostMountMode.NONE:
|
|
135
|
+
args.extend(['-v', f'{mount.host_path}:{mount.mount_path}:{mount.mode.value}'])
|
|
136
|
+
|
|
137
|
+
args.extend([spec.image, 'sh', '-lc', 'while true; do sleep 3600; done'])
|
|
138
|
+
|
|
139
|
+
self.logger.info(
|
|
140
|
+
f'LangBot Box backend start_session: backend={self.name} '
|
|
141
|
+
f'session_id={spec.session_id} container_name={container_name} '
|
|
142
|
+
f'image={spec.image} network={spec.network.value} '
|
|
143
|
+
f'host_path={spec.host_path} host_path_mode={spec.host_path_mode.value} mount_path={spec.mount_path} '
|
|
144
|
+
f'cpus={spec.cpus} memory_mb={spec.memory_mb} pids_limit={spec.pids_limit} '
|
|
145
|
+
f'read_only_rootfs={spec.read_only_rootfs} workspace_quota_mb={spec.workspace_quota_mb}'
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
await self._run_command(args, timeout_sec=30, check=True)
|
|
149
|
+
|
|
150
|
+
return BoxSessionInfo(
|
|
151
|
+
session_id=spec.session_id,
|
|
152
|
+
backend_name=self.name,
|
|
153
|
+
backend_session_id=container_name,
|
|
154
|
+
image=spec.image,
|
|
155
|
+
network=spec.network,
|
|
156
|
+
host_path=spec.host_path,
|
|
157
|
+
host_path_mode=spec.host_path_mode,
|
|
158
|
+
mount_path=spec.mount_path,
|
|
159
|
+
persistent=spec.persistent,
|
|
160
|
+
cpus=spec.cpus,
|
|
161
|
+
memory_mb=spec.memory_mb,
|
|
162
|
+
pids_limit=spec.pids_limit,
|
|
163
|
+
read_only_rootfs=spec.read_only_rootfs,
|
|
164
|
+
workspace_quota_mb=spec.workspace_quota_mb,
|
|
165
|
+
created_at=now,
|
|
166
|
+
last_used_at=now,
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
async def exec(self, session: BoxSessionInfo, spec: BoxSpec) -> BoxExecutionResult:
|
|
170
|
+
start = dt.datetime.now(dt.timezone.utc)
|
|
171
|
+
args = [self.command, 'exec']
|
|
172
|
+
|
|
173
|
+
for key, value in spec.env.items():
|
|
174
|
+
args.extend(['-e', f'{key}={value}'])
|
|
175
|
+
|
|
176
|
+
args.extend(
|
|
177
|
+
[
|
|
178
|
+
session.backend_session_id,
|
|
179
|
+
'sh',
|
|
180
|
+
'-lc',
|
|
181
|
+
self._build_exec_command(spec.workdir, spec.cmd),
|
|
182
|
+
]
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
cmd_preview = spec.cmd.strip()
|
|
186
|
+
if len(cmd_preview) > 400:
|
|
187
|
+
cmd_preview = f'{cmd_preview[:397]}...'
|
|
188
|
+
self.logger.info(
|
|
189
|
+
f'LangBot Box backend exec: backend={self.name} '
|
|
190
|
+
f'session_id={session.session_id} container_name={session.backend_session_id} '
|
|
191
|
+
f'workdir={spec.workdir} timeout_sec={spec.timeout_sec} '
|
|
192
|
+
f'env_keys={sorted(spec.env.keys())} cmd={cmd_preview}'
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
result = await self._run_command(args, timeout_sec=spec.timeout_sec, check=False)
|
|
196
|
+
duration_ms = int((dt.datetime.now(dt.timezone.utc) - start).total_seconds() * 1000)
|
|
197
|
+
|
|
198
|
+
if result.timed_out:
|
|
199
|
+
return BoxExecutionResult(
|
|
200
|
+
session_id=session.session_id,
|
|
201
|
+
backend_name=self.name,
|
|
202
|
+
status=BoxExecutionStatus.TIMED_OUT,
|
|
203
|
+
exit_code=None,
|
|
204
|
+
stdout=result.stdout,
|
|
205
|
+
stderr=result.stderr or f'Command timed out after {spec.timeout_sec} seconds.',
|
|
206
|
+
duration_ms=duration_ms,
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
return BoxExecutionResult(
|
|
210
|
+
session_id=session.session_id,
|
|
211
|
+
backend_name=self.name,
|
|
212
|
+
status=BoxExecutionStatus.COMPLETED,
|
|
213
|
+
exit_code=result.return_code,
|
|
214
|
+
stdout=result.stdout,
|
|
215
|
+
stderr=result.stderr,
|
|
216
|
+
duration_ms=duration_ms,
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
async def stop_session(self, session: BoxSessionInfo):
|
|
220
|
+
self.logger.info(
|
|
221
|
+
f'LangBot Box backend stop_session: backend={self.name} '
|
|
222
|
+
f'session_id={session.session_id} container_name={session.backend_session_id}'
|
|
223
|
+
)
|
|
224
|
+
await self._run_command(
|
|
225
|
+
[self.command, 'rm', '-f', session.backend_session_id],
|
|
226
|
+
timeout_sec=20,
|
|
227
|
+
check=False,
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
async def is_session_alive(self, session: BoxSessionInfo) -> bool:
|
|
231
|
+
result = await self._run_command(
|
|
232
|
+
[
|
|
233
|
+
self.command,
|
|
234
|
+
'inspect',
|
|
235
|
+
'-f',
|
|
236
|
+
'{{.State.Running}}',
|
|
237
|
+
session.backend_session_id,
|
|
238
|
+
],
|
|
239
|
+
timeout_sec=5,
|
|
240
|
+
check=False,
|
|
241
|
+
)
|
|
242
|
+
return result.return_code == 0 and result.stdout.strip().lower() == 'true'
|
|
243
|
+
|
|
244
|
+
async def cleanup_orphaned_containers(self, current_instance_id: str = ''):
|
|
245
|
+
"""Remove langbot.box containers from previous instances.
|
|
246
|
+
|
|
247
|
+
Only removes containers whose ``langbot.box.instance_id`` label does
|
|
248
|
+
NOT match *current_instance_id*. Containers without the label (from
|
|
249
|
+
older versions) are also removed.
|
|
250
|
+
"""
|
|
251
|
+
result = await self._run_command(
|
|
252
|
+
[
|
|
253
|
+
self.command,
|
|
254
|
+
'ps',
|
|
255
|
+
'-a',
|
|
256
|
+
'--filter',
|
|
257
|
+
'label=langbot.box=true',
|
|
258
|
+
'--format',
|
|
259
|
+
'{{.ID}}\t{{.Label "langbot.box.instance_id"}}',
|
|
260
|
+
],
|
|
261
|
+
timeout_sec=10,
|
|
262
|
+
check=False,
|
|
263
|
+
)
|
|
264
|
+
if result.return_code != 0 or not result.stdout.strip():
|
|
265
|
+
return
|
|
266
|
+
orphan_ids = []
|
|
267
|
+
for line in result.stdout.strip().split('\n'):
|
|
268
|
+
line = line.strip()
|
|
269
|
+
if not line:
|
|
270
|
+
continue
|
|
271
|
+
parts = line.split('\t', 1)
|
|
272
|
+
cid = parts[0].strip()
|
|
273
|
+
label_instance = parts[1].strip() if len(parts) > 1 else ''
|
|
274
|
+
if label_instance != current_instance_id:
|
|
275
|
+
orphan_ids.append(cid)
|
|
276
|
+
if not orphan_ids:
|
|
277
|
+
return
|
|
278
|
+
for cid in orphan_ids:
|
|
279
|
+
self.logger.info(f'Cleaning up orphaned Box container: {cid}')
|
|
280
|
+
await self._run_command(
|
|
281
|
+
[self.command, 'rm', '-f', *orphan_ids],
|
|
282
|
+
timeout_sec=30,
|
|
283
|
+
check=False,
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
async def start_managed_process(self, session: BoxSessionInfo, spec) -> asyncio.subprocess.Process:
|
|
287
|
+
args = [self.command, 'exec', '-i']
|
|
288
|
+
|
|
289
|
+
for key, value in spec.env.items():
|
|
290
|
+
args.extend(['-e', f'{key}={value}'])
|
|
291
|
+
|
|
292
|
+
args.extend(
|
|
293
|
+
[
|
|
294
|
+
session.backend_session_id,
|
|
295
|
+
'sh',
|
|
296
|
+
'-lc',
|
|
297
|
+
self._build_spawn_command(spec.cwd, spec.command, spec.args),
|
|
298
|
+
]
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
self.logger.info(
|
|
302
|
+
f'LangBot Box backend start_managed_process: backend={self.name} '
|
|
303
|
+
f'session_id={session.session_id} container_name={session.backend_session_id} '
|
|
304
|
+
f'cwd={spec.cwd} env_keys={sorted(spec.env.keys())} command={spec.command} args={spec.args}'
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
return await asyncio.create_subprocess_exec(
|
|
308
|
+
*args,
|
|
309
|
+
stdin=asyncio.subprocess.PIPE,
|
|
310
|
+
stdout=asyncio.subprocess.PIPE,
|
|
311
|
+
stderr=asyncio.subprocess.PIPE,
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
def _build_container_name(self, session_id: str) -> str:
|
|
315
|
+
normalized = re.sub(r'[^a-zA-Z0-9_.-]+', '-', session_id).strip('-').lower() or 'session'
|
|
316
|
+
suffix = uuid.uuid4().hex[:8]
|
|
317
|
+
return f'langbot-box-{normalized[:32]}-{suffix}'
|
|
318
|
+
|
|
319
|
+
def _build_exec_command(self, workdir: str, cmd: str) -> str:
|
|
320
|
+
quoted_workdir = shlex.quote(workdir)
|
|
321
|
+
return f'mkdir -p {quoted_workdir} && cd {quoted_workdir} && {cmd}'
|
|
322
|
+
|
|
323
|
+
def _build_spawn_command(self, cwd: str, command: str, args: list[str]) -> str:
|
|
324
|
+
quoted_cwd = shlex.quote(cwd)
|
|
325
|
+
command_parts = [shlex.quote(command), *[shlex.quote(arg) for arg in args]]
|
|
326
|
+
return f'mkdir -p {quoted_cwd} && cd {quoted_cwd} && exec {" ".join(command_parts)}'
|
|
327
|
+
|
|
328
|
+
async def _run_command(
|
|
329
|
+
self,
|
|
330
|
+
args: list[str],
|
|
331
|
+
timeout_sec: int,
|
|
332
|
+
check: bool,
|
|
333
|
+
) -> _CommandResult:
|
|
334
|
+
process = await asyncio.create_subprocess_exec(
|
|
335
|
+
*args,
|
|
336
|
+
stdout=asyncio.subprocess.PIPE,
|
|
337
|
+
stderr=asyncio.subprocess.PIPE,
|
|
338
|
+
)
|
|
339
|
+
stdout_task = asyncio.create_task(self._read_stream(process.stdout))
|
|
340
|
+
stderr_task = asyncio.create_task(self._read_stream(process.stderr))
|
|
341
|
+
|
|
342
|
+
timed_out = False
|
|
343
|
+
try:
|
|
344
|
+
await asyncio.wait_for(process.wait(), timeout=timeout_sec)
|
|
345
|
+
except asyncio.TimeoutError:
|
|
346
|
+
process.kill()
|
|
347
|
+
timed_out = True
|
|
348
|
+
await process.wait()
|
|
349
|
+
|
|
350
|
+
stdout_bytes, stdout_total = await stdout_task
|
|
351
|
+
stderr_bytes, stderr_total = await stderr_task
|
|
352
|
+
|
|
353
|
+
if timed_out:
|
|
354
|
+
return _CommandResult(
|
|
355
|
+
return_code=-1,
|
|
356
|
+
stdout=self._clip_captured_bytes(stdout_bytes, stdout_total),
|
|
357
|
+
stderr=self._clip_captured_bytes(stderr_bytes, stderr_total),
|
|
358
|
+
timed_out=True,
|
|
359
|
+
)
|
|
360
|
+
|
|
361
|
+
stdout = self._clip_captured_bytes(stdout_bytes, stdout_total)
|
|
362
|
+
stderr = self._clip_captured_bytes(stderr_bytes, stderr_total)
|
|
363
|
+
|
|
364
|
+
if check and process.returncode != 0:
|
|
365
|
+
raise BoxError(self._format_cli_error(stderr or stdout or 'unknown backend error'))
|
|
366
|
+
|
|
367
|
+
return _CommandResult(
|
|
368
|
+
return_code=process.returncode,
|
|
369
|
+
stdout=stdout,
|
|
370
|
+
stderr=stderr,
|
|
371
|
+
timed_out=False,
|
|
372
|
+
)
|
|
373
|
+
|
|
374
|
+
@staticmethod
|
|
375
|
+
def _clip_captured_bytes(data: bytes, total_size: int, limit: int = _MAX_RAW_OUTPUT_BYTES) -> str:
|
|
376
|
+
text = data.decode('utf-8', errors='replace').strip()
|
|
377
|
+
if total_size > limit:
|
|
378
|
+
text += f'\n... [raw output clipped at {limit} bytes, {total_size - limit} bytes discarded]'
|
|
379
|
+
return text
|
|
380
|
+
|
|
381
|
+
@staticmethod
|
|
382
|
+
async def _read_stream(
|
|
383
|
+
stream: asyncio.StreamReader | None,
|
|
384
|
+
limit: int = _MAX_RAW_OUTPUT_BYTES,
|
|
385
|
+
) -> tuple[bytes, int]:
|
|
386
|
+
if stream is None:
|
|
387
|
+
return b'', 0
|
|
388
|
+
|
|
389
|
+
chunks = bytearray()
|
|
390
|
+
total_size = 0
|
|
391
|
+
while True:
|
|
392
|
+
chunk = await stream.read(65536)
|
|
393
|
+
if not chunk:
|
|
394
|
+
break
|
|
395
|
+
total_size += len(chunk)
|
|
396
|
+
remaining = limit - len(chunks)
|
|
397
|
+
if remaining > 0:
|
|
398
|
+
chunks.extend(chunk[:remaining])
|
|
399
|
+
|
|
400
|
+
return bytes(chunks), total_size
|
|
401
|
+
|
|
402
|
+
def _format_cli_error(self, message: str) -> str:
|
|
403
|
+
message = ' '.join(message.split())
|
|
404
|
+
if len(message) > 300:
|
|
405
|
+
message = f'{message[:297]}...'
|
|
406
|
+
return f'{self.name} backend error: {message}'
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
class DockerBackend(CLISandboxBackend):
|
|
410
|
+
def __init__(self, logger: logging.Logger):
|
|
411
|
+
super().__init__(logger=logger, command='docker', backend_name='docker')
|