From 1fde3b4283bead3335459f1004ea1127fd7c6081 Mon Sep 17 00:00:00 2001 From: RockChinQ <1010553892@qq.com> Date: Tue, 29 Sep 2026 00:56:18 +0800 Subject: [PATCH] fix(certification): align certified sharing and unsigned dedicated admission (#2610) Integrate certification policy, SDK/Core semantics, and documentation on canonical branch. Resolve duplicated documentation and keep invalid Cloud signatures rejected. --- docs/architecture/certified-plugins.md | 45 +++++++------ docs/pipeline-migration-config-map.zh-CN.md | 2 +- src/langbot/pkg/plugin/certification.py | 11 +++- src/langbot/pkg/plugin/connector.py | 5 +- .../plugin/test_certified_plugin_admission.py | 2 +- .../plugin/test_certified_plugin_policy.py | 64 +++++++++++++++++-- .../plugin/test_connector_methods.py | 1 + web/src/i18n/locales/zh-Hans.ts | 3 +- web/src/i18n/locales/zh-Hant.ts | 3 +- 9 files changed, 103 insertions(+), 33 deletions(-) diff --git a/docs/architecture/certified-plugins.md b/docs/architecture/certified-plugins.md index 7af3bd4f4..8dc54ae88 100644 --- a/docs/architecture/certified-plugins.md +++ b/docs/architecture/certified-plugins.md @@ -17,7 +17,10 @@ metadata. ## Trusted issuer configuration -Configure the non-secret Ed25519 public-key ring in `data/config.yaml`: +Hosted Cloud provisions the non-secret Ed25519 issuer public-key ring out of +band. Key IDs must match the SDK envelope; values are base64 public keys, never +private signing keys. Invalid configuration fails closed; keep old issuer keys +through rotation while their signed archives remain installed. ```yaml plugin: @@ -39,10 +42,11 @@ PLUGIN__CERTIFICATION__TRUSTED_PUBLIC_KEYS_JSON='{"ed25519:issuer":""}' ``` An **empty** ring is a supported state, not a misconfiguration. OSS defaults to -it, so a self-hosted instance that has not provisioned any issuer key still -installs packages (see the admission matrix below). Configure the ring to grant -the shared-runtime profile; leave it empty to keep every package on the -dedicated profile. +it and supports one Workspace; configuring certification keys on OSS is not a +supported way to enable cross-tenant sharing. Cloud provisions the ring to grant +shared placement only to eligible v2 artifacts. Certification means eligibility +for Cloud cross-tenant use of the same Worker and the same plugin/component +singleton, never a dedicated certificate or intermediate trust tier. ## Admission matrix @@ -52,21 +56,20 @@ dedicated profile. | 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` | -| OSS | declaration signed by a **key this instance resolves** | true | admitted to the dedicated profile | +| OSS | valid envelope declaring `shared-runtime-v1` + `stateless-v1` | any | dedicated only; OSS has no cross-tenant shared placement | +| OSS | invalid declaration referencing a **key this instance resolves** | false | reject with `CERTIFIED_PLUGIN_OSS_FORCE_REQUIRED` | +| OSS | invalid declaration referencing 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 | -The OSS row that matters for availability is the last one. Marketplace -packages are signed by the marketplace issuer and declare -`shared-runtime-v1`, while OSS ships an empty key ring by default. Treating that -as a rejection made every certified marketplace package uninstallable with -`CERTIFIED_PLUGIN_OSS_FORCE_REQUIRED` before artifact storage. Because the -certificate is signed by an issuer the instance does not declare trusted, no -shared-runtime privilege may be granted, so admission degrades the install to -the existing `oss_dev` dedicated profile and records -`CERTIFIED_PLUGIN_OSS_UNTRUSTED_DEDICATED`. This is not an escalation: it -withholds the shared profile rather than granting it. +There is no dedicated certification, dedicated signature, or intermediate +certification trust tier. Advisory review is an internal prerequisite, not an +installation trust status. An `issued` marketplace badge, source candidate, +matching version, or shared artifact/dependency tree alone does not prove Cloud +admission or Worker sharing. Compare the downloaded archive's ZIP comment, +normalized digest and signed claims with the public version record and deployed +Core trust-key ring. Confirm two Workspace bindings share one Worker PID, one +plugin object, and one object for each declared component before claiming live +cross-tenant sharing. A declaration is "resolvable" only when its `key_id` is present in the configured ring. A parseable declaration from a resolved key that fails @@ -78,7 +81,11 @@ resolved certificate identity are treated as untrusted and stay dedicated. install request carries boolean `true`. The local upload endpoint accepts the multipart field `administrator_force=true`; GitHub and marketplace install payloads carry the same field. The existing resource-manage authorization fence -protects those endpoints. A force never creates a Cloud dedicated fallback. +protects those endpoints. A force never creates a Cloud dedicated fallback or +certifies an invalid signature. A legacy v1 certificate may verify +cryptographically, but without a signed stateless component claim it is +rejected in Cloud; an old marketplace `issued` badge alone grants no placement. +Unsigned Cloud archives are the dedicated fallback. ## Runtime and logs diff --git a/docs/pipeline-migration-config-map.zh-CN.md b/docs/pipeline-migration-config-map.zh-CN.md index 672a6c8c7..482a6bbeb 100644 --- a/docs/pipeline-migration-config-map.zh-CN.md +++ b/docs/pipeline-migration-config-map.zh-CN.md @@ -26,7 +26,7 @@ ## 插件版本要求 -以下是迁移器要求的已发布 Certified Plugin 版本。旧版本即使名称相同,也不能作为当前 Cloud 迁移能力的证明。 +以下是迁移器要求的插件最低版本,不是认证版本清单。已发布的专属运行版本不等于已认证可跨工作区共享;旧版本即使名称相同,也不能作为当前 Cloud 迁移能力的证明。认证须由精确制品的有效签名及 `shared-runtime-v1`、`stateless-v1` 声明证明。 - `local-agent` → `LocalAgent` **0.1.10**。 - `dify-service-api` → `DifyAgent` **0.1.10**。 diff --git a/src/langbot/pkg/plugin/certification.py b/src/langbot/pkg/plugin/certification.py index f5a73674c..b6eabaf0f 100644 --- a/src/langbot/pkg/plugin/certification.py +++ b/src/langbot/pkg/plugin/certification.py @@ -199,7 +199,7 @@ def decide_plugin_admission( mode = DeploymentMode(deployment) certificate = facts.certificate - if certificate.is_valid_shared_runtime: + if mode is DeploymentMode.CLOUD and certificate.is_valid_shared_runtime: return PluginAdmissionDecision( AdmissionDisposition.SHARED_ELIGIBLE, AdmissionCode.SHARED_ELIGIBLE, @@ -213,6 +213,15 @@ def decide_plugin_admission( DEDICATED_RUNTIME, ) + # OSS has no cross-tenant shared placement; a verified claim stays dedicated + # there without inventing a dedicated certification tier. + if mode is DeploymentMode.OSS and certificate.is_valid_shared_runtime: + return PluginAdmissionDecision( + AdmissionDisposition.DEDICATED_ALLOWED, + AdmissionCode.OSS_UNTRUSTED_DEDICATED, + DEDICATED_RUNTIME, + ) + if mode is DeploymentMode.CLOUD: return PluginAdmissionDecision( AdmissionDisposition.REJECTED, AdmissionCode.CLOUD_CERTIFICATE_INVALID, DEDICATED_RUNTIME diff --git a/src/langbot/pkg/plugin/connector.py b/src/langbot/pkg/plugin/connector.py index 4ae024792..c453e016a 100644 --- a/src/langbot/pkg/plugin/connector.py +++ b/src/langbot/pkg/plugin/connector.py @@ -345,8 +345,9 @@ class PluginRuntimeConnector(ManagedRuntimeConnector): artifact_digest=setting.artifact_digest, ) - @staticmethod - def _execution_mode_from_setting(setting: persistence_plugin.PluginSetting) -> PluginExecutionMode: + def _execution_mode_from_setting(self, setting: persistence_plugin.PluginSetting) -> PluginExecutionMode: + if getattr(getattr(self.ap, 'deployment', None), 'mode', 'oss') != 'cloud': + return PluginExecutionMode.DEDICATED return execution_mode_for_persisted_installation( artifact_digest=setting.artifact_digest, install_info=setting.install_info, diff --git a/tests/integration/plugin/test_certified_plugin_admission.py b/tests/integration/plugin/test_certified_plugin_admission.py index ce9b5eb62..74cd4a2bd 100644 --- a/tests/integration/plugin/test_certified_plugin_admission.py +++ b/tests/integration/plugin/test_certified_plugin_admission.py @@ -30,7 +30,7 @@ pytestmark = pytest.mark.integration [ ('cloud', 'signed_shared', False, 'shared-runtime-v1'), ('cloud', 'legacy', False, 'dedicated'), - ('oss', 'signed_shared', False, 'shared-runtime-v1'), + ('oss', 'signed_shared', False, 'dedicated'), ('oss', 'legacy', False, 'dedicated'), ('oss', 'forged_shared', True, 'dedicated'), ], diff --git a/tests/unit_tests/plugin/test_certified_plugin_policy.py b/tests/unit_tests/plugin/test_certified_plugin_policy.py index f2bf45fd7..29d0ae22f 100644 --- a/tests/unit_tests/plugin/test_certified_plugin_policy.py +++ b/tests/unit_tests/plugin/test_certified_plugin_policy.py @@ -12,7 +12,14 @@ from langbot_plugin.entities.io.context import PluginExecutionMode @pytest.mark.parametrize( ('deployment', 'certificate', 'certificate_id', 'force', 'expected_disposition', 'expected_code'), [ - ('cloud', ('valid', 'shared-runtime-v1'), 'issuer', False, 'shared_eligible', 'CERTIFIED_PLUGIN_SHARED_ELIGIBLE'), + ( + 'cloud', + ('valid', 'shared-runtime-v1'), + 'issuer', + False, + 'shared_eligible', + 'CERTIFIED_PLUGIN_SHARED_ELIGIBLE', + ), ('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'), @@ -25,7 +32,14 @@ from langbot_plugin.entities.io.context import PluginExecutionMode 'CERTIFIED_PLUGIN_CLOUD_CERTIFICATE_INVALID', ), ('oss', ('absent', None), None, False, 'dedicated_allowed', 'CERTIFIED_PLUGIN_OSS_LEGACY_DEDICATED'), - ('oss', ('valid', 'shared-runtime-v1'), 'issuer', False, 'shared_eligible', 'CERTIFIED_PLUGIN_SHARED_ELIGIBLE'), + ( + 'oss', + ('valid', 'shared-runtime-v1'), + 'issuer', + False, + 'dedicated_allowed', + 'CERTIFIED_PLUGIN_OSS_UNTRUSTED_DEDICATED', + ), ( 'oss', ('invalid', 'shared-runtime-v1'), @@ -87,9 +101,7 @@ def test_admission_policy_enforces_certification_matrix( verification=CertificateVerification(verification), runtime_profile=runtime_profile, component_model=( - 'stateless-v1' - if verification == 'valid' and runtime_profile == 'shared-runtime-v1' - else None + 'stateless-v1' if verification == 'valid' and runtime_profile == 'shared-runtime-v1' else None ), certificate_id=certificate_id, ), @@ -135,10 +147,48 @@ def test_legacy_shared_certificate_without_stateless_contract_is_not_shared() -> assert decision.disposition is AdmissionDisposition.REJECTED +def test_oss_never_selects_shared_even_with_a_resolved_valid_certificate() -> None: + from langbot.pkg.plugin.certification import ( + CertificateFacts, + CertificateVerification, + PluginCertificationFacts, + decide_plugin_admission, + ) + + facts = PluginCertificationFacts( + 'installation', + 'a' * 64, + CertificateFacts( + CertificateVerification.VALID, + 'shared-runtime-v1', + 'stateless-v1', + 'issuer', + ), + ) + decision = decide_plugin_admission(deployment='oss', facts=facts) + assert decision.runtime_profile == 'dedicated' + assert decision.disposition.value == 'dedicated_allowed' + + +def test_oss_reconcile_never_reuses_persisted_shared_placement() -> None: + from types import SimpleNamespace + from langbot.pkg.plugin.connector import PluginRuntimeConnector + + connector = object.__new__(PluginRuntimeConnector) + connector.ap = SimpleNamespace(deployment=SimpleNamespace(mode='oss')) + setting = SimpleNamespace( + artifact_digest='a' * 64, install_info={'_certification': _complete_persisted_certification()} + ) + assert connector._execution_mode_from_setting(setting) is PluginExecutionMode.DEDICATED + + 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, + AdmissionDisposition, + CertificateFacts, + CertificateVerification, + PluginCertificationFacts, + decide_plugin_admission, ) decision = decide_plugin_admission( diff --git a/tests/unit_tests/plugin/test_connector_methods.py b/tests/unit_tests/plugin/test_connector_methods.py index 99e56bd41..c971324ba 100644 --- a/tests/unit_tests/plugin/test_connector_methods.py +++ b/tests/unit_tests/plugin/test_connector_methods.py @@ -749,6 +749,7 @@ class TestSetPluginConfig: """Config changes are fenced by a new runtime revision.""" get_connector_module() connector = create_mock_connector() + connector.ap.deployment.mode = 'cloud' configure_handler(connector, AsyncMock()) connector.handler.register_installation_binding = Mock() diff --git a/web/src/i18n/locales/zh-Hans.ts b/web/src/i18n/locales/zh-Hans.ts index d206bea70..aa8fd2f9e 100644 --- a/web/src/i18n/locales/zh-Hans.ts +++ b/web/src/i18n/locales/zh-Hans.ts @@ -2175,7 +2175,8 @@ const zhHans = { totpRecoveryCodesRegenerated: '已生成新的恢复代码', totpStatusDisabled: '未启用', totpCodesRemaining: '剩余 {{count}} 个恢复代码', - totpManagerSectionDesc: '所有者和管理员可以查看并重置本工作区账户的两步验证。', + totpManagerSectionDesc: + '所有者和管理员可以查看并重置本工作区账户的两步验证。', revokeTotp: '重新绑定', totpAdminResetTitle: '重新绑定 {{user}} 的两步验证', totpAdminResetDesc: diff --git a/web/src/i18n/locales/zh-Hant.ts b/web/src/i18n/locales/zh-Hant.ts index c6dd57b15..438577fc4 100644 --- a/web/src/i18n/locales/zh-Hant.ts +++ b/web/src/i18n/locales/zh-Hant.ts @@ -2172,7 +2172,8 @@ const zhHant = { totpRecoveryCodesRegenerated: '已產生新的恢復代碼', totpStatusDisabled: '未啟用', totpCodesRemaining: '剩餘 {{count}} 個恢復代碼', - totpManagerSectionDesc: '擁有者與管理員可以檢視並重設本工作區帳號的兩步驗證。', + totpManagerSectionDesc: + '擁有者與管理員可以檢視並重設本工作區帳號的兩步驗證。', revokeTotp: '重新綁定', totpAdminResetTitle: '重新綁定 {{user}} 的兩步驗證', totpAdminResetDesc: