mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-09-29 21:06:41 +08:00
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.
This commit is contained in:
@@ -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":"<base64>"}'
|
||||
```
|
||||
|
||||
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
|
||||
|
||||
|
||||
@@ -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**。
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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'),
|
||||
],
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -2175,7 +2175,8 @@ const zhHans = {
|
||||
totpRecoveryCodesRegenerated: '已生成新的恢复代码',
|
||||
totpStatusDisabled: '未启用',
|
||||
totpCodesRemaining: '剩余 {{count}} 个恢复代码',
|
||||
totpManagerSectionDesc: '所有者和管理员可以查看并重置本工作区账户的两步验证。',
|
||||
totpManagerSectionDesc:
|
||||
'所有者和管理员可以查看并重置本工作区账户的两步验证。',
|
||||
revokeTotp: '重新绑定',
|
||||
totpAdminResetTitle: '重新绑定 {{user}} 的两步验证',
|
||||
totpAdminResetDesc:
|
||||
|
||||
@@ -2172,7 +2172,8 @@ const zhHant = {
|
||||
totpRecoveryCodesRegenerated: '已產生新的恢復代碼',
|
||||
totpStatusDisabled: '未啟用',
|
||||
totpCodesRemaining: '剩餘 {{count}} 個恢復代碼',
|
||||
totpManagerSectionDesc: '擁有者與管理員可以檢視並重設本工作區帳號的兩步驗證。',
|
||||
totpManagerSectionDesc:
|
||||
'擁有者與管理員可以檢視並重設本工作區帳號的兩步驗證。',
|
||||
revokeTotp: '重新綁定',
|
||||
totpAdminResetTitle: '重新綁定 {{user}} 的兩步驗證',
|
||||
totpAdminResetDesc:
|
||||
|
||||
Reference in New Issue
Block a user