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.
Files changed (237) hide show
  1. langbot_plugin-0.4.0/.github/workflows/test.yml +46 -0
  2. {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/.gitignore +1 -0
  3. {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/PKG-INFO +3 -1
  4. {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/pyproject.toml +19 -1
  5. {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/api/entities/builtin/command/context.py +2 -3
  6. {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/api/entities/builtin/command/errors.py +4 -4
  7. {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/api/entities/builtin/provider/message.py +4 -1
  8. langbot_plugin-0.4.0/src/langbot_plugin/box/__init__.py +5 -0
  9. langbot_plugin-0.4.0/src/langbot_plugin/box/actions.py +34 -0
  10. langbot_plugin-0.4.0/src/langbot_plugin/box/backend.py +411 -0
  11. langbot_plugin-0.4.0/src/langbot_plugin/box/client.py +377 -0
  12. langbot_plugin-0.4.0/src/langbot_plugin/box/e2b_backend.py +429 -0
  13. langbot_plugin-0.4.0/src/langbot_plugin/box/errors.py +33 -0
  14. langbot_plugin-0.4.0/src/langbot_plugin/box/models.py +331 -0
  15. langbot_plugin-0.4.0/src/langbot_plugin/box/nsjail_backend.py +552 -0
  16. langbot_plugin-0.4.0/src/langbot_plugin/box/runtime.py +598 -0
  17. langbot_plugin-0.4.0/src/langbot_plugin/box/security.py +52 -0
  18. langbot_plugin-0.4.0/src/langbot_plugin/box/server.py +494 -0
  19. langbot_plugin-0.4.0/src/langbot_plugin/box/skill_store.py +647 -0
  20. {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/cli/__init__.py +57 -1
  21. {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/cli/commands/runplugin.py +23 -3
  22. {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/entities/io/actions/enums.py +5 -0
  23. {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/runtime/app.py +5 -0
  24. {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/runtime/helper/pkgmgr.py +36 -24
  25. {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/runtime/io/handler.py +13 -5
  26. {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/runtime/io/handlers/control.py +18 -0
  27. {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/runtime/plugin/container.py +3 -4
  28. {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/runtime/plugin/mgr.py +21 -5
  29. {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/utils/discover/engine.py +3 -0
  30. langbot_plugin-0.4.0/src/langbot_plugin/utils/importutil.py +52 -0
  31. langbot_plugin-0.4.0/src/langbot_plugin/version.py +1 -0
  32. langbot_plugin-0.4.0/tests/__init__.py +1 -0
  33. langbot_plugin-0.4.0/tests/api/definition/components/test_components.py +97 -0
  34. langbot_plugin-0.4.0/tests/api/definition/test_manifest.py +124 -0
  35. langbot_plugin-0.4.0/tests/api/entities/builtin/test_command_context.py +77 -0
  36. langbot_plugin-0.4.0/tests/api/entities/builtin/test_platform_logger.py +23 -0
  37. langbot_plugin-0.4.0/tests/api/entities/builtin/test_provider_message.py +107 -0
  38. langbot_plugin-0.4.0/tests/api/entities/builtin/test_rag_models.py +133 -0
  39. langbot_plugin-0.4.0/tests/api/entities/test_context.py +163 -0
  40. langbot_plugin-0.4.0/tests/api/proxies/test_base.py +11 -0
  41. langbot_plugin-0.4.0/tests/api/proxies/test_langbot_api.py +218 -0
  42. langbot_plugin-0.4.0/tests/api/proxies/test_query_based_api.py +96 -0
  43. langbot_plugin-0.4.0/tests/box/__init__.py +0 -0
  44. langbot_plugin-0.4.0/tests/box/test_backend.py +865 -0
  45. langbot_plugin-0.4.0/tests/box/test_backend_selection.py +275 -0
  46. langbot_plugin-0.4.0/tests/box/test_client.py +658 -0
  47. langbot_plugin-0.4.0/tests/box/test_e2b_backend.py +482 -0
  48. langbot_plugin-0.4.0/tests/box/test_nsjail_backend.py +445 -0
  49. langbot_plugin-0.4.0/tests/box/test_runtime.py +805 -0
  50. langbot_plugin-0.4.0/tests/box/test_server.py +859 -0
  51. langbot_plugin-0.4.0/tests/box/test_skill_store.py +914 -0
  52. langbot_plugin-0.4.0/tests/cli/run/test_controller.py +172 -0
  53. langbot_plugin-0.4.0/tests/cli/run/test_runtime_handler.py +338 -0
  54. langbot_plugin-0.4.0/tests/cli/test_buildplugin.py +81 -0
  55. langbot_plugin-0.4.0/tests/cli/test_gencomponent.py +90 -0
  56. langbot_plugin-0.4.0/tests/cli/test_i18n_form.py +63 -0
  57. langbot_plugin-0.4.0/tests/cli/test_initplugin.py +102 -0
  58. langbot_plugin-0.4.0/tests/cli/test_login.py +516 -0
  59. langbot_plugin-0.4.0/tests/cli/test_logout_publish.py +184 -0
  60. langbot_plugin-0.4.0/tests/cli/test_page_components.py +63 -0
  61. langbot_plugin-0.4.0/tests/cli/test_renderer.py +64 -0
  62. langbot_plugin-0.4.0/tests/cli/test_runplugin.py +257 -0
  63. langbot_plugin-0.4.0/tests/entities/io/test_protocol.py +88 -0
  64. langbot_plugin-0.4.0/tests/helpers/__init__.py +1 -0
  65. langbot_plugin-0.4.0/tests/helpers/protocol.py +120 -0
  66. langbot_plugin-0.4.0/tests/runtime/helper/test_marketplace.py +131 -0
  67. langbot_plugin-0.4.0/tests/runtime/helper/test_pkgmgr.py +109 -0
  68. langbot_plugin-0.4.0/tests/runtime/io/handlers/test_control_handler.py +452 -0
  69. langbot_plugin-0.4.0/tests/runtime/io/handlers/test_import_contracts.py +24 -0
  70. langbot_plugin-0.4.0/tests/runtime/io/handlers/test_plugin_handler.py +317 -0
  71. langbot_plugin-0.4.0/tests/runtime/io/test_connections.py +148 -0
  72. langbot_plugin-0.4.0/tests/runtime/io/test_controllers.py +193 -0
  73. langbot_plugin-0.4.0/tests/runtime/io/test_handler.py +228 -0
  74. langbot_plugin-0.4.0/tests/runtime/plugin/test_container.py +124 -0
  75. langbot_plugin-0.4.0/tests/runtime/plugin/test_manager.py +536 -0
  76. langbot_plugin-0.4.0/tests/runtime/test_app.py +389 -0
  77. langbot_plugin-0.4.0/tests/utils/test_discovery.py +120 -0
  78. langbot_plugin-0.4.0/tests/utils/test_importutil.py +54 -0
  79. langbot_plugin-0.4.0/tests/utils/test_platform.py +9 -0
  80. langbot_plugin-0.3.10/src/langbot_plugin/utils/importutil.py +0 -38
  81. langbot_plugin-0.3.10/src/langbot_plugin/version.py +0 -1
  82. {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/.github/workflows/publish-pypi.yaml +0 -0
  83. {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/.python-version +0 -0
  84. {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/AGENTS.md +0 -0
  85. {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/CLAUDE.md +0 -0
  86. {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/LICENSE +0 -0
  87. {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/README.md +0 -0
  88. {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/data/.env.example +0 -0
  89. {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/docs/Message.md +0 -0
  90. {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/docs/PluginPages.md +0 -0
  91. {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/docs/communication/apis/lb2rt.md +0 -0
  92. {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/docs/communication/runtime_plugin.md +0 -0
  93. {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/docs/dependency-management.md +0 -0
  94. {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/docs/langbot-plugin-social.png +0 -0
  95. {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/__init__.py +0 -0
  96. {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/api/__init__.py +0 -0
  97. {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/api/definition/__init__.py +0 -0
  98. {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/api/definition/abstract/__init__.py +0 -0
  99. {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/api/definition/abstract/platform/__init__.py +0 -0
  100. {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/api/definition/abstract/platform/adapter.py +0 -0
  101. {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/api/definition/abstract/platform/event_logger.py +0 -0
  102. {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/api/definition/components/__init__.py +0 -0
  103. {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/api/definition/components/base.py +0 -0
  104. {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/api/definition/components/command/__init__.py +0 -0
  105. {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/api/definition/components/command/command.py +0 -0
  106. {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/api/definition/components/common/__init__.py +0 -0
  107. {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/api/definition/components/common/event_listener.py +0 -0
  108. {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/api/definition/components/knowledge_engine/__init__.py +0 -0
  109. {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/api/definition/components/knowledge_engine/engine.py +0 -0
  110. {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/api/definition/components/manifest.py +0 -0
  111. {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/api/definition/components/page/__init__.py +0 -0
  112. {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/api/definition/components/parser/__init__.py +0 -0
  113. {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/api/definition/components/parser/parser.py +0 -0
  114. {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/api/definition/components/tool/__init__.py +0 -0
  115. {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/api/definition/components/tool/tool.py +0 -0
  116. {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/api/definition/plugin.py +0 -0
  117. {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/api/entities/__init__.py +0 -0
  118. {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/api/entities/builtin/__init__.py +0 -0
  119. {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/api/entities/builtin/command/__init__.py +0 -0
  120. {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/api/entities/builtin/pipeline/__init__.py +0 -0
  121. {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/api/entities/builtin/pipeline/query.py +0 -0
  122. {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/api/entities/builtin/platform/__init__.py +0 -0
  123. {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/api/entities/builtin/platform/entities.py +0 -0
  124. {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/api/entities/builtin/platform/events.py +0 -0
  125. {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/api/entities/builtin/platform/logger.py +0 -0
  126. {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/api/entities/builtin/platform/message.py +0 -0
  127. {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/api/entities/builtin/provider/__init__.py +0 -0
  128. {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/api/entities/builtin/provider/prompt.py +0 -0
  129. {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/api/entities/builtin/provider/session.py +0 -0
  130. {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/api/entities/builtin/rag/__init__.py +0 -0
  131. {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/api/entities/builtin/rag/context.py +0 -0
  132. {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/api/entities/builtin/rag/enums.py +0 -0
  133. {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/api/entities/builtin/rag/errors.py +0 -0
  134. {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/api/entities/builtin/rag/models.py +0 -0
  135. {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/api/entities/builtin/resource/__init__.py +0 -0
  136. {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/api/entities/builtin/resource/tool.py +0 -0
  137. {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/api/entities/context.py +0 -0
  138. {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/api/entities/events.py +0 -0
  139. {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/api/proxies/__init__.py +0 -0
  140. {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/api/proxies/base.py +0 -0
  141. {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/api/proxies/event_context.py +0 -0
  142. {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/api/proxies/execute_context.py +0 -0
  143. {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/api/proxies/langbot_api.py +0 -0
  144. {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/api/proxies/query_based_api.py +0 -0
  145. {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/assets/__init__.py +0 -0
  146. {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/assets/langbot-page-sdk.js +0 -0
  147. {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/assets/templates/.env.example.example +0 -0
  148. {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/assets/templates/.gitignore.example +0 -0
  149. {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/assets/templates/.vscode/launch.json.example +0 -0
  150. {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/assets/templates/README.md.example +0 -0
  151. {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/assets/templates/__init__.py +0 -0
  152. {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/assets/templates/assets/icon.svg.example +0 -0
  153. {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/assets/templates/components/__init__.py +0 -0
  154. {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/assets/templates/components/commands/__init__.py +0 -0
  155. {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/assets/templates/components/commands/{cmd_name}.py.example +0 -0
  156. {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/assets/templates/components/commands/{cmd_name}.yaml.example +0 -0
  157. {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/assets/templates/components/event_listener/__init__.py +0 -0
  158. {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/assets/templates/components/event_listener/default.py.example +0 -0
  159. {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/assets/templates/components/event_listener/default.yaml.example +0 -0
  160. {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/assets/templates/components/knowledge_engine/__init__.py +0 -0
  161. {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
  162. {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
  163. {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/assets/templates/components/pages/{page_name}.html.example +0 -0
  164. {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/assets/templates/components/pages/{page_name}.yaml.example +0 -0
  165. {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/assets/templates/components/parser/__init__.py +0 -0
  166. {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/assets/templates/components/parser/{parser_name}.py.example +0 -0
  167. {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/assets/templates/components/parser/{parser_name}.yaml.example +0 -0
  168. {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/assets/templates/components/tools/__init__.py +0 -0
  169. {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/assets/templates/components/tools/{tool_name}.py.example +0 -0
  170. {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/assets/templates/components/tools/{tool_name}.yaml.example +0 -0
  171. {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/assets/templates/main.py.example +0 -0
  172. {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/assets/templates/manifest.yaml.example +0 -0
  173. {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/assets/templates/requirements.txt.example +0 -0
  174. {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/cli/__main__.py +0 -0
  175. {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/cli/commands/__init__.py +0 -0
  176. {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/cli/commands/buildplugin.py +0 -0
  177. {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/cli/commands/gencomponent.py +0 -0
  178. {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/cli/commands/initplugin.py +0 -0
  179. {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/cli/commands/login.py +0 -0
  180. {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/cli/commands/logout.py +0 -0
  181. {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/cli/commands/publish.py +0 -0
  182. {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/cli/gen/__init__.py +0 -0
  183. {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/cli/gen/renderer.py +0 -0
  184. {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/cli/i18n.py +0 -0
  185. {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/cli/locales/__init__.py +0 -0
  186. {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/cli/locales/en_US.py +0 -0
  187. {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/cli/locales/es_ES.py +0 -0
  188. {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/cli/locales/ja_JP.py +0 -0
  189. {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/cli/locales/th_TH.py +0 -0
  190. {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/cli/locales/vi_VN.py +0 -0
  191. {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/cli/locales/zh_Hans.py +0 -0
  192. {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/cli/locales/zh_Hant.py +0 -0
  193. {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/cli/run/__init__.py +0 -0
  194. {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/cli/run/controller.py +0 -0
  195. {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/cli/run/handler.py +0 -0
  196. {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/cli/run/hotreload.py +0 -0
  197. {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/cli/utils/__init__.py +0 -0
  198. {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/cli/utils/cloudsv.py +0 -0
  199. {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/cli/utils/form.py +0 -0
  200. {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/cli/utils/page_components.py +0 -0
  201. {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/entities/__init__.py +0 -0
  202. {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/entities/io/__init__.py +0 -0
  203. {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/entities/io/actions/__init__.py +0 -0
  204. {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/entities/io/errors.py +0 -0
  205. {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/entities/io/req.py +0 -0
  206. {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/entities/io/resp.py +0 -0
  207. {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/entities/marketplace.py +0 -0
  208. {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/runtime/LICENSE +0 -0
  209. {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/runtime/README.md +0 -0
  210. {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/runtime/__init__.py +0 -0
  211. {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/runtime/context.py +0 -0
  212. {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/runtime/helper/__init__.py +0 -0
  213. {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/runtime/helper/marketplace.py +0 -0
  214. {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/runtime/io/__init__.py +0 -0
  215. {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/runtime/io/connection.py +0 -0
  216. {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/runtime/io/connections/__init__.py +0 -0
  217. {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/runtime/io/connections/stdio.py +0 -0
  218. {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/runtime/io/connections/ws.py +0 -0
  219. {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/runtime/io/controller.py +0 -0
  220. {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/runtime/io/controllers/__init__.py +0 -0
  221. {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/runtime/io/controllers/stdio/__init__.py +0 -0
  222. {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/runtime/io/controllers/stdio/client.py +0 -0
  223. {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/runtime/io/controllers/stdio/server.py +0 -0
  224. {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/runtime/io/controllers/ws/__init__.py +0 -0
  225. {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/runtime/io/controllers/ws/client.py +0 -0
  226. {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/runtime/io/controllers/ws/server.py +0 -0
  227. {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/runtime/io/handlers/__init__.py +0 -0
  228. {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/runtime/io/handlers/plugin.py +0 -0
  229. {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/runtime/plugin/__init__.py +0 -0
  230. {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/runtime/settings.py +0 -0
  231. {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/utils/__init__.py +0 -0
  232. {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/utils/discover/__init__.py +0 -0
  233. {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/utils/log.py +0 -0
  234. {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/src/langbot_plugin/utils/platform.py +0 -0
  235. {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/tests/api/entities/test_events.py +0 -0
  236. {langbot_plugin-0.3.10 → langbot_plugin-0.4.0}/tests/test_log.py +0 -0
  237. {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
@@ -174,6 +174,7 @@ cython_debug/
174
174
  .pypirc
175
175
 
176
176
  /data/plugins/
177
+ /data/box/
177
178
  /debug/
178
179
  uv.lock
179
180
  src/.DS_Store
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: langbot-plugin
3
- Version: 0.3.10
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.10"
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(cls, v: Optional[errors.CommandError]) -> Optional[str]:
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 = None):
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 = None):
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 = None):
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 = None):
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
- assert ce.image_url is not None
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,5 @@
1
+ """LangBot Box runtime package."""
2
+
3
+ from .client import BoxRuntimeClient, ActionRPCBoxClient
4
+
5
+ __all__ = ['BoxRuntimeClient', 'ActionRPCBoxClient']
@@ -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')