From b51a448b1e6305fb77304e1ccd0c19e40a735fb1 Mon Sep 17 00:00:00 2001 From: RockChinQ <1010553892@qq.com> Date: Mon, 28 Sep 2026 13:25:40 +0800 Subject: [PATCH] feat: stateless certified shared worker Implements stateless singleton component model for certified shared Workers, integrating with SDK 0.7.0. --- docs/architecture/certified-plugins.md | 31 ++++++++++----- pyproject.toml | 2 +- src/langbot/pkg/plugin/certification.py | 11 +++++- src/langbot/pkg/plugin/connector.py | 1 + .../test_migration_branch_convergence.py | 4 +- .../plugin/test_certified_plugin_admission.py | 5 ++- .../plugin/test_certified_plugin_policy.py | 38 +++++++++++++++++++ uv.lock | 8 ++-- 8 files changed, 81 insertions(+), 19 deletions(-) diff --git a/docs/architecture/certified-plugins.md b/docs/architecture/certified-plugins.md index d30d11747..56d8f6eb8 100644 --- a/docs/architecture/certified-plugins.md +++ b/docs/architecture/certified-plugins.md @@ -10,7 +10,7 @@ ZIP digest without extracting the payload. 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 +profile, `stateless-v1` component model, 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. @@ -48,11 +48,11 @@ dedicated profile. | Deployment | SDK verification | Explicit `administrator_force` | Result | | --- | --- | --- | --- | -| Cloud | valid envelope declaring `shared-runtime-v1` | any | admitted to the shared profile | +| Cloud | valid envelope declaring `shared-runtime-v1` + `stateless-v1` | any | admitted to the shared singleton profile | | Cloud | absent | any | reject before storage with `CERTIFIED_PLUGIN_CLOUD_CERTIFICATE_REQUIRED` | | Cloud | malformed, untrusted, invalid, or non-shared | any | reject before storage with `CERTIFIED_PLUGIN_CLOUD_CERTIFICATE_INVALID` | | OSS | absent legacy envelope | any | admitted to the dedicated profile | -| OSS | valid envelope declaring `shared-runtime-v1` | any | selected shared profile | +| OSS | valid envelope declaring `shared-runtime-v1` + `stateless-v1` | any | selected shared singleton profile | | OSS | declaration signed by a **key this instance resolves** | false | reject with `CERTIFIED_PLUGIN_OSS_FORCE_REQUIRED` | | OSS | declaration signed by a **key this instance resolves** | true | admitted to the dedicated profile | | OSS | declaration this instance **cannot resolve** (empty ring) | any | admitted to the dedicated profile | @@ -69,9 +69,10 @@ the existing `oss_dev` dedicated profile and records withholds the shared profile rather than granting it. A declaration is "resolvable" only when its `key_id` is present in the -configured ring. When the ring is configured and the declaration still fails -(malformed, `signature_invalid`, `digest_mismatch`, `unsupported_schema`, ...), -admission stays explicit and requires `administrator_force`. +configured ring. A parseable declaration from a resolved key that fails +signature, digest, identity, profile, or component-model validation requires +`administrator_force` in OSS. Malformed or unsupported envelopes without a +resolved certificate identity are treated as untrusted and stay dedicated. `administrator_force` is deliberately strict: it is recognized only when the install request carries boolean `true`. The local upload endpoint accepts the @@ -81,8 +82,9 @@ protects those endpoints. A force never creates a Cloud dedicated fallback. ## Runtime and logs -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 first stateless-compatible SDK release will carry the v2 certificate and +singleton component contract; its final version is assigned only at release. +Core must pin that release before sending or selecting the new contract. 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 @@ -90,6 +92,13 @@ 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 certificate must also bind `component_model=stateless-v1`. This profile +creates one `BasePlugin` and one instance of each component per digest Worker. +Installation slots contain immutable config snapshots and authority only; +task-local invocation context selects the active slot. Older certificates that +do not bind the component model are not eligible for shared placement. Their +source packages remain compatible on dedicated Workers under OSS policy. + The existing public plugin-log boundary already applies the immutable installation binding (including workspace UUID) through `RuntimeConnectionHandler.installation_scope()` before requesting logs. This is @@ -100,5 +109,7 @@ same existing installation scope. ## SDK versioning -Core pins `langbot-plugin==0.6.2`, the first published SDK release carrying the -canonical installation execution-mode contract. +Ship in dependency order: SDK v2 certificate support, then Core exact pin and +lockfile, then Space signing, then Runtime/Core rollout. Legacy v1 envelopes +remain verifiable but do not carry `stateless-v1`, so they never select shared +singleton placement without re-review and v2 re-signing. diff --git a/pyproject.toml b/pyproject.toml index cf7ebbf9e..dadd30f61 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.20", + "langbot-plugin==0.7.0", "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 6a538980b..4af562cd5 100644 --- a/src/langbot/pkg/plugin/certification.py +++ b/src/langbot/pkg/plugin/certification.py @@ -18,6 +18,7 @@ from langbot_plugin.entities.io.context import PluginExecutionMode SHARED_RUNTIME_V1 = 'shared-runtime-v1' +STATELESS_COMPONENT_MODEL_V1 = 'stateless-v1' DEDICATED_RUNTIME = 'dedicated' @@ -66,11 +67,16 @@ class CertificateFacts: verification: CertificateVerification runtime_profile: str | None = None + component_model: str | None = None certificate_id: str | None = None @property def is_valid_shared_runtime(self) -> bool: - return self.verification is CertificateVerification.VALID and self.runtime_profile == SHARED_RUNTIME_V1 + return ( + self.verification is CertificateVerification.VALID + and self.runtime_profile == SHARED_RUNTIME_V1 + and self.component_model == STATELESS_COMPONENT_MODEL_V1 + ) @property def is_declared(self) -> bool: @@ -153,6 +159,7 @@ def verify_plugin_archive_certificate( verification = verify_archive(archive, key_ring.get) envelope = verification.envelope runtime_profile = envelope.shared_runtime if envelope is not None else None + component_model = envelope.component_model if envelope is not None else None # Record the issuer identity only when this instance actually resolved it # through the configured ring. When the ring is empty (for example a # self-hosted deployment that never configured @@ -170,6 +177,7 @@ def verify_plugin_archive_certificate( certificate=CertificateFacts( verification=state, runtime_profile=runtime_profile, + component_model=component_model, certificate_id=certificate_id, ), ) @@ -299,6 +307,7 @@ def execution_mode_for_persisted_installation( 'artifact_digest': artifact_digest, 'verification': CertificateVerification.VALID.value, 'certificate_runtime_profile': SHARED_RUNTIME_V1, + 'certificate_component_model': STATELESS_COMPONENT_MODEL_V1, 'runtime_profile': SHARED_RUNTIME_V1, 'admission_code': AdmissionCode.SHARED_ELIGIBLE.value, } diff --git a/src/langbot/pkg/plugin/connector.py b/src/langbot/pkg/plugin/connector.py index a7d140539..4ae024792 100644 --- a/src/langbot/pkg/plugin/connector.py +++ b/src/langbot/pkg/plugin/connector.py @@ -1809,6 +1809,7 @@ class PluginRuntimeConnector(ManagedRuntimeConnector): 'normalized_digest': facts.artifact_digest, 'verification': facts.certificate.verification.value, 'certificate_runtime_profile': facts.certificate.runtime_profile, + 'certificate_component_model': facts.certificate.component_model, 'certificate_id': facts.certificate.certificate_id, 'runtime_profile': decision.runtime_profile, 'admission_code': decision.code.value, diff --git a/tests/integration/persistence/test_migration_branch_convergence.py b/tests/integration/persistence/test_migration_branch_convergence.py index 759b46b03..6020c9b37 100644 --- a/tests/integration/persistence/test_migration_branch_convergence.py +++ b/tests/integration/persistence/test_migration_branch_convergence.py @@ -370,7 +370,7 @@ def _legacy_shared_certification(**overrides): @pytest.mark.asyncio -async def test_certification_artifact_digest_backfill_is_safe_and_enables_shared_placement(convergence_engine): +async def test_certification_artifact_digest_backfill_is_safe_and_keeps_legacy_dedicated(convergence_engine): engine = convergence_engine async with engine.begin() as conn: await conn.run_sync(Base.metadata.create_all) @@ -461,7 +461,7 @@ async def test_certification_artifact_digest_backfill_is_safe_and_enables_shared artifact_digest=eligible_digest, install_info=eligible_info, ) - is PluginExecutionMode.SHARED_CERTIFIED + is PluginExecutionMode.DEDICATED ) for name, (original_certification, artifact_digest) in rows.items(): diff --git a/tests/integration/plugin/test_certified_plugin_admission.py b/tests/integration/plugin/test_certified_plugin_admission.py index 0a5126ad9..04b50a16f 100644 --- a/tests/integration/plugin/test_certified_plugin_admission.py +++ b/tests/integration/plugin/test_certified_plugin_admission.py @@ -249,7 +249,10 @@ def _connector(deployment: str, trusted_public_keys: dict[str, str]): def _archive(kind: str) -> tuple[bytes, dict[str, str]]: manifest = { 'metadata': {'author': 'certified', 'name': 'example', 'version': '1.0.0'}, - 'execution': {'sharedRuntime': 'shared-runtime-v1'}, + 'execution': { + 'sharedRuntime': 'shared-runtime-v1', + 'componentModel': 'stateless-v1', + }, } if kind == 'legacy': manifest.pop('execution') diff --git a/tests/unit_tests/plugin/test_certified_plugin_policy.py b/tests/unit_tests/plugin/test_certified_plugin_policy.py index 0f5d80da2..4537b9e2c 100644 --- a/tests/unit_tests/plugin/test_certified_plugin_policy.py +++ b/tests/unit_tests/plugin/test_certified_plugin_policy.py @@ -85,6 +85,11 @@ def test_admission_policy_enforces_certification_matrix( certificate=CertificateFacts( verification=CertificateVerification(verification), runtime_profile=runtime_profile, + component_model=( + 'stateless-v1' + if verification == 'valid' and runtime_profile == 'shared-runtime-v1' + else None + ), certificate_id=certificate_id, ), ) @@ -102,6 +107,33 @@ def test_admission_policy_enforces_certification_matrix( ) +def test_legacy_shared_certificate_without_stateless_contract_is_not_shared() -> None: + from langbot.pkg.plugin.certification import ( + AdmissionDisposition, + CertificateFacts, + CertificateVerification, + DeploymentMode, + PluginCertificationFacts, + decide_plugin_admission, + ) + + decision = decide_plugin_admission( + deployment=DeploymentMode.CLOUD, + facts=PluginCertificationFacts( + installation_uuid='00000000-0000-4000-8000-000000000001', + artifact_digest='a' * 64, + certificate=CertificateFacts( + verification=CertificateVerification.VALID, + runtime_profile='shared-runtime-v1', + component_model=None, + certificate_id='legacy-issuer', + ), + ), + ) + + assert decision.disposition is AdmissionDisposition.REJECTED + + def test_archive_inspection_preserves_legacy_tuple_and_exposes_certificate_facts() -> None: from langbot.pkg.plugin.archive import ( ArchiveCertificateState, @@ -181,6 +213,7 @@ def test_log_visibility_policy_only_scopes_valid_shared_certifications( certificate=CertificateFacts( verification=CertificateVerification(verification), runtime_profile='shared-runtime-v1', + component_model='stateless-v1', ), ) @@ -195,6 +228,7 @@ def _complete_persisted_certification() -> dict[str, object]: 'normalized_digest': 'b' * 64, 'verification': 'valid', 'certificate_runtime_profile': 'shared-runtime-v1', + 'certificate_component_model': 'stateless-v1', 'certificate_id': 'ed25519:trusted-issuer', 'runtime_profile': 'shared-runtime-v1', 'admission_code': 'CERTIFIED_PLUGIN_SHARED_ELIGIBLE', @@ -261,6 +295,7 @@ def test_persisted_certification_selects_shared_execution_only_for_exact_admitte 'normalized_digest', 'verification', 'certificate_runtime_profile', + 'certificate_component_model', 'certificate_id', 'runtime_profile', 'admission_code', @@ -303,6 +338,9 @@ def test_persisted_certification_requires_every_shared_admission_fact(missing_fi ('certificate_runtime_profile', None), ('certificate_runtime_profile', 123), ('certificate_runtime_profile', ''), + ('certificate_component_model', None), + ('certificate_component_model', 123), + ('certificate_component_model', ''), ('runtime_profile', None), ('runtime_profile', 123), ('runtime_profile', ''), diff --git a/uv.lock b/uv.lock index 9274f34cd..74dd61719 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.20" }, + { name = "langbot-plugin", specifier = "==0.7.0" }, { 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.20" +version = "0.7.0" 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/dd/bb/08bfa37c29a9b6823678e0b837adddacab162b982b36737e0b5e7bea6c40/langbot_plugin-0.6.20.tar.gz", hash = "sha256:86b3803fb83e67379db4a6cf166dec67a8233fbfbc1dbbf744b2b765d0615879", size = 663741, upload-time = "2026-09-27T08:56:36.473Z" } +sdist = { url = "https://files.pythonhosted.org/packages/43/0a/b06aea51c8fe2611952e920002933046888ca20f040e27ef5d14531376ec/langbot_plugin-0.7.0.tar.gz", hash = "sha256:4709cbeedf5abea8b0bd1cccf8449857c1b22e81f843c5ac0db349795f216732", size = 669519, upload-time = "2026-09-28T04:35:18.244Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/48/28/f537c212f67afd3820bb56920b9b29783b68096fb6b2ca1a388d268dd27c/langbot_plugin-0.6.20-py3-none-any.whl", hash = "sha256:b110878d880b24ac739b26b3eb158067fb4d0be8b55ba3baf97d2092da920f10", size = 429929, upload-time = "2026-09-27T08:56:34.995Z" }, + { url = "https://files.pythonhosted.org/packages/b9/1e/2177341166347aa7e594eaf5cdf7cae56eb1087b63c696edcdae9e598eb2/langbot_plugin-0.7.0-py3-none-any.whl", hash = "sha256:5c3c9a22b0dc7072467fb5901abdeb2f24a6a07d53662ce3eaf666b1bf4c3c59", size = 432251, upload-time = "2026-09-28T04:35:16.744Z" }, ] [[package]]