From ce118b4f41ff4b711fda1226dd28994f5eb79dae Mon Sep 17 00:00:00 2001 From: RockChinQ Date: Mon, 28 Sep 2026 14:26:10 +0000 Subject: [PATCH] fix(plugin): allow unsigned Cloud plugins on dedicated workers --- docs/architecture/certified-plugins.md | 4 +- src/langbot/pkg/plugin/certification.py | 21 +++-------- .../plugin/test_certified_plugin_admission.py | 37 ++++++++----------- .../plugin/test_certified_plugin_policy.py | 24 +++++++++++- 4 files changed, 46 insertions(+), 40 deletions(-) diff --git a/docs/architecture/certified-plugins.md b/docs/architecture/certified-plugins.md index 56d8f6eb8..7af3bd4f4 100644 --- a/docs/architecture/certified-plugins.md +++ b/docs/architecture/certified-plugins.md @@ -49,8 +49,8 @@ dedicated profile. | Deployment | SDK verification | Explicit `administrator_force` | Result | | --- | --- | --- | --- | | 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` | +| Cloud | no signature | any | install on dedicated worker, without shared eligibility | +| Cloud | malformed, untrusted, invalid, or non-shared declaration | any | reject before storage with `CERTIFIED_PLUGIN_CLOUD_CERTIFICATE_INVALID`; do not treat a broken signature as unsigned | | OSS | absent legacy envelope | any | admitted to the dedicated 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` | diff --git a/src/langbot/pkg/plugin/certification.py b/src/langbot/pkg/plugin/certification.py index 4af562cd5..f5a73674c 100644 --- a/src/langbot/pkg/plugin/certification.py +++ b/src/langbot/pkg/plugin/certification.py @@ -43,12 +43,11 @@ class AdmissionDisposition(str, Enum): class AdmissionCode(str, Enum): SHARED_ELIGIBLE = 'CERTIFIED_PLUGIN_SHARED_ELIGIBLE' - CLOUD_CERTIFICATE_REQUIRED = 'CERTIFIED_PLUGIN_CLOUD_CERTIFICATE_REQUIRED' + UNSIGNED_DEDICATED = 'CERTIFIED_PLUGIN_UNSIGNED_DEDICATED' CLOUD_CERTIFICATE_INVALID = 'CERTIFIED_PLUGIN_CLOUD_CERTIFICATE_INVALID' OSS_LEGACY_DEDICATED = 'CERTIFIED_PLUGIN_OSS_LEGACY_DEDICATED' OSS_FORCE_REQUIRED = 'CERTIFIED_PLUGIN_OSS_FORCE_REQUIRED' OSS_FORCED_DEDICATED = 'CERTIFIED_PLUGIN_OSS_FORCED_DEDICATED' - OSS_CERTIFIED_DEDICATED = 'CERTIFIED_PLUGIN_OSS_CERTIFIED_DEDICATED' OSS_UNTRUSTED_DEDICATED = 'CERTIFIED_PLUGIN_OSS_UNTRUSTED_DEDICATED' @@ -196,7 +195,7 @@ def decide_plugin_admission( facts: PluginCertificationFacts, administrator_force: bool = False, ) -> PluginAdmissionDecision: - """Apply Cloud fail-closed and OSS administrator-force admission rules.""" + """Only verified cross-tenant certificates grant shared placement.""" mode = DeploymentMode(deployment) certificate = facts.certificate @@ -207,26 +206,16 @@ def decide_plugin_admission( SHARED_RUNTIME_V1, ) - if mode is DeploymentMode.CLOUD: - code = ( - AdmissionCode.CLOUD_CERTIFICATE_REQUIRED - if certificate.verification is CertificateVerification.ABSENT - else AdmissionCode.CLOUD_CERTIFICATE_INVALID - ) - return PluginAdmissionDecision(AdmissionDisposition.REJECTED, code, DEDICATED_RUNTIME) - if certificate.verification is CertificateVerification.ABSENT: return PluginAdmissionDecision( AdmissionDisposition.DEDICATED_ALLOWED, - AdmissionCode.OSS_LEGACY_DEDICATED, + AdmissionCode.UNSIGNED_DEDICATED if mode is DeploymentMode.CLOUD else AdmissionCode.OSS_LEGACY_DEDICATED, DEDICATED_RUNTIME, ) - if certificate.verification is CertificateVerification.VALID: + if mode is DeploymentMode.CLOUD: return PluginAdmissionDecision( - AdmissionDisposition.DEDICATED_ALLOWED, - AdmissionCode.OSS_CERTIFIED_DEDICATED, - DEDICATED_RUNTIME, + AdmissionDisposition.REJECTED, AdmissionCode.CLOUD_CERTIFICATE_INVALID, DEDICATED_RUNTIME ) # A self-hosted deployment that has not configured the issuer key ring cannot diff --git a/tests/integration/plugin/test_certified_plugin_admission.py b/tests/integration/plugin/test_certified_plugin_admission.py index 04b50a16f..ce9b5eb62 100644 --- a/tests/integration/plugin/test_certified_plugin_admission.py +++ b/tests/integration/plugin/test_certified_plugin_admission.py @@ -29,6 +29,7 @@ pytestmark = pytest.mark.integration ('deployment', 'archive_kind', 'administrator_force', 'expected_profile'), [ ('cloud', 'signed_shared', False, 'shared-runtime-v1'), + ('cloud', 'legacy', False, 'dedicated'), ('oss', 'signed_shared', False, 'shared-runtime-v1'), ('oss', 'legacy', False, 'dedicated'), ('oss', 'forged_shared', True, 'dedicated'), @@ -75,7 +76,7 @@ async def test_install_plugin_admits_archive_before_persistence_and_applies_sele @pytest.mark.asyncio -@pytest.mark.parametrize('archive_kind', ['legacy', 'invalid_shared']) +@pytest.mark.parametrize('archive_kind', ['invalid_shared']) async def test_cloud_rejects_untrusted_archive_before_storage_persistence_or_runtime_apply(archive_kind: str) -> None: package, trusted_public_keys = _archive(archive_kind) connector, _execution_context, _binding = _connector('cloud', trusted_public_keys) @@ -180,26 +181,20 @@ async def test_marketplace_version_selection_keeps_certificate_gate_and_single_a if requested_version is not None: info['plugin_version'] = requested_version task_context = TaskContext.new() - if archive_kind == 'legacy': - with pytest.raises(ValueError, match='CERTIFIED_PLUGIN_CLOUD_CERTIFICATE_REQUIRED'): - await connector.install_plugin(PluginInstallSource.MARKETPLACE, info, task_context) - connector._store_artifact_package.assert_not_awaited() - connector._persist_installation_package.assert_not_awaited() - connector.handler.apply_plugin_installation.assert_not_awaited() - connector._refresh_runner_registry.assert_not_awaited() - else: - await connector.install_plugin(PluginInstallSource.MARKETPLACE, info, task_context) - persisted_info = connector._persist_installation_package.await_args.kwargs['install_info'] - 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, - execution_mode=PluginExecutionMode.SHARED_CERTIFIED, - ) - connector._refresh_runner_registry.assert_awaited_once() - assert task_context.metadata['progress_percent'] == 100 + await connector.install_plugin(PluginInstallSource.MARKETPLACE, info, task_context) + persisted_info = connector._persist_installation_package.await_args.kwargs['install_info'] + assert persisted_info['plugin_version'] == '1.0.0' + assert persisted_info['_certification']['runtime_profile'] == ( + 'shared-runtime-v1' if archive_kind == 'signed_shared' else 'dedicated' + ) + connector.handler.apply_plugin_installation.assert_awaited_once_with( + binding, + artifact_package=package, + enabled=True, + execution_mode=(PluginExecutionMode.SHARED_CERTIFIED if archive_kind == 'signed_shared' else PluginExecutionMode.DEDICATED), + ) + connector._refresh_runner_registry.assert_awaited_once() + assert task_context.metadata['progress_percent'] == 100 if requested_version is not None: # Confirmed migrations must fetch the reviewed release, never latest/MCP/skill. assert len(requests) == 1 diff --git a/tests/unit_tests/plugin/test_certified_plugin_policy.py b/tests/unit_tests/plugin/test_certified_plugin_policy.py index 4537b9e2c..f2bf45fd7 100644 --- a/tests/unit_tests/plugin/test_certified_plugin_policy.py +++ b/tests/unit_tests/plugin/test_certified_plugin_policy.py @@ -13,7 +13,8 @@ from langbot_plugin.entities.io.context import PluginExecutionMode ('deployment', 'certificate', 'certificate_id', 'force', 'expected_disposition', 'expected_code'), [ ('cloud', ('valid', 'shared-runtime-v1'), 'issuer', False, 'shared_eligible', 'CERTIFIED_PLUGIN_SHARED_ELIGIBLE'), - ('cloud', ('absent', None), None, False, 'rejected', 'CERTIFIED_PLUGIN_CLOUD_CERTIFICATE_REQUIRED'), + ('cloud', ('absent', None), None, False, 'dedicated_allowed', 'CERTIFIED_PLUGIN_UNSIGNED_DEDICATED'), + ('cloud', ('absent', None), None, True, 'dedicated_allowed', 'CERTIFIED_PLUGIN_UNSIGNED_DEDICATED'), ('cloud', ('malformed', None), None, False, 'rejected', 'CERTIFIED_PLUGIN_CLOUD_CERTIFICATE_INVALID'), ( 'cloud', @@ -134,6 +135,27 @@ def test_legacy_shared_certificate_without_stateless_contract_is_not_shared() -> assert decision.disposition is AdmissionDisposition.REJECTED +def test_oss_does_not_treat_valid_nonshared_signature_as_dedicated_certification() -> None: + from langbot.pkg.plugin.certification import ( + AdmissionDisposition, CertificateFacts, CertificateVerification, + PluginCertificationFacts, decide_plugin_admission, + ) + + decision = decide_plugin_admission( + deployment='oss', + facts=PluginCertificationFacts( + installation_uuid='00000000-0000-4000-8000-000000000001', + artifact_digest='a' * 64, + certificate=CertificateFacts( + verification=CertificateVerification.VALID, + runtime_profile=None, + certificate_id='issuer', + ), + ), + ) + assert decision.disposition is AdmissionDisposition.ADMINISTRATOR_FORCE_REQUIRED + + def test_archive_inspection_preserves_legacy_tuple_and_exposes_certificate_facts() -> None: from langbot.pkg.plugin.archive import ( ArchiveCertificateState,