From 9f296bd57e0cd9778bb12de3ebd42240d53bcda1 Mon Sep 17 00:00:00 2001 From: RockChinQ Date: Fri, 25 Sep 2026 12:48:55 +0000 Subject: [PATCH 01/12] 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 = [ From 300f5825bbf7b78f2978fb71cc0bf6044e0a932c Mon Sep 17 00:00:00 2001 From: RockChinQ <45992437+RockChinQ@users.noreply.github.com> Date: Fri, 25 Sep 2026 13:13:35 +0000 Subject: [PATCH 02/12] fix(plugin): fail closed on incomplete certification records --- src/langbot/pkg/plugin/certification.py | 10 ++ .../plugin/test_certified_plugin_admission.py | 2 + .../plugin/test_certified_plugin_policy.py | 136 ++++++++++++++---- .../plugin/test_connector_methods.py | 2 + .../plugin/test_connector_reconcile.py | 3 + 5 files changed, 126 insertions(+), 27 deletions(-) diff --git a/src/langbot/pkg/plugin/certification.py b/src/langbot/pkg/plugin/certification.py index 65488d98b..958f37c50 100644 --- a/src/langbot/pkg/plugin/certification.py +++ b/src/langbot/pkg/plugin/certification.py @@ -242,11 +242,21 @@ def execution_mode_for_persisted_installation( ) -> PluginExecutionMode: """Derive placement only from persisted facts bound to the exact artifact.""" + if len(artifact_digest) != 64 or any(character not in '0123456789abcdef' for character in artifact_digest.lower()): + return PluginExecutionMode.DEDICATED if not isinstance(install_info, Mapping): return PluginExecutionMode.DEDICATED certification = install_info.get('_certification') if not isinstance(certification, Mapping): return PluginExecutionMode.DEDICATED + normalized_digest = certification.get('normalized_digest') + if not isinstance(normalized_digest, str) or len(normalized_digest) != 64: + return PluginExecutionMode.DEDICATED + if any(character not in '0123456789abcdef' for character in normalized_digest.lower()): + return PluginExecutionMode.DEDICATED + certificate_id = certification.get('certificate_id') + if not isinstance(certificate_id, str) or not certificate_id.strip(): + return PluginExecutionMode.DEDICATED shared_facts = { 'artifact_digest': artifact_digest, 'verification': CertificateVerification.VALID.value, diff --git a/tests/integration/plugin/test_certified_plugin_admission.py b/tests/integration/plugin/test_certified_plugin_admission.py index 6106c9248..13b8019de 100644 --- a/tests/integration/plugin/test_certified_plugin_admission.py +++ b/tests/integration/plugin/test_certified_plugin_admission.py @@ -60,6 +60,8 @@ async def test_install_plugin_admits_archive_before_persistence_and_applies_sele 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) + if archive_kind == 'signed_shared': + assert persisted_info['_certification']['certificate_id'] == 'ephemeral' connector.handler.apply_plugin_installation.assert_awaited_once_with( binding, artifact_package=package, diff --git a/tests/unit_tests/plugin/test_certified_plugin_policy.py b/tests/unit_tests/plugin/test_certified_plugin_policy.py index 344e45918..9d05cb52a 100644 --- a/tests/unit_tests/plugin/test_certified_plugin_policy.py +++ b/tests/unit_tests/plugin/test_certified_plugin_policy.py @@ -152,57 +152,48 @@ def test_log_visibility_policy_only_scopes_valid_shared_certifications( assert visibility.value == expected_visibility +def _complete_persisted_certification() -> dict[str, object]: + return { + 'artifact_digest': 'a' * 64, + 'normalized_digest': 'b' * 64, + 'verification': 'valid', + 'certificate_runtime_profile': 'shared-runtime-v1', + 'certificate_id': 'ed25519:trusted-issuer', + 'runtime_profile': 'shared-runtime-v1', + 'admission_code': 'CERTIFIED_PLUGIN_SHARED_ELIGIBLE', + } + + @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, - ), + (_complete_persisted_certification(), PluginExecutionMode.SHARED_CERTIFIED), (None, PluginExecutionMode.DEDICATED), ({}, PluginExecutionMode.DEDICATED), ( { - 'artifact_digest': 'a' * 64, + **_complete_persisted_certification(), 'verification': 'invalid', - 'certificate_runtime_profile': 'shared-runtime-v1', - 'runtime_profile': 'shared-runtime-v1', - 'admission_code': 'CERTIFIED_PLUGIN_SHARED_ELIGIBLE', }, PluginExecutionMode.DEDICATED, ), ( { + **_complete_persisted_certification(), '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', + **_complete_persisted_certification(), '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', + **_complete_persisted_certification(), 'admission_code': 'CERTIFIED_PLUGIN_OSS_FORCED_DEDICATED', }, PluginExecutionMode.DEDICATED, @@ -210,7 +201,7 @@ def test_log_visibility_policy_only_scopes_valid_shared_certifications( ], ) def test_persisted_certification_selects_shared_execution_only_for_exact_admitted_artifact( - certification: dict[str, str] | None, + certification: dict[str, object] | None, expected_mode: PluginExecutionMode, ) -> None: from langbot.pkg.plugin.certification import execution_mode_for_persisted_installation @@ -226,6 +217,97 @@ def test_persisted_certification_selects_shared_execution_only_for_exact_admitte ) +@pytest.mark.parametrize( + 'missing_field', + [ + 'artifact_digest', + 'normalized_digest', + 'verification', + 'certificate_runtime_profile', + 'certificate_id', + 'runtime_profile', + 'admission_code', + ], +) +def test_persisted_certification_requires_every_shared_admission_fact(missing_field: str) -> None: + from langbot.pkg.plugin.certification import execution_mode_for_persisted_installation + + certification = _complete_persisted_certification() + del certification[missing_field] + + assert ( + execution_mode_for_persisted_installation( + artifact_digest='a' * 64, + install_info={'_certification': certification}, + ) + is PluginExecutionMode.DEDICATED + ) + + +@pytest.mark.parametrize( + ('field', 'malformed_value'), + [ + ('artifact_digest', None), + ('artifact_digest', 123), + ('artifact_digest', ''), + ('normalized_digest', None), + ('normalized_digest', 123), + ('normalized_digest', ''), + ('normalized_digest', ' ' * 64), + ('normalized_digest', 'b' * 63), + ('normalized_digest', 'g' * 64), + ('certificate_id', None), + ('certificate_id', 123), + ('certificate_id', ''), + ('certificate_id', ' '), + ('verification', None), + ('verification', 123), + ('verification', ''), + ('certificate_runtime_profile', None), + ('certificate_runtime_profile', 123), + ('certificate_runtime_profile', ''), + ('runtime_profile', None), + ('runtime_profile', 123), + ('runtime_profile', ''), + ('admission_code', None), + ('admission_code', 123), + ('admission_code', ''), + ], +) +def test_persisted_certification_rejects_malformed_shared_admission_fact( + field: str, + malformed_value: object, +) -> None: + from langbot.pkg.plugin.certification import execution_mode_for_persisted_installation + + certification = _complete_persisted_certification() + certification[field] = malformed_value + + assert ( + execution_mode_for_persisted_installation( + artifact_digest='a' * 64, + install_info={'_certification': certification}, + ) + is PluginExecutionMode.DEDICATED + ) + + +@pytest.mark.parametrize('malformed_digest', ['', 'a' * 63, 'g' * 64]) +def test_persisted_certification_rejects_malformed_matching_raw_digest(malformed_digest: str) -> None: + from langbot.pkg.plugin.certification import execution_mode_for_persisted_installation + + certification = _complete_persisted_certification() + certification['artifact_digest'] = malformed_digest + + assert ( + execution_mode_for_persisted_installation( + artifact_digest=malformed_digest, + install_info={'_certification': certification}, + ) + is PluginExecutionMode.DEDICATED + ) + + 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 f6ed0ec5d..716b0f1bf 100644 --- a/tests/unit_tests/plugin/test_connector_methods.py +++ b/tests/unit_tests/plugin/test_connector_methods.py @@ -762,8 +762,10 @@ class TestSetPluginConfig: '_artifact_storage': 'tenant_binary_storage_v1', '_certification': { 'artifact_digest': TEST_INSTALLATION_BINDING.artifact_digest, + 'normalized_digest': 'b' * 64, 'verification': 'valid', 'certificate_runtime_profile': 'shared-runtime-v1', + 'certificate_id': 'ed25519:trusted-issuer', 'runtime_profile': 'shared-runtime-v1', 'admission_code': 'CERTIFIED_PLUGIN_SHARED_ELIGIBLE', }, diff --git a/tests/unit_tests/plugin/test_connector_reconcile.py b/tests/unit_tests/plugin/test_connector_reconcile.py index e3cf319ce..fde178fa2 100644 --- a/tests/unit_tests/plugin/test_connector_reconcile.py +++ b/tests/unit_tests/plugin/test_connector_reconcile.py @@ -52,6 +52,7 @@ def mock_archive_admission(connector: PluginRuntimeConnector, digest: str) -> No 'normalized_digest': digest, 'verification': 'valid', 'certificate_runtime_profile': 'shared-runtime-v1', + 'certificate_id': 'ed25519:trusted-issuer', 'runtime_profile': 'shared-runtime-v1', 'admission_code': 'CERTIFIED_PLUGIN_SHARED_ELIGIBLE', }, @@ -186,8 +187,10 @@ async def test_reconcile_reload_projects_certified_exact_artifact_to_shared_exec digest, certification={ 'artifact_digest': digest, + 'normalized_digest': 'b' * 64, 'verification': 'valid', 'certificate_runtime_profile': 'shared-runtime-v1', + 'certificate_id': 'ed25519:trusted-issuer', 'runtime_profile': 'shared-runtime-v1', 'admission_code': 'CERTIFIED_PLUGIN_SHARED_ELIGIBLE', }, From deca26a666da3b77331d0bbafc8f01c8ffb601ba Mon Sep 17 00:00:00 2001 From: RockChinQ Date: Fri, 25 Sep 2026 19:09:16 +0000 Subject: [PATCH 03/12] chore(plugin): pin SDK 0.6.3 --- pyproject.toml | 2 +- .../integration/plugin/test_certified_plugin_admission.py | 1 + uv.lock | 8 ++++---- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 973d55160..7304d10e7 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.2", + "langbot-plugin==0.6.3", "asyncpg>=0.30.0", "line-bot-sdk>=3.19.0", "matrix-nio>=0.25.2", diff --git a/tests/integration/plugin/test_certified_plugin_admission.py b/tests/integration/plugin/test_certified_plugin_admission.py index 8e4c21498..0a5126ad9 100644 --- a/tests/integration/plugin/test_certified_plugin_admission.py +++ b/tests/integration/plugin/test_certified_plugin_admission.py @@ -133,6 +133,7 @@ async def test_oss_admits_unresolvable_declaration_on_the_dedicated_profile() -> binding, artifact_package=package, enabled=True, + execution_mode=PluginExecutionMode.DEDICATED, ) diff --git a/uv.lock b/uv.lock index 03ddf5086..19c8e30ab 100644 --- a/uv.lock +++ b/uv.lock @@ -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.2" }, + { name = "langbot-plugin", specifier = "==0.6.3" }, { 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.2" +version = "0.6.3" 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/48/df/376749b79b133a3f9e65975243c1b4a734f072cbdbb2a3f5d5add7dc800c/langbot_plugin-0.6.2.tar.gz", hash = "sha256:64dc0f47b625c3595028dadea3e500ee123cb0e5481c97fecd7f4308d703623c", size = 639489, upload-time = "2026-09-25T12:16:11.18Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ba/01/c45f438206802c15ada87277fb1a219c4f88b8149a899ba0a0d8b47b28f8/langbot_plugin-0.6.3.tar.gz", hash = "sha256:73ff8e2d3efa88c0a6ccc7166e270b9c6f9b3fe9bb598932614be54c6ff8d920", size = 653192, upload-time = "2026-09-25T19:06:20.892Z" } wheels = [ - { 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" }, + { url = "https://files.pythonhosted.org/packages/05/77/1aa9f9374690ffb5d1d5fa266afd10485c92d7c5f0bf22e37bed82a639ea/langbot_plugin-0.6.3-py3-none-any.whl", hash = "sha256:8afbe883fbf2e61a8d927ffaedabc66a7fab9dc9135b1657662226319cb5b34e", size = 426068, upload-time = "2026-09-25T19:06:19.597Z" }, ] [[package]] From a5e5ee5fb81245551eb0a94c86cb2f74c5c2eecb Mon Sep 17 00:00:00 2001 From: RockChinQ Date: Fri, 25 Sep 2026 21:03:47 +0000 Subject: [PATCH 04/12] fix(plugin): backfill certified artifact digest --- ..._certification_artifact_digest_backfill.py | 138 +++++++++++++++++ .../test_migration_branch_convergence.py | 139 ++++++++++++++++++ .../persistence/test_migrations_postgres.py | 70 +++++++++ 3 files changed, 347 insertions(+) create mode 100644 src/langbot/pkg/persistence/alembic/versions/0032_certification_artifact_digest_backfill.py diff --git a/src/langbot/pkg/persistence/alembic/versions/0032_certification_artifact_digest_backfill.py b/src/langbot/pkg/persistence/alembic/versions/0032_certification_artifact_digest_backfill.py new file mode 100644 index 000000000..1dc4e613f --- /dev/null +++ b/src/langbot/pkg/persistence/alembic/versions/0032_certification_artifact_digest_backfill.py @@ -0,0 +1,138 @@ +"""Backfill raw artifact digests for legacy shared certificates. + +Revision ID: 0032_cert_artifact_digest +Revises: 0031_merge_totp_assistant + +Older certified-plugin rows persisted every shared-admission fact except the raw +archive digest. Placement now requires that digest to match the installation's +immutable artifact digest, so this migration fills only records whose remaining +facts already prove the exact legacy shared-admission shape. +""" + +from __future__ import annotations + +from collections.abc import Mapping + +import sqlalchemy as sa +from alembic import op + +revision = '0032_cert_artifact_digest' +down_revision = '0031_merge_totp_assistant' +branch_labels = None +depends_on = None + +_TABLE = 'plugin_settings' +_SHARED_RUNTIME = 'shared-runtime-v1' +_SHARED_ADMISSION_CODE = 'CERTIFIED_PLUGIN_SHARED_ELIGIBLE' +_LOWER_HEX = frozenset('0123456789abcdef') +_HEX = frozenset('0123456789abcdefABCDEF') + + +def _is_digest(value: object, *, lowercase: bool) -> bool: + allowed = _LOWER_HEX if lowercase else _HEX + return isinstance(value, str) and len(value) == 64 and all(character in allowed for character in value) + + +def _eligible_certification(install_info: object, artifact_digest: object) -> Mapping[object, object] | None: + if not _is_digest(artifact_digest, lowercase=True) or not isinstance(install_info, Mapping): + return None + certification = install_info.get('_certification') + if not isinstance(certification, Mapping) or 'artifact_digest' in certification: + return None + certificate_id = certification.get('certificate_id') + if not isinstance(certificate_id, str) or not certificate_id.strip(): + return None + if not _is_digest(certification.get('normalized_digest'), lowercase=False): + return None + required_facts = { + 'verification': 'valid', + 'certificate_runtime_profile': _SHARED_RUNTIME, + 'runtime_profile': _SHARED_RUNTIME, + 'admission_code': _SHARED_ADMISSION_CODE, + } + if not all(certification.get(key) == value for key, value in required_facts.items()): + return None + return certification + + +def _suspend_postgres_rls(conn: sa.Connection) -> tuple[bool, bool]: + if conn.dialect.name != 'postgresql': + return False, False + state = conn.execute( + sa.text('SELECT relrowsecurity, relforcerowsecurity FROM pg_class WHERE oid = to_regclass(:table_name)'), + {'table_name': _TABLE}, + ).one() + rls_enabled, rls_forced = bool(state.relrowsecurity), bool(state.relforcerowsecurity) + if rls_forced: + conn.execute(sa.text(f'ALTER TABLE {_TABLE} NO FORCE ROW LEVEL SECURITY')) + if rls_enabled: + conn.execute(sa.text(f'ALTER TABLE {_TABLE} DISABLE ROW LEVEL SECURITY')) + return rls_enabled, rls_forced + + +def _restore_postgres_rls(conn: sa.Connection, state: tuple[bool, bool]) -> None: + if conn.dialect.name != 'postgresql': + return + rls_enabled, rls_forced = state + if rls_enabled: + conn.execute(sa.text(f'ALTER TABLE {_TABLE} ENABLE ROW LEVEL SECURITY')) + if rls_forced: + conn.execute(sa.text(f'ALTER TABLE {_TABLE} FORCE ROW LEVEL SECURITY')) + + +def backfill_certification_artifact_digests(conn: sa.Connection) -> None: + inspector = sa.inspect(conn) + if _TABLE not in inspector.get_table_names(): + return + columns = {column['name'] for column in inspector.get_columns(_TABLE)} + required_columns = { + 'workspace_uuid', + 'plugin_author', + 'plugin_name', + 'artifact_digest', + 'install_info', + } + if not required_columns <= columns: + return + + plugin_settings = sa.table( + _TABLE, + sa.column('workspace_uuid', sa.String(36)), + sa.column('plugin_author', sa.String(255)), + sa.column('plugin_name', sa.String(255)), + sa.column('artifact_digest', sa.String(64)), + sa.column('install_info', sa.JSON()), + ) + rows = conn.execute(sa.select(plugin_settings)).mappings().all() + for row in rows: + certification = _eligible_certification(row['install_info'], row['artifact_digest']) + if certification is None: + continue + updated_certification = dict(certification) + updated_certification['artifact_digest'] = row['artifact_digest'] + updated_install_info = dict(row['install_info']) + updated_install_info['_certification'] = updated_certification + conn.execute( + plugin_settings.update() + .where(plugin_settings.c.workspace_uuid == row['workspace_uuid']) + .where(plugin_settings.c.plugin_author == row['plugin_author']) + .where(plugin_settings.c.plugin_name == row['plugin_name']) + .values(install_info=updated_install_info) + ) + + +def upgrade() -> None: + conn = op.get_bind() + if _TABLE not in sa.inspect(conn).get_table_names(): + return + rls_state = _suspend_postgres_rls(conn) + try: + backfill_certification_artifact_digests(conn) + finally: + _restore_postgres_rls(conn, rls_state) + + +def downgrade() -> None: + # The source digest cannot be distinguished from a digest persisted by + # current Core, so downgrade intentionally preserves the safe enrichment. + pass diff --git a/tests/integration/persistence/test_migration_branch_convergence.py b/tests/integration/persistence/test_migration_branch_convergence.py index 5b03caa29..759b46b03 100644 --- a/tests/integration/persistence/test_migration_branch_convergence.py +++ b/tests/integration/persistence/test_migration_branch_convergence.py @@ -16,6 +16,7 @@ import uuid import pytest import sqlalchemy as sa from sqlalchemy.ext.asyncio import create_async_engine +from langbot_plugin.entities.io.context import PluginExecutionMode from langbot.pkg.entity import persistence from langbot.pkg.entity.persistence.base import Base @@ -25,6 +26,7 @@ from langbot.pkg.persistence.alembic_runner import ( run_alembic_downgrade, run_alembic_upgrade, ) +from langbot.pkg.plugin.certification import execution_mode_for_persisted_installation from langbot.pkg.utils import importutil @@ -353,6 +355,143 @@ async def test_empty_database_startup_schema_then_real_migrations(convergence_en assert await get_alembic_current(engine) == get_alembic_head() +def _legacy_shared_certification(**overrides): + certification = { + 'normalized_digest': 'B' * 64, + 'verification': 'valid', + 'certificate_runtime_profile': 'shared-runtime-v1', + 'certificate_id': 'ed25519:trusted-issuer', + 'runtime_profile': 'shared-runtime-v1', + 'admission_code': 'CERTIFIED_PLUGIN_SHARED_ELIGIBLE', + 'preserved_certificate_key': {'nested': True}, + } + certification.update(overrides) + return certification + + +@pytest.mark.asyncio +async def test_certification_artifact_digest_backfill_is_safe_and_enables_shared_placement(convergence_engine): + engine = convergence_engine + async with engine.begin() as conn: + await conn.run_sync(Base.metadata.create_all) + await run_alembic_upgrade(engine, '0031_merge_totp_assistant') + + valid_digest = 'a' * 64 + rows = { + 'eligible-a': (_legacy_shared_certification(), valid_digest), + 'eligible-b': (_legacy_shared_certification(), valid_digest), + 'present-matching': (_legacy_shared_certification(artifact_digest=valid_digest), valid_digest), + 'present-mismatched': (_legacy_shared_certification(artifact_digest='c' * 64), valid_digest), + 'invalid': (_legacy_shared_certification(verification='invalid'), valid_digest), + 'incomplete': (_legacy_shared_certification(certificate_id=' '), valid_digest), + 'missing-fact': ( + { + key: value + for key, value in _legacy_shared_certification().items() + if key != 'certificate_runtime_profile' + }, + valid_digest, + ), + 'malformed-normalized': (_legacy_shared_certification(normalized_digest='g' * 64), valid_digest), + 'dedicated-certificate': ( + _legacy_shared_certification(certificate_runtime_profile='dedicated'), + valid_digest, + ), + 'dedicated': (_legacy_shared_certification(runtime_profile='dedicated'), valid_digest), + 'wrong-admission': (_legacy_shared_certification(admission_code='SHARED_ELIGIBLE'), valid_digest), + 'uppercase-row-digest': (_legacy_shared_certification(), 'A' * 64), + 'nonhex-row-digest': (_legacy_shared_certification(), 'g' * 64), + } + plugin_settings = Base.metadata.tables['plugin_settings'] + workspaces = Base.metadata.tables['workspaces'] + async with engine.begin() as conn: + for index, (name, (certification, artifact_digest)) in enumerate(rows.items(), start=1): + workspace_uuid = f'41100000-0000-4000-8000-{index:012d}' + await conn.execute( + workspaces.insert().values( + uuid=workspace_uuid, + instance_uuid=f'cert-backfill-{index}', + name=name, + slug=name, + ) + ) + await conn.execute( + plugin_settings.insert().values( + workspace_uuid=workspace_uuid, + plugin_author='langbot', + plugin_name=name, + installation_uuid=f'51100000-0000-4000-8000-{index:012d}', + artifact_digest=artifact_digest, + runtime_revision=1, + install_info={ + '_certification': certification, + 'preserved_install_key': ['keep', {'nested': True}], + }, + ) + ) + + await run_alembic_upgrade(engine, 'head') + # Re-running the data revision itself must also be harmless. + migration = __import__( + 'langbot.pkg.persistence.alembic.versions.0032_certification_artifact_digest_backfill', + fromlist=['upgrade'], + ) + async with engine.begin() as conn: + await conn.run_sync(lambda sync: migration.backfill_certification_artifact_digests(sync)) + stored = { + name: (artifact_digest, install_info) + for name, artifact_digest, install_info in ( + await conn.execute( + sa.select( + plugin_settings.c.plugin_name, + plugin_settings.c.artifact_digest, + plugin_settings.c.install_info, + ) + ) + ).all() + } + + for name in ('eligible-a', 'eligible-b'): + eligible_digest, eligible_info = stored[name] + assert eligible_info['preserved_install_key'] == ['keep', {'nested': True}] + assert eligible_info['_certification']['preserved_certificate_key'] == {'nested': True} + assert eligible_info['_certification']['artifact_digest'] == eligible_digest == valid_digest + assert ( + execution_mode_for_persisted_installation( + artifact_digest=eligible_digest, + install_info=eligible_info, + ) + is PluginExecutionMode.SHARED_CERTIFIED + ) + + for name, (original_certification, artifact_digest) in rows.items(): + if name in {'eligible-a', 'eligible-b'}: + continue + stored_digest, stored_info = stored[name] + assert stored_digest == artifact_digest + assert stored_info['_certification'] == original_certification + if name != 'present-matching': + assert ( + execution_mode_for_persisted_installation( + artifact_digest=stored_digest, + install_info=stored_info, + ) + is PluginExecutionMode.DEDICATED + ) + + +@pytest.mark.asyncio +async def test_certification_artifact_digest_backfill_accepts_fresh_current_schema(convergence_engine): + engine = convergence_engine + async with engine.begin() as conn: + await conn.run_sync(Base.metadata.create_all) + + await run_alembic_upgrade(engine, 'head') + await run_alembic_upgrade(engine, 'head') + + assert await get_alembic_current(engine) == get_alembic_head() + + @pytest.mark.asyncio async def test_merge_only_downgrade_preserves_both_branch_schemas(convergence_engine): engine = convergence_engine diff --git a/tests/integration/persistence/test_migrations_postgres.py b/tests/integration/persistence/test_migrations_postgres.py index db3625848..babe2c207 100644 --- a/tests/integration/persistence/test_migrations_postgres.py +++ b/tests/integration/persistence/test_migrations_postgres.py @@ -329,6 +329,76 @@ class TestPostgreSQLMigrationBaseline: rev = await get_alembic_current(postgres_engine) assert rev == '0001_baseline' + @pytest.mark.asyncio + async def test_certification_artifact_digest_backfill_updates_only_complete_legacy_shared_rows( + self, + postgres_engine, + clean_tables, + clean_alembic_version, + ): + async with postgres_engine.begin() as conn: + await conn.run_sync(Base.metadata.create_all) + await run_alembic_stamp(postgres_engine, '0031_merge_totp_assistant') + + plugin_settings = Base.metadata.tables['plugin_settings'] + workspaces = Base.metadata.tables['workspaces'] + digest = 'a' * 64 + complete = { + 'normalized_digest': 'B' * 64, + 'verification': 'valid', + 'certificate_runtime_profile': 'shared-runtime-v1', + 'certificate_id': 'ed25519:trusted-issuer', + 'runtime_profile': 'shared-runtime-v1', + 'admission_code': 'CERTIFIED_PLUGIN_SHARED_ELIGIBLE', + 'preserved': {'key': True}, + } + rows = { + 'eligible': complete, + 'invalid': {**complete, 'verification': 'invalid'}, + 'present': {**complete, 'artifact_digest': 'c' * 64}, + } + async with postgres_engine.begin() as conn: + for index, (name, certification) in enumerate(rows.items(), start=1): + workspace_uuid = f'61100000-0000-4000-8000-{index:012d}' + await conn.execute( + workspaces.insert().values( + uuid=workspace_uuid, + instance_uuid=f'postgres-cert-backfill-{index}', + name=name, + slug=name, + ) + ) + await conn.execute( + plugin_settings.insert().values( + workspace_uuid=workspace_uuid, + plugin_author='langbot', + plugin_name=name, + installation_uuid=f'71100000-0000-4000-8000-{index:012d}', + artifact_digest=digest, + runtime_revision=1, + install_info={'_certification': certification, 'preserved': ['install-info']}, + ) + ) + + await run_alembic_upgrade(postgres_engine, 'head') + + async with postgres_engine.connect() as conn: + stored = dict( + (await conn.execute(sa.select(plugin_settings.c.plugin_name, plugin_settings.c.install_info))).all() + ) + assert stored['eligible'] == { + '_certification': {**complete, 'artifact_digest': digest}, + 'preserved': ['install-info'], + } + assert stored['invalid'] == { + '_certification': rows['invalid'], + 'preserved': ['install-info'], + } + assert stored['present'] == { + '_certification': rows['present'], + 'preserved': ['install-info'], + } + @pytest.mark.asyncio async def test_fresh_postgres_schema_accepts_application_casefold_identity( self, From ede433f0b11a2690b139995e8243901ac289aad6 Mon Sep 17 00:00:00 2001 From: RockChinQ Date: Fri, 25 Sep 2026 22:34:08 +0000 Subject: [PATCH 05/12] fix(plugin): pin runtime SDK 0.6.4 --- pyproject.toml | 2 +- uv.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 7304d10e7..8c1befce3 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.3", + "langbot-plugin==0.6.4", "asyncpg>=0.30.0", "line-bot-sdk>=3.19.0", "matrix-nio>=0.25.2", diff --git a/uv.lock b/uv.lock index 19c8e30ab..8da454cfe 100644 --- a/uv.lock +++ b/uv.lock @@ -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.3" }, + { name = "langbot-plugin", specifier = "==0.6.4" }, { 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.3" +version = "0.6.4" 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/ba/01/c45f438206802c15ada87277fb1a219c4f88b8149a899ba0a0d8b47b28f8/langbot_plugin-0.6.3.tar.gz", hash = "sha256:73ff8e2d3efa88c0a6ccc7166e270b9c6f9b3fe9bb598932614be54c6ff8d920", size = 653192, upload-time = "2026-09-25T19:06:20.892Z" } +sdist = { url = "https://files.pythonhosted.org/packages/36/25/fd4659835ee216b33b3e88d4c820c28d12b7c13cb51e92542c71087c4dd9/langbot_plugin-0.6.4.tar.gz", hash = "sha256:ed2b11f687a3dc1947a32d75276d914461967bd636bc447890c66940965dfbc8", size = 653753, upload-time = "2026-09-25T22:29:17.897Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/05/77/1aa9f9374690ffb5d1d5fa266afd10485c92d7c5f0bf22e37bed82a639ea/langbot_plugin-0.6.3-py3-none-any.whl", hash = "sha256:8afbe883fbf2e61a8d927ffaedabc66a7fab9dc9135b1657662226319cb5b34e", size = 426068, upload-time = "2026-09-25T19:06:19.597Z" }, + { url = "https://files.pythonhosted.org/packages/54/a7/20f716dd7f53e1b56ab6fae82a36ca4e3a08d54a834760b1d00c1e5d6ec1/langbot_plugin-0.6.4-py3-none-any.whl", hash = "sha256:a403c91748c8fb71da5f2bdb8b12080464c836199748eda340261258e760b241", size = 426491, upload-time = "2026-09-25T22:29:16.453Z" }, ] [[package]] From 9690f0bc6c1bf2159159c74860f1b4ed63df5e78 Mon Sep 17 00:00:00 2001 From: RockChinQ Date: Sat, 26 Sep 2026 04:29:05 +0000 Subject: [PATCH 06/12] fix(plugin): pin runtime SDK 0.6.5 --- pyproject.toml | 2 +- uv.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 8c1befce3..da7c778f1 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.4", + "langbot-plugin==0.6.5", "asyncpg>=0.30.0", "line-bot-sdk>=3.19.0", "matrix-nio>=0.25.2", diff --git a/uv.lock b/uv.lock index 8da454cfe..ce34dbc92 100644 --- a/uv.lock +++ b/uv.lock @@ -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.4" }, + { name = "langbot-plugin", specifier = "==0.6.5" }, { 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.4" +version = "0.6.5" 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/36/25/fd4659835ee216b33b3e88d4c820c28d12b7c13cb51e92542c71087c4dd9/langbot_plugin-0.6.4.tar.gz", hash = "sha256:ed2b11f687a3dc1947a32d75276d914461967bd636bc447890c66940965dfbc8", size = 653753, upload-time = "2026-09-25T22:29:17.897Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/32/673fbbb1db2339f8ecc549a891ba973a42a03038fdf9cde656dccc2ee354/langbot_plugin-0.6.5.tar.gz", hash = "sha256:e6cdda191dd656a5da819fd61ea7f8da1edb2e1cca517d1ad4d2a81cb19489bf", size = 654121, upload-time = "2026-09-26T04:24:05.309Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/54/a7/20f716dd7f53e1b56ab6fae82a36ca4e3a08d54a834760b1d00c1e5d6ec1/langbot_plugin-0.6.4-py3-none-any.whl", hash = "sha256:a403c91748c8fb71da5f2bdb8b12080464c836199748eda340261258e760b241", size = 426491, upload-time = "2026-09-25T22:29:16.453Z" }, + { url = "https://files.pythonhosted.org/packages/e4/bc/5cf953d24551ac39568374cdca3bfa09f0a5efd0f2fee03691305966217a/langbot_plugin-0.6.5-py3-none-any.whl", hash = "sha256:754af57782fb36d533d15dd2b802e166e693a125126ec56c190e86183144a2c9", size = 426632, upload-time = "2026-09-26T04:24:03.885Z" }, ] [[package]] From 37e80e510f4b8866fd1dae766839679ece80802d Mon Sep 17 00:00:00 2001 From: RockChinQ Date: Sat, 26 Sep 2026 05:21:50 +0000 Subject: [PATCH 07/12] fix(plugin): pin runtime SDK 0.6.6 --- pyproject.toml | 2 +- uv.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index da7c778f1..7d72ec17a 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.5", + "langbot-plugin==0.6.6", "asyncpg>=0.30.0", "line-bot-sdk>=3.19.0", "matrix-nio>=0.25.2", diff --git a/uv.lock b/uv.lock index ce34dbc92..a86fdc249 100644 --- a/uv.lock +++ b/uv.lock @@ -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.5" }, + { name = "langbot-plugin", specifier = "==0.6.6" }, { 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.5" +version = "0.6.6" 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/1b/32/673fbbb1db2339f8ecc549a891ba973a42a03038fdf9cde656dccc2ee354/langbot_plugin-0.6.5.tar.gz", hash = "sha256:e6cdda191dd656a5da819fd61ea7f8da1edb2e1cca517d1ad4d2a81cb19489bf", size = 654121, upload-time = "2026-09-26T04:24:05.309Z" } +sdist = { url = "https://files.pythonhosted.org/packages/fe/1e/49001ea4c7d6971676a80a71789993f59788bf49c5a1e27b7111a9bf1458/langbot_plugin-0.6.6.tar.gz", hash = "sha256:d8a5bdc052193f888cf9bbc2a66d189ac94dfa3535332a31556af4b2b90a0322", size = 654253, upload-time = "2026-09-26T05:19:55.803Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e4/bc/5cf953d24551ac39568374cdca3bfa09f0a5efd0f2fee03691305966217a/langbot_plugin-0.6.5-py3-none-any.whl", hash = "sha256:754af57782fb36d533d15dd2b802e166e693a125126ec56c190e86183144a2c9", size = 426632, upload-time = "2026-09-26T04:24:03.885Z" }, + { url = "https://files.pythonhosted.org/packages/5d/8a/0a913a62876ae22a0e9e2369c170ffac1ecbecfa9925c219beacd9f9e13d/langbot_plugin-0.6.6-py3-none-any.whl", hash = "sha256:a2aefcfb3375e3c9ffde52ce9a24bb752ad21c88fb8dd103cce555088f3b2341", size = 426631, upload-time = "2026-09-26T05:19:54.178Z" }, ] [[package]] From 92669ef58492100171a833ea665bc9931d3c024a Mon Sep 17 00:00:00 2001 From: RockChinQ Date: Sat, 26 Sep 2026 06:19:15 +0000 Subject: [PATCH 08/12] fix(plugin): pin runtime SDK 0.6.7 --- pyproject.toml | 2 +- uv.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 7d72ec17a..dba7ccf90 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.6", + "langbot-plugin==0.6.7", "asyncpg>=0.30.0", "line-bot-sdk>=3.19.0", "matrix-nio>=0.25.2", diff --git a/uv.lock b/uv.lock index a86fdc249..f580984a4 100644 --- a/uv.lock +++ b/uv.lock @@ -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.6" }, + { name = "langbot-plugin", specifier = "==0.6.7" }, { 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.6" +version = "0.6.7" 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/fe/1e/49001ea4c7d6971676a80a71789993f59788bf49c5a1e27b7111a9bf1458/langbot_plugin-0.6.6.tar.gz", hash = "sha256:d8a5bdc052193f888cf9bbc2a66d189ac94dfa3535332a31556af4b2b90a0322", size = 654253, upload-time = "2026-09-26T05:19:55.803Z" } +sdist = { url = "https://files.pythonhosted.org/packages/13/10/8befffaa147cc35a38eaba8911f37c7fb235a4c59138022d9af6f846f28a/langbot_plugin-0.6.7.tar.gz", hash = "sha256:c9dda943db76544677724d62ca57b83d62fc0acfa08a1b091998c306c469cfa1", size = 654262, upload-time = "2026-09-26T06:15:14.617Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5d/8a/0a913a62876ae22a0e9e2369c170ffac1ecbecfa9925c219beacd9f9e13d/langbot_plugin-0.6.6-py3-none-any.whl", hash = "sha256:a2aefcfb3375e3c9ffde52ce9a24bb752ad21c88fb8dd103cce555088f3b2341", size = 426631, upload-time = "2026-09-26T05:19:54.178Z" }, + { url = "https://files.pythonhosted.org/packages/60/04/b322410853981d52f5f38250df8f16d64a47ae8e50f3f9bbefd2fc78d941/langbot_plugin-0.6.7-py3-none-any.whl", hash = "sha256:f52b836e3d0833d25af84232af3418480f88855aad4864843bf4c4b961f49917", size = 426625, upload-time = "2026-09-26T06:15:13.327Z" }, ] [[package]] From ccbf7c226ed419d7cb7b97cb8de30fa2e92cc531 Mon Sep 17 00:00:00 2001 From: RockChinQ <45992437+RockChinQ@users.noreply.github.com> Date: Sat, 26 Sep 2026 13:02:43 +0000 Subject: [PATCH 09/12] chore(runtime): pin plugin SDK 0.6.8 --- pyproject.toml | 2 +- uv.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index dba7ccf90..2a9ee61c8 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.7", + "langbot-plugin==0.6.8", "asyncpg>=0.30.0", "line-bot-sdk>=3.19.0", "matrix-nio>=0.25.2", diff --git a/uv.lock b/uv.lock index f580984a4..9732bd714 100644 --- a/uv.lock +++ b/uv.lock @@ -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.7" }, + { name = "langbot-plugin", specifier = "==0.6.8" }, { 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.7" +version = "0.6.8" 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/13/10/8befffaa147cc35a38eaba8911f37c7fb235a4c59138022d9af6f846f28a/langbot_plugin-0.6.7.tar.gz", hash = "sha256:c9dda943db76544677724d62ca57b83d62fc0acfa08a1b091998c306c469cfa1", size = 654262, upload-time = "2026-09-26T06:15:14.617Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/fe/8115c497b3b37fa8199c74bdca406bf91b817c95e367bb077fd7b1c8e955/langbot_plugin-0.6.8.tar.gz", hash = "sha256:5b15479f0520e0cb46fd80d9770ef755bbf0bf56a06876d133e01801725213ad", size = 654528, upload-time = "2026-09-26T12:50:02.96Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/60/04/b322410853981d52f5f38250df8f16d64a47ae8e50f3f9bbefd2fc78d941/langbot_plugin-0.6.7-py3-none-any.whl", hash = "sha256:f52b836e3d0833d25af84232af3418480f88855aad4864843bf4c4b961f49917", size = 426625, upload-time = "2026-09-26T06:15:13.327Z" }, + { url = "https://files.pythonhosted.org/packages/91/ba/f0949d126387482b3012e351635fbca526b5141435c57c25b0ab0605929f/langbot_plugin-0.6.8-py3-none-any.whl", hash = "sha256:e533003c19af87fe54ced2fac6884be1edc7abf4d2da123000f4a63a8fda33dd", size = 426618, upload-time = "2026-09-26T12:50:01.469Z" }, ] [[package]] From 64dc04d84857970505508f3e87e24840dfa88e55 Mon Sep 17 00:00:00 2001 From: RockChinQ <45992437+RockChinQ@users.noreply.github.com> Date: Sat, 26 Sep 2026 14:33:35 +0000 Subject: [PATCH 10/12] chore(runtime): pin plugin SDK 0.6.9 --- pyproject.toml | 2 +- uv.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 2a9ee61c8..0d9fcae86 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.8", + "langbot-plugin==0.6.9", "asyncpg>=0.30.0", "line-bot-sdk>=3.19.0", "matrix-nio>=0.25.2", diff --git a/uv.lock b/uv.lock index 9732bd714..6d3444a78 100644 --- a/uv.lock +++ b/uv.lock @@ -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.8" }, + { name = "langbot-plugin", specifier = "==0.6.9" }, { 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.8" +version = "0.6.9" 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/e0/fe/8115c497b3b37fa8199c74bdca406bf91b817c95e367bb077fd7b1c8e955/langbot_plugin-0.6.8.tar.gz", hash = "sha256:5b15479f0520e0cb46fd80d9770ef755bbf0bf56a06876d133e01801725213ad", size = 654528, upload-time = "2026-09-26T12:50:02.96Z" } +sdist = { url = "https://files.pythonhosted.org/packages/8f/fc/ea0ab5b4299071a232b5bd636a73d3595a2c6caa386f410ecddefe19a4ff/langbot_plugin-0.6.9.tar.gz", hash = "sha256:361686d641d0b19a7a4542760e5077b82e60de66465d00ed5f372e6f9c4fb31c", size = 654792, upload-time = "2026-09-26T14:30:16.9Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/91/ba/f0949d126387482b3012e351635fbca526b5141435c57c25b0ab0605929f/langbot_plugin-0.6.8-py3-none-any.whl", hash = "sha256:e533003c19af87fe54ced2fac6884be1edc7abf4d2da123000f4a63a8fda33dd", size = 426618, upload-time = "2026-09-26T12:50:01.469Z" }, + { url = "https://files.pythonhosted.org/packages/dd/e2/9b9bdbcefeaf68234f36fe1c837522601f89cb067efef9a60aafee98d23b/langbot_plugin-0.6.9-py3-none-any.whl", hash = "sha256:78a8ba6760d3f6754ab158a41c503f172bdc328737a6184fb7329da7aa3ef159", size = 426803, upload-time = "2026-09-26T14:30:15.41Z" }, ] [[package]] From 3493660f5e9bdd09bd9f508ff15bf6e9e5cc2770 Mon Sep 17 00:00:00 2001 From: RockChinQ <45992437+RockChinQ@users.noreply.github.com> Date: Sat, 26 Sep 2026 17:08:51 +0000 Subject: [PATCH 11/12] chore(runtime): pin plugin SDK 0.6.10 --- pyproject.toml | 2 +- uv.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 0d9fcae86..4a6398302 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.9", + "langbot-plugin==0.6.10", "asyncpg>=0.30.0", "line-bot-sdk>=3.19.0", "matrix-nio>=0.25.2", diff --git a/uv.lock b/uv.lock index 6d3444a78..0e69db076 100644 --- a/uv.lock +++ b/uv.lock @@ -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.9" }, + { name = "langbot-plugin", specifier = "==0.6.10" }, { 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.9" +version = "0.6.10" 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/8f/fc/ea0ab5b4299071a232b5bd636a73d3595a2c6caa386f410ecddefe19a4ff/langbot_plugin-0.6.9.tar.gz", hash = "sha256:361686d641d0b19a7a4542760e5077b82e60de66465d00ed5f372e6f9c4fb31c", size = 654792, upload-time = "2026-09-26T14:30:16.9Z" } +sdist = { url = "https://files.pythonhosted.org/packages/3e/30/9653e29a0cba8d0d45c59a6fa1450ba427bcb6f48631eeda62fb91fc661b/langbot_plugin-0.6.10.tar.gz", hash = "sha256:322d78d02ca33b325f4226636598936df5645b4965eb32df448751d6f2dc41f6", size = 655800, upload-time = "2026-09-26T17:07:03.64Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/dd/e2/9b9bdbcefeaf68234f36fe1c837522601f89cb067efef9a60aafee98d23b/langbot_plugin-0.6.9-py3-none-any.whl", hash = "sha256:78a8ba6760d3f6754ab158a41c503f172bdc328737a6184fb7329da7aa3ef159", size = 426803, upload-time = "2026-09-26T14:30:15.41Z" }, + { url = "https://files.pythonhosted.org/packages/0f/c5/7f62fafad6ed20e3f5141c24d9cc33ab4eb94916852d1d98e222f9105c44/langbot_plugin-0.6.10-py3-none-any.whl", hash = "sha256:5d2cffaf645c3f318e789cfb7a4503b78664d9020e2605da65bc8e55ccbd64fb", size = 427642, upload-time = "2026-09-26T17:07:02.074Z" }, ] [[package]] From 755149ab0a1090105293b1897a6a7af82458db2d Mon Sep 17 00:00:00 2001 From: RockChinQ <45992437+RockChinQ@users.noreply.github.com> Date: Sat, 26 Sep 2026 18:16:39 +0000 Subject: [PATCH 12/12] chore(runtime): pin plugin SDK 0.6.11 --- pyproject.toml | 2 +- uv.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 4a6398302..fbbf55421 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.10", + "langbot-plugin==0.6.11", "asyncpg>=0.30.0", "line-bot-sdk>=3.19.0", "matrix-nio>=0.25.2", diff --git a/uv.lock b/uv.lock index 0e69db076..1ddf4e82a 100644 --- a/uv.lock +++ b/uv.lock @@ -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.10" }, + { name = "langbot-plugin", specifier = "==0.6.11" }, { 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.10" +version = "0.6.11" 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/3e/30/9653e29a0cba8d0d45c59a6fa1450ba427bcb6f48631eeda62fb91fc661b/langbot_plugin-0.6.10.tar.gz", hash = "sha256:322d78d02ca33b325f4226636598936df5645b4965eb32df448751d6f2dc41f6", size = 655800, upload-time = "2026-09-26T17:07:03.64Z" } +sdist = { url = "https://files.pythonhosted.org/packages/41/13/7f913f88494205abb1a6390746e88de4e1148fbf6d0a273bbe2450f5fb0c/langbot_plugin-0.6.11.tar.gz", hash = "sha256:2ecddc2ef3a2eb92925c08cf0f19887e9be573b93e7d5de49494c01c083cfac7", size = 655797, upload-time = "2026-09-26T18:15:19.81Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0f/c5/7f62fafad6ed20e3f5141c24d9cc33ab4eb94916852d1d98e222f9105c44/langbot_plugin-0.6.10-py3-none-any.whl", hash = "sha256:5d2cffaf645c3f318e789cfb7a4503b78664d9020e2605da65bc8e55ccbd64fb", size = 427642, upload-time = "2026-09-26T17:07:02.074Z" }, + { url = "https://files.pythonhosted.org/packages/25/10/00e3d5d985d82aa08dd5f2f01aa2b85e8b46880dcfa2aa9b923f7be285da/langbot_plugin-0.6.11-py3-none-any.whl", hash = "sha256:85458919c943894780db85bbd96c0a61354e231bc396a3f74072a8eddc978b07", size = 427659, upload-time = "2026-09-26T18:15:18.003Z" }, ] [[package]]