From 9f296bd57e0cd9778bb12de3ebd42240d53bcda1 Mon Sep 17 00:00:00 2001 From: RockChinQ Date: Fri, 25 Sep 2026 12:48:55 +0000 Subject: [PATCH] feat(plugin): route certified installs to shared workers --- docs/architecture/certified-plugins.md | 32 ++--- pyproject.toml | 2 +- src/langbot/pkg/plugin/certification.py | 25 ++++ src/langbot/pkg/plugin/connector.py | 23 +++- src/langbot/pkg/plugin/handler.py | 3 + .../plugin/test_certified_plugin_admission.py | 12 +- .../plugin/test_certified_plugin_policy.py | 76 +++++++++++ .../plugin/test_connector_methods.py | 14 +- .../plugin/test_connector_reconcile.py | 70 +++++++++- tests/unit_tests/plugin/test_handler.py | 27 +++- uv.lock | 126 +++++++++--------- 11 files changed, 320 insertions(+), 90 deletions(-) diff --git a/docs/architecture/certified-plugins.md b/docs/architecture/certified-plugins.md index f4c9400ac..61694df6e 100644 --- a/docs/architecture/certified-plugins.md +++ b/docs/architecture/certified-plugins.md @@ -8,11 +8,12 @@ persistence, or a Plugin Runtime apply request. It calls the SDK public certificate envelope from the ZIP comment and verifies the signed normalized ZIP digest without extracting the payload. -Core retains the normalized digest (`normalized_zip_digest()`), verification -state, declared shared-runtime profile, key ID, selected admission profile, and -stable admission code in the durable plugin `install_info._certification` -record. The record belongs to the installation row; no schema migration is -needed for this additive JSON metadata. +Core retains the artifact SHA-256, normalized digest +(`normalized_zip_digest()`), verification state, declared shared-runtime +profile, key ID, selected admission profile, and stable admission code in the +durable plugin `install_info._certification` record. The record belongs to the +installation row; no schema migration is needed for this additive JSON +metadata. ## Trusted issuer configuration @@ -50,13 +51,14 @@ protects those endpoints. A force never creates a Cloud dedicated fallback. ## Runtime and logs -The current Plugin Runtime control protocol has one process-wide runtime profile -per Core instance. In Cloud that existing profile is `shared`; Cloud admission -therefore prevents an archive that did not select `shared-runtime-v1` from -reaching its apply API. In OSS the existing `oss_dev` runtime remains the -dedicated compatibility profile. Core records the selected profile for every -installation so a future multi-runtime control protocol can consume it without -re-verifying an already persisted archive. +SDK 0.6.2 carries an installation-level execution mode in both apply and +authoritative reconcile payloads. Core selects `shared-runtime-v1` only when +the persisted certification record says verification was valid, both the +certificate and admission profiles are `shared-runtime-v1`, the admission code +is shared-eligible, and the record's artifact SHA-256 exactly matches the +installation row. Missing, malformed, stale, invalid, or dedicated admission +facts select `dedicated`. Install, upgrade, configuration revision, restart, +and reconnect all use this same persisted-fact derivation. The existing public plugin-log boundary already applies the immutable installation binding (including workspace UUID) through @@ -68,7 +70,5 @@ same existing installation scope. ## SDK versioning -Core intentionally continues to declare `langbot-plugin==0.5.8` until the SDK -beta containing this public certification API is released. Local development -and the integration tests may install the SDK source checkout, but this Core -change does not publish or pin a prerelease. +Core pins `langbot-plugin==0.6.2`, the first published SDK release carrying the +canonical installation execution-mode contract. diff --git a/pyproject.toml b/pyproject.toml index fd9c7a177..973d55160 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -71,7 +71,7 @@ dependencies = [ "langchain-text-splitters>=1.1.2", "chromadb>=1.0.0,<2.0.0", "qdrant-client (>=1.15.1,<2.0.0)", - "langbot-plugin==0.6.1", + "langbot-plugin==0.6.2", "asyncpg>=0.30.0", "line-bot-sdk>=3.19.0", "matrix-nio>=0.25.2", diff --git a/src/langbot/pkg/plugin/certification.py b/src/langbot/pkg/plugin/certification.py index d1a233847..65488d98b 100644 --- a/src/langbot/pkg/plugin/certification.py +++ b/src/langbot/pkg/plugin/certification.py @@ -14,6 +14,7 @@ from enum import Enum from cryptography.exceptions import InvalidSignature from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey from langbot_plugin.certification import normalized_zip_digest, verify_archive +from langbot_plugin.entities.io.context import PluginExecutionMode SHARED_RUNTIME_V1 = 'shared-runtime-v1' @@ -232,3 +233,27 @@ def decide_plugin_log_visibility(facts: PluginCertificationFacts) -> PluginLogVi if facts.certificate.is_valid_shared_runtime: return PluginLogVisibility.TENANT_SCOPED return PluginLogVisibility.DETAILED_PROCESS + + +def execution_mode_for_persisted_installation( + *, + artifact_digest: str, + install_info: object, +) -> PluginExecutionMode: + """Derive placement only from persisted facts bound to the exact artifact.""" + + if not isinstance(install_info, Mapping): + return PluginExecutionMode.DEDICATED + certification = install_info.get('_certification') + if not isinstance(certification, Mapping): + return PluginExecutionMode.DEDICATED + shared_facts = { + 'artifact_digest': artifact_digest, + 'verification': CertificateVerification.VALID.value, + 'certificate_runtime_profile': SHARED_RUNTIME_V1, + 'runtime_profile': SHARED_RUNTIME_V1, + 'admission_code': AdmissionCode.SHARED_ELIGIBLE.value, + } + if all(certification.get(key) == value for key, value in shared_facts.items()): + return PluginExecutionMode.SHARED_CERTIFIED + return PluginExecutionMode.DEDICATED diff --git a/src/langbot/pkg/plugin/connector.py b/src/langbot/pkg/plugin/connector.py index 47b54c20f..e9675d147 100644 --- a/src/langbot/pkg/plugin/connector.py +++ b/src/langbot/pkg/plugin/connector.py @@ -34,6 +34,7 @@ from .certification import ( PluginCertificationFacts, VerifiedArchiveCertificate, decide_plugin_admission, + execution_mode_for_persisted_installation, verify_plugin_archive_certificate, ) from .github import ( @@ -62,6 +63,7 @@ from langbot_plugin.runtime.security import ( ) from langbot_plugin.entities.io.context import ( InstallationBinding, + PluginExecutionMode, PluginInstallationDesiredState, PluginWorkerPolicy, RuntimeIdentity, @@ -343,6 +345,13 @@ class PluginRuntimeConnector(ManagedRuntimeConnector): artifact_digest=setting.artifact_digest, ) + @staticmethod + def _execution_mode_from_setting(setting: persistence_plugin.PluginSetting) -> PluginExecutionMode: + return execution_mode_for_persisted_installation( + artifact_digest=setting.artifact_digest, + install_info=setting.install_info, + ) + def _legacy_oss_bridge_binding(self, execution_context: ExecutionContext) -> InstallationBinding: seed = f'langbot:oss-plugin-bridge:{execution_context.instance_uuid}:{execution_context.workspace_uuid}' return InstallationBinding( @@ -511,6 +520,7 @@ class PluginRuntimeConnector(ManagedRuntimeConnector): PluginInstallationDesiredState( binding=binding, enabled=setting.enabled, + execution_mode=self._execution_mode_from_setting(setting), ) ) return tuple(desired_states) @@ -526,6 +536,7 @@ class PluginRuntimeConnector(ManagedRuntimeConnector): desired.binding, artifact_package=artifact_package, enabled=desired.enabled, + execution_mode=desired.execution_mode, ) self._raise_apply_failure(desired, result) if result.get('state') != 'artifact_missing': @@ -551,6 +562,7 @@ class PluginRuntimeConnector(ManagedRuntimeConnector): desired.binding, artifact_package=persisted_package, enabled=desired.enabled, + execution_mode=desired.execution_mode, ) self._raise_apply_failure(desired, repaired) if repaired.get('state') == 'artifact_missing': @@ -1793,6 +1805,7 @@ class PluginRuntimeConnector(ManagedRuntimeConnector): }: raise ValueError(decision.code.value) certification_info = { + 'artifact_digest': hashlib.sha256(file_bytes).hexdigest(), 'normalized_digest': facts.artifact_digest, 'verification': facts.certificate.verification.value, 'certificate_runtime_profile': facts.certificate.runtime_profile, @@ -1893,7 +1906,14 @@ class PluginRuntimeConnector(ManagedRuntimeConnector): install_info=install_info, artifact_digest=artifact_digest, ) - desired = PluginInstallationDesiredState(binding=binding, enabled=True) + desired = PluginInstallationDesiredState( + binding=binding, + enabled=True, + execution_mode=execution_mode_for_persisted_installation( + artifact_digest=artifact_digest, + install_info=install_info, + ), + ) runtime_handler.register_installation_binding( binding, plugin_author=plugin_author, @@ -2143,6 +2163,7 @@ class PluginRuntimeConnector(ManagedRuntimeConnector): desired = PluginInstallationDesiredState( binding=binding, enabled=setting.enabled, + execution_mode=self._execution_mode_from_setting(setting), ) is_legacy_oss = self.runtime_profile == 'oss_dev' and ( not isinstance(setting.install_info, dict) diff --git a/src/langbot/pkg/plugin/handler.py b/src/langbot/pkg/plugin/handler.py index 90e9e9a17..9df92e778 100644 --- a/src/langbot/pkg/plugin/handler.py +++ b/src/langbot/pkg/plugin/handler.py @@ -24,6 +24,7 @@ from langbot_plugin.entities.io.context import ( ActionContext, ApplyPluginInstallationRequest, InstallationBinding, + PluginExecutionMode, PluginInstallationDesiredState, PluginWorkerPolicy, ReconcilePluginInstallationsRequest, @@ -2793,6 +2794,7 @@ class RuntimeConnectionHandler(handler.Handler): *, artifact_package: bytes | None, enabled: bool, + execution_mode: PluginExecutionMode = PluginExecutionMode.DEDICATED, ) -> dict[str, Any]: with self.installation_scope(binding): artifact_file_key = None @@ -2801,6 +2803,7 @@ class RuntimeConnectionHandler(handler.Handler): request = ApplyPluginInstallationRequest( artifact_file_key=artifact_file_key, enabled=enabled, + execution_mode=execution_mode, ) return await self.call_action( LangBotToRuntimeAction.APPLY_PLUGIN_INSTALLATION, diff --git a/tests/integration/plugin/test_certified_plugin_admission.py b/tests/integration/plugin/test_certified_plugin_admission.py index 7da555e86..6106c9248 100644 --- a/tests/integration/plugin/test_certified_plugin_admission.py +++ b/tests/integration/plugin/test_certified_plugin_admission.py @@ -17,6 +17,7 @@ from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey from langbot.pkg.api.http.context import ExecutionContext from langbot.pkg.plugin.connector import PluginRuntimeConnector from langbot_plugin.entities.io.context import InstallationBinding +from langbot_plugin.entities.io.context import PluginExecutionMode from langbot_plugin.runtime.plugin.mgr import PluginInstallSource @@ -57,11 +58,17 @@ async def test_install_plugin_admits_archive_before_persistence_and_applies_sele ) persisted_info = connector._persist_installation_package.await_args.kwargs['install_info'] assert persisted_info['_certification']['runtime_profile'] == expected_profile + assert persisted_info['_certification']['artifact_digest'] == hashlib.sha256(package).hexdigest() assert persisted_info['_certification']['normalized_digest'] == _normalized_digest(package) connector.handler.apply_plugin_installation.assert_awaited_once_with( binding, artifact_package=package, enabled=True, + execution_mode=( + PluginExecutionMode.SHARED_CERTIFIED + if expected_profile == 'shared-runtime-v1' + else PluginExecutionMode.DEDICATED + ), ) @@ -135,7 +142,10 @@ async def test_marketplace_version_selection_keeps_certificate_gate_and_single_a assert persisted_info['plugin_version'] == '1.0.0' assert persisted_info['_certification']['runtime_profile'] == 'shared-runtime-v1' connector.handler.apply_plugin_installation.assert_awaited_once_with( - binding, artifact_package=package, enabled=True + binding, + artifact_package=package, + enabled=True, + execution_mode=PluginExecutionMode.SHARED_CERTIFIED, ) connector._refresh_runner_registry.assert_awaited_once() assert task_context.metadata['progress_percent'] == 100 diff --git a/tests/unit_tests/plugin/test_certified_plugin_policy.py b/tests/unit_tests/plugin/test_certified_plugin_policy.py index 9e1314c24..344e45918 100644 --- a/tests/unit_tests/plugin/test_certified_plugin_policy.py +++ b/tests/unit_tests/plugin/test_certified_plugin_policy.py @@ -6,6 +6,8 @@ import zipfile import pytest +from langbot_plugin.entities.io.context import PluginExecutionMode + @pytest.mark.parametrize( ('deployment', 'certificate', 'force', 'expected_disposition', 'expected_code'), @@ -150,6 +152,80 @@ def test_log_visibility_policy_only_scopes_valid_shared_certifications( assert visibility.value == expected_visibility +@pytest.mark.parametrize( + ('certification', 'expected_mode'), + [ + ( + { + 'artifact_digest': 'a' * 64, + 'verification': 'valid', + 'certificate_runtime_profile': 'shared-runtime-v1', + 'runtime_profile': 'shared-runtime-v1', + 'admission_code': 'CERTIFIED_PLUGIN_SHARED_ELIGIBLE', + }, + PluginExecutionMode.SHARED_CERTIFIED, + ), + (None, PluginExecutionMode.DEDICATED), + ({}, PluginExecutionMode.DEDICATED), + ( + { + 'artifact_digest': 'a' * 64, + 'verification': 'invalid', + 'certificate_runtime_profile': 'shared-runtime-v1', + 'runtime_profile': 'shared-runtime-v1', + 'admission_code': 'CERTIFIED_PLUGIN_SHARED_ELIGIBLE', + }, + PluginExecutionMode.DEDICATED, + ), + ( + { + 'artifact_digest': 'b' * 64, + 'verification': 'valid', + 'certificate_runtime_profile': 'shared-runtime-v1', + 'runtime_profile': 'shared-runtime-v1', + 'admission_code': 'CERTIFIED_PLUGIN_SHARED_ELIGIBLE', + }, + PluginExecutionMode.DEDICATED, + ), + ( + { + 'artifact_digest': 'a' * 64, + 'verification': 'valid', + 'certificate_runtime_profile': 'shared-runtime-v1', + 'runtime_profile': 'dedicated', + 'admission_code': 'CERTIFIED_PLUGIN_SHARED_ELIGIBLE', + }, + PluginExecutionMode.DEDICATED, + ), + ( + { + 'artifact_digest': 'a' * 64, + 'verification': 'valid', + 'certificate_runtime_profile': 'shared-runtime-v1', + 'runtime_profile': 'shared-runtime-v1', + 'admission_code': 'CERTIFIED_PLUGIN_OSS_FORCED_DEDICATED', + }, + PluginExecutionMode.DEDICATED, + ), + ], +) +def test_persisted_certification_selects_shared_execution_only_for_exact_admitted_artifact( + certification: dict[str, str] | None, + expected_mode: PluginExecutionMode, +) -> None: + from langbot.pkg.plugin.certification import execution_mode_for_persisted_installation + + install_info = {} if certification is None else {'_certification': certification} + + assert ( + execution_mode_for_persisted_installation( + artifact_digest='a' * 64, + install_info=install_info, + ) + is expected_mode + ) + + def _archive_bytes(manifest: dict[str, object]) -> bytes: buffer = io.BytesIO() with zipfile.ZipFile(buffer, 'w') as archive: diff --git a/tests/unit_tests/plugin/test_connector_methods.py b/tests/unit_tests/plugin/test_connector_methods.py index 7ccd0b8a1..f6ed0ec5d 100644 --- a/tests/unit_tests/plugin/test_connector_methods.py +++ b/tests/unit_tests/plugin/test_connector_methods.py @@ -17,7 +17,7 @@ from unittest.mock import AsyncMock, Mock from importlib import import_module from tests.factories import text_query -from langbot_plugin.entities.io.context import InstallationBinding +from langbot_plugin.entities.io.context import InstallationBinding, PluginExecutionMode from langbot.pkg.api.http.context import ExecutionContext from langbot.pkg.workspace.errors import WorkspaceNotFoundError @@ -758,7 +758,16 @@ class TestSetPluginConfig: runtime_revision=1, artifact_digest=TEST_INSTALLATION_BINDING.artifact_digest, enabled=True, - install_info={'_artifact_storage': 'tenant_binary_storage_v1'}, + install_info={ + '_artifact_storage': 'tenant_binary_storage_v1', + '_certification': { + 'artifact_digest': TEST_INSTALLATION_BINDING.artifact_digest, + 'verification': 'valid', + 'certificate_runtime_profile': 'shared-runtime-v1', + 'runtime_profile': 'shared-runtime-v1', + 'admission_code': 'CERTIFIED_PLUGIN_SHARED_ELIGIBLE', + }, + }, ) connector._setting_for_plugin = AsyncMock(return_value=(TEST_EXECUTION_CONTEXT, setting)) connector.ap.persistence_mgr.execute_async = AsyncMock(return_value=SimpleNamespace(rowcount=1)) @@ -777,6 +786,7 @@ class TestSetPluginConfig: applied_binding, artifact_package=None, enabled=True, + execution_mode=PluginExecutionMode.SHARED_CERTIFIED, ) diff --git a/tests/unit_tests/plugin/test_connector_reconcile.py b/tests/unit_tests/plugin/test_connector_reconcile.py index 2a7ea1e1e..e3cf319ce 100644 --- a/tests/unit_tests/plugin/test_connector_reconcile.py +++ b/tests/unit_tests/plugin/test_connector_reconcile.py @@ -8,7 +8,7 @@ from types import SimpleNamespace from unittest.mock import AsyncMock, Mock import pytest -from langbot_plugin.entities.io.context import InstallationBinding +from langbot_plugin.entities.io.context import InstallationBinding, PluginExecutionMode from langbot_plugin.runtime.plugin.mgr import PluginInstallSource from langbot.pkg.api.http.context import ExecutionContext @@ -45,7 +45,17 @@ def mock_archive_admission(connector: PluginRuntimeConnector, digest: str) -> No # is exercised by integration/plugin/test_certified_plugin_admission.py. connector._admit_plugin_archive = Mock( side_effect=lambda _package, info: ( - {**info, '_certification': {'normalized_digest': digest}}, + { + **info, + '_certification': { + 'artifact_digest': digest, + 'normalized_digest': digest, + 'verification': 'valid', + 'certificate_runtime_profile': 'shared-runtime-v1', + 'runtime_profile': 'shared-runtime-v1', + 'admission_code': 'CERTIFIED_PLUGIN_SHARED_ELIGIBLE', + }, + }, SimpleNamespace(for_installation=lambda _uuid: SimpleNamespace(artifact_digest=digest)), ) ) @@ -56,6 +66,7 @@ def plugin_setting( artifact_digest: str, *, durable: bool = True, + certification: dict[str, str] | None = None, ) -> SimpleNamespace: return SimpleNamespace( plugin_author='author', @@ -67,7 +78,10 @@ def plugin_setting( priority=0, created_at=datetime.datetime(2026, 1, 1), install_source='local', - install_info={'_artifact_storage': 'tenant_binary_storage_v1'} if durable else {}, + install_info={ + **({'_artifact_storage': 'tenant_binary_storage_v1'} if durable else {}), + **({'_certification': certification} if certification is not None else {}), + }, ) @@ -163,6 +177,43 @@ async def test_shared_reconnect_replays_two_workspaces_and_removes_missing_proje assert set(connector._known_desired_states) == {setting_a.installation_uuid} +@pytest.mark.asyncio +async def test_reconcile_reload_projects_certified_exact_artifact_to_shared_execution(): + binding = execution_binding('workspace-a') + digest = 'a' * 64 + setting = plugin_setting( + '01', + digest, + certification={ + 'artifact_digest': digest, + 'verification': 'valid', + 'certificate_runtime_profile': 'shared-runtime-v1', + 'runtime_profile': 'shared-runtime-v1', + 'admission_code': 'CERTIFIED_PLUGIN_SHARED_ELIGIBLE', + }, + ) + connector = shared_connector([[binding]], {'workspace-a': [setting]}) + connector.handler = runtime_handler() + + await connector._prepare_connected_runtime() + + desired = connector.handler.reconcile_plugin_installations.await_args.args[0][0] + assert desired.execution_mode is PluginExecutionMode.SHARED_CERTIFIED + + +@pytest.mark.asyncio +async def test_reconcile_reload_defaults_legacy_installation_to_dedicated_execution(): + binding = execution_binding('workspace-a') + setting = plugin_setting('01', 'a' * 64) + connector = shared_connector([[binding]], {'workspace-a': [setting]}) + connector.handler = runtime_handler() + + await connector._prepare_connected_runtime() + + desired = connector.handler.reconcile_plugin_installations.await_args.args[0][0] + assert desired.execution_mode is PluginExecutionMode.DEDICATED + + @pytest.mark.asyncio async def test_empty_projected_workspaces_do_not_retain_installation_sets(): binding_a = execution_binding('workspace-a') @@ -223,6 +274,7 @@ async def test_fresh_shared_runtime_cache_replays_persisted_local_package(): desired.binding, artifact_package=package, enabled=True, + execution_mode=PluginExecutionMode.DEDICATED, ) @@ -304,6 +356,7 @@ async def test_local_install_persists_verified_package_before_runtime_apply(): binding, artifact_package=package, enabled=True, + execution_mode=PluginExecutionMode.DEDICATED, ) @@ -396,6 +449,9 @@ async def test_marketplace_upgrade_reports_multistep_progress(): 'download_current': 0, 'download_speed': 0, } + assert connector.handler.apply_plugin_installation.await_args.kwargs['execution_mode'] is ( + PluginExecutionMode.SHARED_CERTIFIED + ) @pytest.mark.asyncio @@ -430,7 +486,13 @@ async def test_workspace_reads_do_not_wait_for_an_installation_apply(): connector._persist_installation_package = AsyncMock(return_value=(binding, None, False)) connector._wait_for_installed_plugin_ready = AsyncMock() connector._load_workspace_desired_states = AsyncMock( - return_value=[PluginInstallationDesiredState(binding=binding, enabled=True)] + return_value=[ + PluginInstallationDesiredState( + binding=binding, + enabled=True, + execution_mode=PluginExecutionMode.SHARED_CERTIFIED, + ) + ] ) apply_started = asyncio.Event() release_apply = asyncio.Event() diff --git a/tests/unit_tests/plugin/test_handler.py b/tests/unit_tests/plugin/test_handler.py index 860d16989..23be3490b 100644 --- a/tests/unit_tests/plugin/test_handler.py +++ b/tests/unit_tests/plugin/test_handler.py @@ -10,7 +10,12 @@ from unittest.mock import AsyncMock, MagicMock, Mock import pytest from langbot_plugin.entities.io.actions.enums import LangBotToRuntimeAction, PluginToRuntimeAction -from langbot_plugin.entities.io.context import ActionContext, InstallationBinding, PluginInstallationDesiredState +from langbot_plugin.entities.io.context import ( + ActionContext, + InstallationBinding, + PluginExecutionMode, + PluginInstallationDesiredState, +) def make_handler(app): @@ -90,7 +95,25 @@ async def test_reconcile_plugin_installations_accepts_configured_cold_start_time await runtime_handler.reconcile_plugin_installations((desired,), timeout=900) - assert runtime_handler.call_action.await_args.kwargs["timeout"] == 900 + assert runtime_handler.call_action.await_args.kwargs['timeout'] == 900 + + +@pytest.mark.asyncio +async def test_apply_plugin_installation_serializes_certified_shared_execution_mode(): + runtime_handler = make_handler(SimpleNamespace()) + runtime_handler.send_file = AsyncMock(return_value='artifact-file') + runtime_handler.call_action = AsyncMock(return_value={'state': 'starting'}) + binding = next(iter(runtime_handler._installation_bindings.values()))[0] + + await runtime_handler.apply_plugin_installation( + binding, + artifact_package=b'package', + enabled=True, + execution_mode=PluginExecutionMode.SHARED_CERTIFIED, + ) + + assert runtime_handler.call_action.await_args.args[0] == LangBotToRuntimeAction.APPLY_PLUGIN_INSTALLATION + assert runtime_handler.call_action.await_args.args[1]['execution_mode'] == 'shared-runtime-v1' class TestHandlerQueryVariables: diff --git a/uv.lock b/uv.lock index 1c261c4ef..03ddf5086 100644 --- a/uv.lock +++ b/uv.lock @@ -1066,7 +1066,7 @@ name = "cuda-bindings" version = "13.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "cuda-pathfinder" }, + { name = "cuda-pathfinder", marker = "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/51/6b/457ca12dad3ee9bfcc9a545cfd6b64b359ba49de40f776f6e028e678f262/cuda_bindings-13.3.1-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c5879712accf6e14bb01aa5e67440eb84998b8d104b509cc7a6dc0b8f656a474", size = 6053539, upload-time = "2026-05-29T23:11:43.19Z" }, @@ -1099,34 +1099,34 @@ wheels = [ [package.optional-dependencies] cudart = [ - { name = "nvidia-cuda-runtime" }, + { name = "nvidia-cuda-runtime", marker = "python_full_version >= '3.14' and sys_platform == 'linux'" }, ] cufft = [ - { name = "nvidia-cufft" }, + { name = "nvidia-cufft", marker = "python_full_version >= '3.14' and sys_platform == 'linux'" }, ] cufile = [ - { name = "nvidia-cufile" }, + { name = "nvidia-cufile", marker = "python_full_version >= '3.14' and sys_platform == 'linux'" }, ] cupti = [ - { name = "nvidia-cuda-cupti" }, + { name = "nvidia-cuda-cupti", marker = "python_full_version >= '3.14' and sys_platform == 'linux'" }, ] curand = [ - { name = "nvidia-curand" }, + { name = "nvidia-curand", marker = "python_full_version >= '3.14' and sys_platform == 'linux'" }, ] cusolver = [ - { name = "nvidia-cusolver" }, + { name = "nvidia-cusolver", marker = "python_full_version >= '3.14' and sys_platform == 'linux'" }, ] cusparse = [ - { name = "nvidia-cusparse" }, + { name = "nvidia-cusparse", marker = "python_full_version >= '3.14' and sys_platform == 'linux'" }, ] nvjitlink = [ - { name = "nvidia-nvjitlink" }, + { name = "nvidia-nvjitlink", marker = "python_full_version >= '3.14' and sys_platform == 'linux'" }, ] nvrtc = [ - { name = "nvidia-cuda-nvrtc" }, + { name = "nvidia-cuda-nvrtc", marker = "python_full_version >= '3.14' and sys_platform == 'linux'" }, ] nvtx = [ - { name = "nvidia-nvtx" }, + { name = "nvidia-nvtx", marker = "python_full_version >= '3.14' and sys_platform == 'linux'" }, ] [[package]] @@ -2185,7 +2185,7 @@ requires-dist = [ { name = "gewechat-client", specifier = ">=0.1.5" }, { name = "html2text", specifier = ">=2024.2.26" }, { name = "httpx", extras = ["socks"], specifier = ">=0.28.1" }, - { name = "langbot-plugin", specifier = "==0.6.1" }, + { name = "langbot-plugin", specifier = "==0.6.2" }, { name = "langchain", specifier = ">=1.3.9" }, { name = "langchain-core", specifier = ">=1.3.3" }, { name = "langchain-text-splitters", specifier = ">=1.1.2" }, @@ -2255,7 +2255,7 @@ dev = [ [[package]] name = "langbot-plugin" -version = "0.6.1" +version = "0.6.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiofiles" }, @@ -2276,9 +2276,9 @@ dependencies = [ { name = "watchdog" }, { name = "websockets" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/1f/70/ade1e2e71e666a80acfffde0fc753173e74b7a528ef495a1705f53c40ea8/langbot_plugin-0.6.1.tar.gz", hash = "sha256:214e415c2cd3c286f4b07cfbbe11e6b865839596ad500c2100944b00b3ba5f2b", size = 625357, upload-time = "2026-09-24T14:55:08.202Z" } +sdist = { url = "https://files.pythonhosted.org/packages/48/df/376749b79b133a3f9e65975243c1b4a734f072cbdbb2a3f5d5add7dc800c/langbot_plugin-0.6.2.tar.gz", hash = "sha256:64dc0f47b625c3595028dadea3e500ee123cb0e5481c97fecd7f4308d703623c", size = 639489, upload-time = "2026-09-25T12:16:11.18Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/42/06/4d8a889b0c28dcbf7ccf5ca40bcc7125ac9639942f7d05340768b28d159c/langbot_plugin-0.6.1-py3-none-any.whl", hash = "sha256:5607b4c787a92259b4a1f7beb4bdca9f371e37b316eaf8f316d2448e888da77e", size = 413828, upload-time = "2026-09-24T14:55:06.517Z" }, + { url = "https://files.pythonhosted.org/packages/d3/6e/d7f725ff4c42e7092368d2651354a84d5f195baf4dfbd5c537979b6d50aa/langbot_plugin-0.6.2-py3-none-any.whl", hash = "sha256:65845f20e65e32b4daeffdf69119b35be8775ce902d9e050e38f2cf5d77e5203", size = 419610, upload-time = "2026-09-25T12:16:09.973Z" }, ] [[package]] @@ -3306,7 +3306,7 @@ name = "nvidia-cublas" version = "13.1.1.3" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "nvidia-cuda-nvrtc" }, + { name = "nvidia-cuda-nvrtc", marker = "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/a7/a1/0bd24ee8c8d03adac032fd2909426a00c88f8c57961b1277ded97f91119f/nvidia_cublas-13.1.1.3-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:b7a210458267ac818974c53038fbec2e969d5c99f305ab15c72522fa9f001dd5", size = 542848918, upload-time = "2026-04-08T18:46:22.985Z" }, @@ -3345,7 +3345,7 @@ name = "nvidia-cudnn-cu13" version = "9.20.0.48" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "nvidia-cublas" }, + { name = "nvidia-cublas", marker = "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/56/c5/83384d846b2fd17c44bd499b36c75a45ed4f095fbbb2252294e89cea5c5c/nvidia_cudnn_cu13-9.20.0.48-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:e31454ae00094b0c55319d9d15b6fa2fc50a9e1c0f5c8c80fb75258234e731e1", size = 444574296, upload-time = "2026-03-09T19:28:27.751Z" }, @@ -3357,7 +3357,7 @@ name = "nvidia-cufft" version = "12.0.0.61" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "nvidia-nvjitlink" }, + { name = "nvidia-nvjitlink", marker = "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/8b/ae/f417a75c0259e85c1d2f83ca4e960289a5f814ed0cea74d18c353d3e989d/nvidia_cufft-12.0.0.61-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2708c852ef8cd89d1d2068bdbece0aa188813a0c934db3779b9b1faa8442e5f5", size = 214053554, upload-time = "2025-09-04T08:31:38.196Z" }, @@ -3387,9 +3387,9 @@ name = "nvidia-cusolver" version = "12.0.4.66" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "nvidia-cublas" }, - { name = "nvidia-cusparse" }, - { name = "nvidia-nvjitlink" }, + { name = "nvidia-cublas", marker = "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'" }, + { name = "nvidia-cusparse", marker = "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'" }, + { name = "nvidia-nvjitlink", marker = "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/c8/c3/b30c9e935fc01e3da443ec0116ed1b2a009bb867f5324d3f2d7e533e776b/nvidia_cusolver-12.0.4.66-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:02c2457eaa9e39de20f880f4bd8820e6a1cfb9f9a34f820eb12a155aa5bc92d2", size = 223467760, upload-time = "2025-09-04T08:33:04.222Z" }, @@ -3401,7 +3401,7 @@ name = "nvidia-cusparse" version = "12.6.3.3" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "nvidia-nvjitlink" }, + { name = "nvidia-nvjitlink", marker = "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/f8/94/5c26f33738ae35276672f12615a64bd008ed5be6d1ebcb23579285d960a9/nvidia_cusparse-12.6.3.3-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:80bcc4662f23f1054ee334a15c72b8940402975e0eab63178fc7e670aa59472c", size = 162155568, upload-time = "2025-09-04T08:33:42.864Z" }, @@ -4494,7 +4494,7 @@ name = "pylibseekdb" version = "1.4.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pymysql" }, + { name = "pymysql", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/ae/a8/7413d33218aff55a14ec9d20532b49243ffd0579e7a92244922c1885444e/pylibseekdb-1.4.0-cp311-cp311-macosx_15_0_arm64.whl", hash = "sha256:5cb2efab9f1321cdb4b034d3a2bd92e41a402fc95e7dc9579c7473a426f96e24", size = 52173499, upload-time = "2026-08-27T13:05:09.347Z" }, @@ -5262,10 +5262,10 @@ name = "scikit-learn" version = "1.8.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "joblib" }, - { name = "numpy" }, - { name = "scipy" }, - { name = "threadpoolctl" }, + { name = "joblib", marker = "python_full_version >= '3.14'" }, + { name = "numpy", marker = "python_full_version >= '3.14'" }, + { name = "scipy", marker = "python_full_version >= '3.14'" }, + { name = "threadpoolctl", marker = "python_full_version >= '3.14'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/0e/d4/40988bf3b8e34feec1d0e6a051446b1f66225f8529b9309becaeef62b6c4/scikit_learn-1.8.0.tar.gz", hash = "sha256:9bccbb3b40e3de10351f8f5068e105d0f4083b1a65fa07b6634fbc401a6287fd", size = 7335585, upload-time = "2025-12-10T07:08:53.618Z" } wheels = [ @@ -5312,7 +5312,7 @@ name = "scipy" version = "1.17.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "numpy" }, + { name = "numpy", marker = "python_full_version >= '3.14'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/7a/97/5a3609c4f8d58b039179648e62dd220f89864f56f7357f5d4f45c29eb2cc/scipy-1.17.1.tar.gz", hash = "sha256:95d8e012d8cb8816c226aef832200b1d45109ed4464303e997c5b13122b297c0", size = 30573822, upload-time = "2026-02-23T00:26:24.851Z" } wheels = [ @@ -5383,14 +5383,14 @@ name = "sentence-transformers" version = "5.2.3" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "huggingface-hub" }, - { name = "numpy" }, - { name = "scikit-learn" }, - { name = "scipy" }, - { name = "torch" }, - { name = "tqdm" }, - { name = "transformers" }, - { name = "typing-extensions" }, + { name = "huggingface-hub", marker = "python_full_version >= '3.14'" }, + { name = "numpy", marker = "python_full_version >= '3.14'" }, + { name = "scikit-learn", marker = "python_full_version >= '3.14'" }, + { name = "scipy", marker = "python_full_version >= '3.14'" }, + { name = "torch", marker = "python_full_version >= '3.14'" }, + { name = "tqdm", marker = "python_full_version >= '3.14'" }, + { name = "transformers", marker = "python_full_version >= '3.14'" }, + { name = "typing-extensions", marker = "python_full_version >= '3.14'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/5b/30/21664028fc0776eb1ca024879480bbbab36f02923a8ff9e4cae5a150fa35/sentence_transformers-5.2.3.tar.gz", hash = "sha256:3cd3044e1f3fe859b6a1b66336aac502eaae5d3dd7d5c8fc237f37fbf58137c7", size = 381623, upload-time = "2026-02-17T14:05:20.238Z" } wheels = [ @@ -5772,21 +5772,21 @@ name = "torch" version = "2.12.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "cuda-bindings", marker = "sys_platform == 'linux'" }, - { name = "cuda-toolkit", extra = ["cudart", "cufft", "cufile", "cupti", "curand", "cusolver", "cusparse", "nvjitlink", "nvrtc", "nvtx"], marker = "sys_platform == 'linux'" }, - { name = "filelock" }, - { name = "fsspec" }, - { name = "jinja2" }, - { name = "networkx" }, - { name = "nvidia-cublas", marker = "sys_platform == 'linux'" }, - { name = "nvidia-cudnn-cu13", marker = "sys_platform == 'linux'" }, - { name = "nvidia-cusparselt-cu13", marker = "sys_platform == 'linux'" }, - { name = "nvidia-nccl-cu13", marker = "sys_platform == 'linux'" }, - { name = "nvidia-nvshmem-cu13", marker = "sys_platform == 'linux'" }, - { name = "setuptools" }, - { name = "sympy" }, - { name = "triton", marker = "sys_platform == 'linux'" }, - { name = "typing-extensions" }, + { name = "cuda-bindings", marker = "python_full_version >= '3.14' and sys_platform == 'linux'" }, + { name = "cuda-toolkit", extra = ["cudart", "cufft", "cufile", "cupti", "curand", "cusolver", "cusparse", "nvjitlink", "nvrtc", "nvtx"], marker = "python_full_version >= '3.14' and sys_platform == 'linux'" }, + { name = "filelock", marker = "python_full_version >= '3.14'" }, + { name = "fsspec", marker = "python_full_version >= '3.14'" }, + { name = "jinja2", marker = "python_full_version >= '3.14'" }, + { name = "networkx", marker = "python_full_version >= '3.14'" }, + { name = "nvidia-cublas", marker = "python_full_version >= '3.14' and sys_platform == 'linux'" }, + { name = "nvidia-cudnn-cu13", marker = "python_full_version >= '3.14' and sys_platform == 'linux'" }, + { name = "nvidia-cusparselt-cu13", marker = "python_full_version >= '3.14' and sys_platform == 'linux'" }, + { name = "nvidia-nccl-cu13", marker = "python_full_version >= '3.14' and sys_platform == 'linux'" }, + { name = "nvidia-nvshmem-cu13", marker = "python_full_version >= '3.14' and sys_platform == 'linux'" }, + { name = "setuptools", marker = "python_full_version >= '3.14'" }, + { name = "sympy", marker = "python_full_version >= '3.14'" }, + { name = "triton", marker = "python_full_version >= '3.14' and sys_platform == 'linux'" }, + { name = "typing-extensions", marker = "python_full_version >= '3.14'" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/59/38/7028d3be540f1dcdf41660a2b01d0c51d2cb73915fe370d84e4d277a6d47/torch-2.12.1-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:ef81f503912effea2ce3d9b12a2e3a6ed488943e91271c90c7a829f60baf6aa2", size = 87975425, upload-time = "2026-06-17T21:08:34.094Z" }, @@ -5828,15 +5828,15 @@ name = "transformers" version = "5.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "huggingface-hub" }, - { name = "numpy" }, - { name = "packaging" }, - { name = "pyyaml" }, - { name = "regex" }, - { name = "safetensors" }, - { name = "tokenizers" }, - { name = "tqdm" }, - { name = "typer" }, + { name = "huggingface-hub", marker = "python_full_version >= '3.14'" }, + { name = "numpy", marker = "python_full_version >= '3.14'" }, + { name = "packaging", marker = "python_full_version >= '3.14'" }, + { name = "pyyaml", marker = "python_full_version >= '3.14'" }, + { name = "regex", marker = "python_full_version >= '3.14'" }, + { name = "safetensors", marker = "python_full_version >= '3.14'" }, + { name = "tokenizers", marker = "python_full_version >= '3.14'" }, + { name = "tqdm", marker = "python_full_version >= '3.14'" }, + { name = "typer", marker = "python_full_version >= '3.14'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/fc/1a/70e830d53ecc96ce69cfa8de38f163712d2b43ac52fbd743f39f56025c31/transformers-5.3.0.tar.gz", hash = "sha256:009555b364029da9e2946d41f1c5de9f15e6b1df46b189b7293f33a161b9c557", size = 8830831, upload-time = "2026-03-04T17:41:46.119Z" } wheels = [ @@ -6097,9 +6097,9 @@ name = "valkey-glide" version = "2.4.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "anyio" }, - { name = "protobuf" }, - { name = "sniffio" }, + { name = "anyio", marker = "sys_platform != 'win32'" }, + { name = "protobuf", marker = "sys_platform != 'win32'" }, + { name = "sniffio", marker = "sys_platform != 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/72/a2/582b34c6acc8dc857c537f6007459cba48dfa0dc404789a657e5c1a998c0/valkey_glide-2.4.1.tar.gz", hash = "sha256:f1155d84156d11b90488aa67e90102f0bf98a45314f5b99308ac9074c05f7241", size = 898030, upload-time = "2026-05-28T21:41:55.881Z" } wheels = [