feat(plugin): enforce certified archive admission (#2553)

* feat(plugin): add certified admission policy

* feat(plugin): enforce certified archive admission

* chore(plugin): pin certified SDK beta

* fix(plugin): consume SDK beta 5

* style(plugin): format certification admission
This commit is contained in:
RockChinQ
2026-09-20 18:54:45 +08:00
committed by GitHub
parent 8f8356fe97
commit 20a04a77bf
14 changed files with 871 additions and 71 deletions
+1
View File
@@ -158,6 +158,7 @@ In this repo:
- `pkg/plugin/handler.py` exposes LangBot actions to the runtime and calls runtime actions for plugin operations. - `pkg/plugin/handler.py` exposes LangBot actions to the runtime and calls runtime actions for plugin operations.
- `pkg/provider/tools/loaders/plugin.py` exposes plugin Tool components to LLM runners. - `pkg/provider/tools/loaders/plugin.py` exposes plugin Tool components to LLM runners.
- Pipeline handlers emit SDK events such as normal-message events and prompt-processing events. - Pipeline handlers emit SDK events such as normal-message events and prompt-processing events.
- [Certified plugin policy](docs/architecture/certified-plugins.md) defines Core's archive-fact, admission, and tenant-log-visibility boundary; the SDK remains responsible for certificate verification.
In `langbot-plugin-sdk`: In `langbot-plugin-sdk`:
+74
View File
@@ -0,0 +1,74 @@
# Certified Plugins
## Admission boundary
Core verifies a plugin archive **before** artifact storage, `PluginSetting`
persistence, or a Plugin Runtime apply request. It calls the SDK public
`langbot_plugin.certification.verify_archive()` API, which reads the strict
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.
## Trusted issuer configuration
Configure the non-secret Ed25519 public-key ring in `data/config.yaml`:
```yaml
plugin:
certification:
trusted_public_keys:
issuer-2026-q3: "<base64 encoded 32-byte Ed25519 public key>"
```
Key IDs must match the SDK envelope. Values are standard base64 raw public
keys, not private/signing keys. An invalid key-ring configuration is rejected
rather than weakening verification. Keep active issuer keys during a rotation
until archives signed by retired IDs are no longer installed.
## Admission matrix
| Deployment | SDK verification | Explicit `administrator_force` | Result |
| --- | --- | --- | --- |
| Cloud | valid envelope declaring `shared-runtime-v1` | any | admitted to the shared 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 | malformed or invalid declaration | false | reject with `CERTIFIED_PLUGIN_OSS_FORCE_REQUIRED` |
| OSS | malformed or invalid declaration | true | admitted to the dedicated profile |
`administrator_force` is deliberately strict: it is recognized only when the
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.
## 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.
The existing public plugin-log boundary already applies the immutable
installation binding (including workspace UUID) through
`RuntimeConnectionHandler.installation_scope()` before requesting logs. This is
the actual tenant exposure boundary, so valid shared certificates use that
binding-scoped transport; Core does not invent a second log stream or expose
process-wide log output. Dedicated and invalid/legacy installations use the
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.
+1 -1
View File
@@ -71,7 +71,7 @@ dependencies = [
"langchain-text-splitters>=1.1.2", "langchain-text-splitters>=1.1.2",
"chromadb>=1.0.0,<2.0.0", "chromadb>=1.0.0,<2.0.0",
"qdrant-client (>=1.15.1,<2.0.0)", "qdrant-client (>=1.15.1,<2.0.0)",
"langbot-plugin==0.5.8", "langbot-plugin==0.6.0b5",
"asyncpg>=0.30.0", "asyncpg>=0.30.0",
"line-bot-sdk>=3.19.0", "line-bot-sdk>=3.19.0",
"matrix-nio>=0.25.2", "matrix-nio>=0.25.2",
@@ -923,10 +923,13 @@ class PluginsRouterGroup(group.RouterGroup):
return self.http_status(400, -1, 'file is required') return self.http_status(400, -1, 'file is required')
file_bytes = file.read() file_bytes = file.read()
form = await quart.request.form
administrator_force = form.get('administrator_force', '').strip().lower() == 'true'
execution_context = await self.ap.plugin_connector.require_workspace_context(request_context) execution_context = await self.ap.plugin_connector.require_workspace_context(request_context)
data = { data = {
'plugin_file': file_bytes, 'plugin_file': file_bytes,
'administrator_force': administrator_force,
} }
ctx = taskmgr.TaskContext.new() ctx = taskmgr.TaskContext.new()
@@ -42,6 +42,7 @@ _RUNTIME_POLICY_DEFAULTS = {
}, },
'plugin': { 'plugin': {
'connect_timeout_seconds': 180.0, 'connect_timeout_seconds': 180.0,
'certification': {'trusted_public_keys': {}},
'worker': { 'worker': {
'max_cpus': 1.0, 'max_cpus': 1.0,
'max_memory_mb': 512, 'max_memory_mb': 512,
+77 -7
View File
@@ -1,7 +1,10 @@
from __future__ import annotations from __future__ import annotations
import hashlib
import io import io
import zipfile import zipfile
from dataclasses import dataclass
from enum import Enum
import yaml import yaml
@@ -14,6 +17,34 @@ _PLUGIN_METADATA_MAX_BYTES = 1024 * 1024
_PLUGIN_REQUIREMENTS_MAX_ENTRIES = 1000 _PLUGIN_REQUIREMENTS_MAX_ENTRIES = 1000
class ArchiveCertificateState(str, Enum):
"""Syntactic certificate declaration state; this is not verification."""
ABSENT = 'absent'
MALFORMED = 'malformed'
DECLARED = 'declared'
@dataclass(frozen=True)
class ArchiveCertificateDeclaration:
"""Bounded, verifier-facing certificate declaration from ``manifest.yaml``."""
state: ArchiveCertificateState
runtime_profile: str | None = None
payload: dict[str, object] | None = None
@dataclass(frozen=True)
class PluginArchiveInspection:
"""Validated archive metadata plus unverified certificate declaration facts."""
manifest: dict
requirements: list[str]
names: list[str]
artifact_digest: str
certificate: ArchiveCertificateDeclaration
def _read_plugin_archive_member( def _read_plugin_archive_member(
archive: zipfile.ZipFile, archive: zipfile.ZipFile,
member: zipfile.ZipInfo, member: zipfile.ZipInfo,
@@ -29,12 +60,30 @@ def _read_plugin_archive_member(
return content return content
def inspect_plugin_archive_metadata( def _inspect_certificate_declaration(manifest: dict) -> ArchiveCertificateDeclaration:
file_bytes: bytes, declaration = manifest.get('certification')
*, if declaration is None:
require_manifest: bool = True, return ArchiveCertificateDeclaration(ArchiveCertificateState.ABSENT)
) -> tuple[dict, list[str], list[str]]: if not isinstance(declaration, dict):
"""Validate archive size metadata and read only bounded preview fields.""" return ArchiveCertificateDeclaration(ArchiveCertificateState.MALFORMED)
runtime_profile = declaration.get('runtime_profile')
payload = declaration.get('certificate')
if not isinstance(runtime_profile, str) or not runtime_profile.strip() or not isinstance(payload, dict):
return ArchiveCertificateDeclaration(ArchiveCertificateState.MALFORMED)
return ArchiveCertificateDeclaration(
ArchiveCertificateState.DECLARED,
runtime_profile=runtime_profile,
payload=payload,
)
def inspect_plugin_archive(file_bytes: bytes, *, require_manifest: bool = True) -> PluginArchiveInspection:
"""Validate an archive and expose certificate declaration facts for a verifier.
Certificate signatures and issuer trust are deliberately not evaluated here;
callers must pass the declaration and artifact digest to an SDK verifier.
"""
with zipfile.ZipFile(io.BytesIO(file_bytes)) as archive: with zipfile.ZipFile(io.BytesIO(file_bytes)) as archive:
members = archive.infolist() members = archive.infolist()
@@ -95,4 +144,25 @@ def inspect_plugin_archive_metadata(
for line in content.splitlines() for line in content.splitlines()
if line.strip() and not line.strip().startswith('#') if line.strip() and not line.strip().startswith('#')
][:_PLUGIN_REQUIREMENTS_MAX_ENTRIES] ][:_PLUGIN_REQUIREMENTS_MAX_ENTRIES]
return manifest, requirements, names
return PluginArchiveInspection(
manifest=manifest,
requirements=requirements,
names=names,
artifact_digest=hashlib.sha256(file_bytes).hexdigest(),
certificate=_inspect_certificate_declaration(manifest),
)
def inspect_plugin_archive_metadata(
file_bytes: bytes,
*,
require_manifest: bool = True,
) -> tuple[dict, list[str], list[str]]:
"""Legacy tuple API for archive metadata callers.
Use ``inspect_plugin_archive`` when certificate declaration facts are needed.
"""
inspection = inspect_plugin_archive(file_bytes, require_manifest=require_manifest)
return inspection.manifest, inspection.requirements, inspection.names
+234
View File
@@ -0,0 +1,234 @@
"""Pure certified-plugin facts and admission policies.
This module intentionally does not verify signatures. An SDK-backed verifier
must produce ``CertificateFacts`` from an inspected archive before admission.
"""
from __future__ import annotations
import base64
from collections.abc import Callable, Mapping
from dataclasses import dataclass
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
SHARED_RUNTIME_V1 = 'shared-runtime-v1'
DEDICATED_RUNTIME = 'dedicated'
class CertificateVerification(str, Enum):
ABSENT = 'absent'
MALFORMED = 'malformed'
INVALID = 'invalid'
VALID = 'valid'
class DeploymentMode(str, Enum):
CLOUD = 'cloud'
OSS = 'oss'
class AdmissionDisposition(str, Enum):
SHARED_ELIGIBLE = 'shared_eligible'
DEDICATED_ALLOWED = 'dedicated_allowed'
REJECTED = 'rejected'
ADMINISTRATOR_FORCE_REQUIRED = 'administrator_force_required'
class AdmissionCode(str, Enum):
SHARED_ELIGIBLE = 'CERTIFIED_PLUGIN_SHARED_ELIGIBLE'
CLOUD_CERTIFICATE_REQUIRED = 'CERTIFIED_PLUGIN_CLOUD_CERTIFICATE_REQUIRED'
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'
class PluginLogVisibility(str, Enum):
TENANT_SCOPED = 'tenant_scoped'
DETAILED_PROCESS = 'detailed_process'
@dataclass(frozen=True)
class CertificateFacts:
"""Certificate result supplied by an archive verifier.
``VALID`` means the verifier has validated both the certificate and its
binding to the immutable artifact digest in ``PluginCertificationFacts``.
"""
verification: CertificateVerification
runtime_profile: 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
@property
def is_declared(self) -> bool:
return self.verification is not CertificateVerification.ABSENT
@dataclass(frozen=True)
class PluginCertificationFacts:
"""Immutable Core-side facts for one plugin installation artifact."""
installation_uuid: str
artifact_digest: str
certificate: CertificateFacts
def __post_init__(self) -> None:
if len(self.artifact_digest) != 64 or any(
character not in '0123456789abcdef' for character in self.artifact_digest.lower()
):
raise ValueError('artifact_digest must be a lowercase-or-uppercase SHA-256 hex digest')
@dataclass(frozen=True)
class VerifiedArchiveCertificate:
"""SDK verification facts bound to the comment-normalized ZIP digest."""
normalized_digest: str
certificate: CertificateFacts
def for_installation(self, installation_uuid: str) -> PluginCertificationFacts:
return PluginCertificationFacts(
installation_uuid=installation_uuid,
artifact_digest=self.normalized_digest,
certificate=self.certificate,
)
def trusted_public_key_ring(config: object) -> dict[str, Callable[[bytes, bytes], bool]]:
"""Build the non-secret Ed25519 verifier ring from instance configuration.
``plugin.certification.trusted_public_keys`` is a mapping of key IDs to
standard base64-encoded 32-byte Ed25519 public keys. Configuration errors
are explicit so an operator never silently gets a weaker trust policy.
"""
if config is None:
return {}
if not isinstance(config, Mapping):
raise ValueError('plugin.certification.trusted_public_keys must be a mapping')
ring: dict[str, Callable[[bytes, bytes], bool]] = {}
for raw_key_id, raw_public_key in config.items():
key_id = str(raw_key_id).strip()
if not key_id or not isinstance(raw_public_key, str):
raise ValueError('plugin.certification.trusted_public_keys entries must have string IDs and values')
try:
public_key_bytes = base64.b64decode(raw_public_key.encode('ascii'), validate=True)
public_key = Ed25519PublicKey.from_public_bytes(public_key_bytes)
except (UnicodeEncodeError, ValueError) as exc:
raise ValueError(f'plugin.certification trusted public key {key_id!r} is invalid') from exc
def verify(payload: bytes, signature: bytes, *, verifier: Ed25519PublicKey = public_key) -> bool:
try:
verifier.verify(signature, payload)
except (InvalidSignature, TypeError, ValueError):
return False
return True
ring[key_id] = verify
return ring
def verify_plugin_archive_certificate(
archive: bytes,
*,
trusted_public_keys: object,
) -> VerifiedArchiveCertificate:
"""Use the SDK ZIP-comment API and retain its normalized-digest binding."""
verification = verify_archive(archive, trusted_public_key_ring(trusted_public_keys).get)
envelope = verification.envelope
runtime_profile = envelope.shared_runtime if envelope is not None else None
certificate_id = envelope.key_id if envelope is not None else None
state = {
'absent': CertificateVerification.ABSENT,
'malformed': CertificateVerification.MALFORMED,
'valid': CertificateVerification.VALID,
}.get(verification.status, CertificateVerification.INVALID)
return VerifiedArchiveCertificate(
normalized_digest=normalized_zip_digest(archive),
certificate=CertificateFacts(
verification=state,
runtime_profile=runtime_profile,
certificate_id=certificate_id,
),
)
@dataclass(frozen=True)
class PluginAdmissionDecision:
disposition: AdmissionDisposition
code: AdmissionCode
runtime_profile: str
def decide_plugin_admission(
*,
deployment: DeploymentMode | str,
facts: PluginCertificationFacts,
administrator_force: bool = False,
) -> PluginAdmissionDecision:
"""Apply Cloud fail-closed and OSS administrator-force admission rules."""
mode = DeploymentMode(deployment)
certificate = facts.certificate
if certificate.is_valid_shared_runtime:
return PluginAdmissionDecision(
AdmissionDisposition.SHARED_ELIGIBLE,
AdmissionCode.SHARED_ELIGIBLE,
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,
DEDICATED_RUNTIME,
)
if certificate.verification is CertificateVerification.VALID:
return PluginAdmissionDecision(
AdmissionDisposition.DEDICATED_ALLOWED,
AdmissionCode.OSS_CERTIFIED_DEDICATED,
DEDICATED_RUNTIME,
)
if administrator_force:
return PluginAdmissionDecision(
AdmissionDisposition.DEDICATED_ALLOWED,
AdmissionCode.OSS_FORCED_DEDICATED,
DEDICATED_RUNTIME,
)
return PluginAdmissionDecision(
AdmissionDisposition.ADMINISTRATOR_FORCE_REQUIRED,
AdmissionCode.OSS_FORCE_REQUIRED,
DEDICATED_RUNTIME,
)
def decide_plugin_log_visibility(facts: PluginCertificationFacts) -> PluginLogVisibility:
"""Select the minimum log visibility compatible with a verified shared runtime."""
if facts.certificate.is_valid_shared_runtime:
return PluginLogVisibility.TENANT_SCOPED
return PluginLogVisibility.DETAILED_PROCESS
+50
View File
@@ -22,6 +22,13 @@ from langbot_plugin.api.entities.builtin.pipeline.query import provider_session
from ..core import app from ..core import app
from . import handler from . import handler
from .archive import inspect_plugin_archive_metadata from .archive import inspect_plugin_archive_metadata
from .certification import (
AdmissionDisposition,
PluginCertificationFacts,
VerifiedArchiveCertificate,
decide_plugin_admission,
verify_plugin_archive_certificate,
)
from .github import ( from .github import (
validate_github_plugin_install_info, validate_github_plugin_install_info,
validate_github_release_asset_url, validate_github_release_asset_url,
@@ -1683,6 +1690,45 @@ class PluginRuntimeConnector(ManagedRuntimeConnector):
) )
return plugin_package, latest_version return plugin_package, latest_version
def _admit_plugin_archive(
self,
file_bytes: bytes,
install_info: dict[str, Any],
) -> tuple[dict[str, Any], VerifiedArchiveCertificate]:
"""Verify and admit one archive before it can reach durable storage or Runtime."""
certification_config = self.ap.instance_config.data.get('plugin', {}).get('certification', {})
if not isinstance(certification_config, dict):
raise ValueError('plugin.certification must be a mapping')
verified = verify_plugin_archive_certificate(
file_bytes,
trusted_public_keys=certification_config.get('trusted_public_keys', {}),
)
facts = PluginCertificationFacts(
installation_uuid='pending-installation',
artifact_digest=verified.normalized_digest,
certificate=verified.certificate,
)
decision = decide_plugin_admission(
deployment=getattr(getattr(self.ap, 'deployment', None), 'mode', 'oss'),
facts=facts,
administrator_force=install_info.get('administrator_force') is True,
)
if decision.disposition not in {
AdmissionDisposition.DEDICATED_ALLOWED,
AdmissionDisposition.SHARED_ELIGIBLE,
}:
raise ValueError(decision.code.value)
certification_info = {
'normalized_digest': facts.artifact_digest,
'verification': facts.certificate.verification.value,
'certificate_runtime_profile': facts.certificate.runtime_profile,
'certificate_id': facts.certificate.certificate_id,
'runtime_profile': decision.runtime_profile,
'admission_code': decision.code.value,
}
return {**install_info, '_certification': certification_info}, verified
async def install_plugin( async def install_plugin(
self, self,
install_source: PluginInstallSource, install_source: PluginInstallSource,
@@ -1719,6 +1765,7 @@ class PluginRuntimeConnector(ManagedRuntimeConnector):
else: else:
raise ValueError(f'Unsupported plugin install source: {install_source.value}') raise ValueError(f'Unsupported plugin install source: {install_source.value}')
install_info, verified_certificate = self._admit_plugin_archive(file_bytes, install_info)
manifest_author, manifest_name = self._inspect_plugin_package(file_bytes, task_context) manifest_author, manifest_name = self._inspect_plugin_package(file_bytes, task_context)
if not manifest_author or not manifest_name: if not manifest_author or not manifest_name:
raise ValueError('Plugin package manifest identity is missing') raise ValueError('Plugin package manifest identity is missing')
@@ -1749,6 +1796,9 @@ class PluginRuntimeConnector(ManagedRuntimeConnector):
plugin_author=plugin_author, plugin_author=plugin_author,
plugin_name=plugin_name, plugin_name=plugin_name,
) )
certification_facts = verified_certificate.for_installation(binding.installation_uuid)
if certification_facts.artifact_digest != install_info['_certification']['normalized_digest']:
raise RuntimeError('Plugin certification digest changed before Runtime apply')
await self._apply_desired_state( await self._apply_desired_state(
PluginInstallationDesiredState(binding=binding, enabled=True), PluginInstallationDesiredState(binding=binding, enabled=True),
artifact_package=file_bytes, artifact_package=file_bytes,
+6
View File
@@ -261,6 +261,12 @@ plugin:
runtime_ws_url: 'ws://langbot_plugin_runtime:5400/control/ws' runtime_ws_url: 'ws://langbot_plugin_runtime:5400/control/ws'
enable_marketplace: true enable_marketplace: true
display_plugin_debug_url: 'ws://localhost:5401/plugin/debug/ws' display_plugin_debug_url: 'ws://localhost:5401/plugin/debug/ws'
certification:
# Non-secret Ed25519 issuer key ring used to verify the SDK ZIP-comment
# certification envelope. Values are standard base64-encoded raw public
# keys; add keys during issuer rotation and remove retired IDs only after
# every affected archive has been upgraded.
trusted_public_keys: {}
worker: worker:
# Instance-wide maximum for every plugin installation. Plugin # Instance-wide maximum for every plugin installation. Plugin
# manifests cannot raise or override these limits. # manifests cannot raise or override these limits.
@@ -3,11 +3,13 @@
from __future__ import annotations from __future__ import annotations
import copy import copy
import io
from types import SimpleNamespace from types import SimpleNamespace
from unittest.mock import AsyncMock, Mock, call from unittest.mock import AsyncMock, Mock, call
import pytest import pytest
import quart import quart
from quart.datastructures import FileStorage
pytestmark = pytest.mark.integration pytestmark = pytest.mark.integration
@@ -287,3 +289,34 @@ async def test_github_install_rejects_internal_asset_url_before_task_creation(
assert response.status_code == 400 assert response.status_code == 400
assert 'HTTPS GitHub release asset URL' in (await response.get_json())['msg'] assert 'HTTPS GitHub release asset URL' in (await response.get_json())['msg']
application.task_mgr.create_user_task.assert_not_called() application.task_mgr.create_user_task.assert_not_called()
@pytest.mark.asyncio
async def test_local_install_forwards_explicit_administrator_force(plugin_security_api):
application, client, _ = plugin_security_api
execution_context = SimpleNamespace(
instance_uuid='instance-test',
workspace_uuid=WORKSPACE_UUID,
placement_generation=1,
)
application.persistence_mgr.tenant_scope = None
application.plugin_connector.require_workspace_context = AsyncMock(return_value=execution_context)
application.plugin_connector.install_plugin = AsyncMock()
application.task_mgr.create_user_task = Mock(return_value=SimpleNamespace(id='task-certification'))
response = await client.post(
'/api/v1/plugins/install/local',
headers=_headers('manager-token'),
files={
'file': FileStorage(stream=io.BytesIO(b'archive'), filename='plugin.lbpkg'),
},
form={'administrator_force': 'true'},
)
assert response.status_code == 200
operation = application.task_mgr.create_user_task.call_args.args[0]
await operation
assert application.plugin_connector.install_plugin.await_args.args[1] == {
'plugin_file': b'archive',
'administrator_force': True,
}
@@ -0,0 +1,169 @@
"""Certified archive admission through the public Core installation API."""
from __future__ import annotations
import base64
import hashlib
import io
import zipfile
from types import SimpleNamespace
from unittest.mock import AsyncMock, Mock
import pytest
import yaml
from cryptography.hazmat.primitives import serialization
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.runtime.plugin.mgr import PluginInstallSource
pytestmark = pytest.mark.integration
@pytest.mark.asyncio
@pytest.mark.parametrize(
('deployment', 'archive_kind', 'administrator_force', 'expected_profile'),
[
('cloud', 'signed_shared', False, 'shared-runtime-v1'),
('oss', 'signed_shared', False, 'shared-runtime-v1'),
('oss', 'legacy', False, 'dedicated'),
('oss', 'invalid_shared', True, 'dedicated'),
],
)
async def test_install_plugin_admits_archive_before_persistence_and_applies_selected_profile(
deployment: str,
archive_kind: str,
administrator_force: bool,
expected_profile: str,
) -> None:
package, trusted_public_keys = _archive(archive_kind)
connector, execution_context, binding = _connector(deployment, trusted_public_keys)
await connector.install_plugin(
PluginInstallSource.LOCAL,
{
'plugin_file': package,
'administrator_force': administrator_force,
},
)
connector._store_artifact_package.assert_awaited_once_with(
execution_context,
hashlib.sha256(package).hexdigest(),
package,
)
persisted_info = connector._persist_installation_package.await_args.kwargs['install_info']
assert persisted_info['_certification']['runtime_profile'] == expected_profile
assert persisted_info['_certification']['normalized_digest'] == _normalized_digest(package)
connector.handler.apply_plugin_installation.assert_awaited_once_with(
binding,
artifact_package=package,
enabled=True,
)
@pytest.mark.asyncio
@pytest.mark.parametrize('archive_kind', ['legacy', '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)
with pytest.raises(ValueError, match='CERTIFIED_PLUGIN_CLOUD_CERTIFICATE_'):
await connector.install_plugin(PluginInstallSource.LOCAL, {'plugin_file': package})
connector._store_artifact_package.assert_not_awaited()
connector._persist_installation_package.assert_not_awaited()
connector.handler.apply_plugin_installation.assert_not_awaited()
@pytest.mark.asyncio
async def test_oss_requires_explicit_administrator_force_for_declared_invalid_archive() -> None:
package, trusted_public_keys = _archive('invalid_shared')
connector, _execution_context, _binding = _connector('oss', trusted_public_keys)
with pytest.raises(ValueError, match='CERTIFIED_PLUGIN_OSS_FORCE_REQUIRED'):
await connector.install_plugin(PluginInstallSource.LOCAL, {'plugin_file': package})
connector._store_artifact_package.assert_not_awaited()
connector._persist_installation_package.assert_not_awaited()
connector.handler.apply_plugin_installation.assert_not_awaited()
def _connector(deployment: str, trusted_public_keys: dict[str, str]):
package_digest = 'a' * 64
execution_context = ExecutionContext(
instance_uuid='instance-a',
workspace_uuid='workspace-a',
placement_generation=1,
)
binding = InstallationBinding(
instance_uuid='instance-a',
workspace_uuid='workspace-a',
placement_generation=1,
installation_uuid='00000000-0000-4000-8000-000000000001',
runtime_revision=1,
artifact_digest=package_digest,
)
app = SimpleNamespace(
instance_config=SimpleNamespace(
data={
'plugin': {
'enable': True,
'certification': {'trusted_public_keys': trusted_public_keys},
}
}
),
deployment=SimpleNamespace(mode=deployment),
logger=Mock(),
)
connector = PluginRuntimeConnector(app, AsyncMock())
connector.handler = SimpleNamespace(
register_installation_binding=Mock(),
apply_plugin_installation=AsyncMock(return_value={'state': 'running'}),
)
connector._current_execution_context = AsyncMock(return_value=execution_context)
connector._store_artifact_package = AsyncMock()
connector._persist_installation_package = AsyncMock(return_value=(binding, None, False))
connector._wait_for_installed_plugin_ready = AsyncMock()
return connector, execution_context, binding
def _archive(kind: str) -> tuple[bytes, dict[str, str]]:
manifest = {
'metadata': {'author': 'certified', 'name': 'example', 'version': '1.0.0'},
'execution': {'sharedRuntime': 'shared-runtime-v1'},
}
if kind == 'legacy':
manifest.pop('execution')
archive = io.BytesIO()
with zipfile.ZipFile(archive, 'w') as package:
package.writestr('manifest.yaml', yaml.safe_dump(manifest))
raw_archive = archive.getvalue()
if kind == 'legacy':
return raw_archive, {}
from langbot_plugin.certification import create_envelope, write_envelope
signing_key = Ed25519PrivateKey.generate()
signed_archive = write_envelope(
raw_archive,
create_envelope(
raw_archive,
'wrong-key' if kind == 'invalid_shared' else 'ephemeral',
signing_key.sign,
),
)
trusted_key = signing_key.public_key().public_bytes(
serialization.Encoding.Raw,
serialization.PublicFormat.Raw,
)
return signed_archive, {'ephemeral': base64.b64encode(trusted_key).decode('ascii')}
def _normalized_digest(archive: bytes) -> str:
from langbot_plugin.certification import normalized_zip_digest
return normalized_zip_digest(archive)
@@ -0,0 +1,153 @@
from __future__ import annotations
import hashlib
import io
import zipfile
import pytest
@pytest.mark.parametrize(
('deployment', 'certificate', 'force', 'expected_disposition', 'expected_code'),
[
('cloud', ('valid', 'shared-runtime-v1'), False, 'shared_eligible', 'CERTIFIED_PLUGIN_SHARED_ELIGIBLE'),
('cloud', ('absent', None), False, 'rejected', 'CERTIFIED_PLUGIN_CLOUD_CERTIFICATE_REQUIRED'),
('cloud', ('malformed', None), False, 'rejected', 'CERTIFIED_PLUGIN_CLOUD_CERTIFICATE_INVALID'),
('cloud', ('invalid', 'shared-runtime-v1'), True, 'rejected', 'CERTIFIED_PLUGIN_CLOUD_CERTIFICATE_INVALID'),
('oss', ('absent', None), False, 'dedicated_allowed', 'CERTIFIED_PLUGIN_OSS_LEGACY_DEDICATED'),
('oss', ('valid', 'shared-runtime-v1'), False, 'shared_eligible', 'CERTIFIED_PLUGIN_SHARED_ELIGIBLE'),
('oss', ('invalid', 'shared-runtime-v1'), False, 'administrator_force_required', 'CERTIFIED_PLUGIN_OSS_FORCE_REQUIRED'),
('oss', ('invalid', 'shared-runtime-v1'), True, 'dedicated_allowed', 'CERTIFIED_PLUGIN_OSS_FORCED_DEDICATED'),
],
)
def test_admission_policy_enforces_certification_matrix(
deployment: str,
certificate: tuple[str, str | None],
force: bool,
expected_disposition: str,
expected_code: str,
) -> None:
from langbot.pkg.plugin.certification import (
CertificateFacts,
CertificateVerification,
PluginCertificationFacts,
decide_plugin_admission,
)
verification, runtime_profile = certificate
facts = PluginCertificationFacts(
installation_uuid='00000000-0000-4000-8000-000000000001',
artifact_digest='a' * 64,
certificate=CertificateFacts(
verification=CertificateVerification(verification),
runtime_profile=runtime_profile,
),
)
decision = decide_plugin_admission(
deployment=deployment,
facts=facts,
administrator_force=force,
)
assert decision.disposition.value == expected_disposition
assert decision.code.value == expected_code
assert decision.runtime_profile == (
'shared-runtime-v1' if expected_disposition == 'shared_eligible' else 'dedicated'
)
def test_archive_inspection_preserves_legacy_tuple_and_exposes_certificate_facts() -> None:
from langbot.pkg.plugin.archive import (
ArchiveCertificateState,
inspect_plugin_archive,
inspect_plugin_archive_metadata,
)
manifest = {
'kind': 'Plugin',
'metadata': {'name': 'example'},
'certification': {
'runtime_profile': 'shared-runtime-v1',
'certificate': {'issuer': 'sdk-test', 'signature': 'not-verified-by-core'},
},
}
archive_bytes = _archive_bytes(manifest)
inspection = inspect_plugin_archive(archive_bytes)
assert inspection.artifact_digest == hashlib.sha256(archive_bytes).hexdigest()
assert inspection.certificate.state is ArchiveCertificateState.DECLARED
assert inspection.certificate.runtime_profile == 'shared-runtime-v1'
assert inspection.certificate.payload == manifest['certification']['certificate']
assert inspect_plugin_archive_metadata(archive_bytes) == (
inspection.manifest,
inspection.requirements,
inspection.names,
)
@pytest.mark.parametrize(
('certification', 'expected_state'),
[
(None, 'absent'),
({'runtime_profile': 123, 'certificate': {}}, 'malformed'),
({'runtime_profile': 'shared-runtime-v1'}, 'malformed'),
],
)
def test_archive_inspection_reports_nonverifying_certificate_states(
certification: object,
expected_state: str,
) -> None:
from langbot.pkg.plugin.archive import inspect_plugin_archive
manifest: dict[str, object] = {'kind': 'Plugin', 'metadata': {'name': 'example'}}
if certification is not None:
manifest['certification'] = certification
inspection = inspect_plugin_archive(_archive_bytes(manifest))
assert inspection.certificate.state.value == expected_state
@pytest.mark.parametrize(
('verification', 'expected_visibility'),
[
('valid', 'tenant_scoped'),
('absent', 'detailed_process'),
('malformed', 'detailed_process'),
('invalid', 'detailed_process'),
],
)
def test_log_visibility_policy_only_scopes_valid_shared_certifications(
verification: str,
expected_visibility: str,
) -> None:
from langbot.pkg.plugin.certification import (
CertificateFacts,
CertificateVerification,
PluginCertificationFacts,
decide_plugin_log_visibility,
)
facts = PluginCertificationFacts(
installation_uuid='00000000-0000-4000-8000-000000000001',
artifact_digest='a' * 64,
certificate=CertificateFacts(
verification=CertificateVerification(verification),
runtime_profile='shared-runtime-v1',
),
)
visibility = decide_plugin_log_visibility(facts)
assert visibility.value == expected_visibility
def _archive_bytes(manifest: dict[str, object]) -> bytes:
buffer = io.BytesIO()
with zipfile.ZipFile(buffer, 'w') as archive:
import yaml
archive.writestr('manifest.yaml', yaml.safe_dump(manifest))
return buffer.getvalue()
@@ -272,6 +272,12 @@ async def test_local_install_persists_verified_package_before_runtime_apply():
connector._inspect_plugin_package = Mock(return_value=('author', 'plugin')) connector._inspect_plugin_package = Mock(return_value=('author', 'plugin'))
connector._store_artifact_package = AsyncMock() connector._store_artifact_package = AsyncMock()
connector._persist_installation_package = AsyncMock(return_value=(binding, None, False)) connector._persist_installation_package = AsyncMock(return_value=(binding, None, False))
connector._admit_plugin_archive = Mock(
return_value=(
{'_certification': {'normalized_digest': digest}},
SimpleNamespace(for_installation=lambda _installation_uuid: SimpleNamespace(artifact_digest=digest)),
)
)
connector._wait_for_installed_plugin_ready = AsyncMock() connector._wait_for_installed_plugin_ready = AsyncMock()
await connector.install_plugin( await connector.install_plugin(
Generated
+63 -63
View File
@@ -1066,7 +1066,7 @@ name = "cuda-bindings"
version = "13.3.1" version = "13.3.1"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "cuda-pathfinder" }, { name = "cuda-pathfinder", marker = "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'" },
] ]
wheels = [ 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" }, { 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] [package.optional-dependencies]
cudart = [ cudart = [
{ name = "nvidia-cuda-runtime" }, { name = "nvidia-cuda-runtime", marker = "python_full_version >= '3.14' and sys_platform == 'linux'" },
] ]
cufft = [ cufft = [
{ name = "nvidia-cufft" }, { name = "nvidia-cufft", marker = "python_full_version >= '3.14' and sys_platform == 'linux'" },
] ]
cufile = [ cufile = [
{ name = "nvidia-cufile" }, { name = "nvidia-cufile", marker = "python_full_version >= '3.14' and sys_platform == 'linux'" },
] ]
cupti = [ cupti = [
{ name = "nvidia-cuda-cupti" }, { name = "nvidia-cuda-cupti", marker = "python_full_version >= '3.14' and sys_platform == 'linux'" },
] ]
curand = [ curand = [
{ name = "nvidia-curand" }, { name = "nvidia-curand", marker = "python_full_version >= '3.14' and sys_platform == 'linux'" },
] ]
cusolver = [ cusolver = [
{ name = "nvidia-cusolver" }, { name = "nvidia-cusolver", marker = "python_full_version >= '3.14' and sys_platform == 'linux'" },
] ]
cusparse = [ cusparse = [
{ name = "nvidia-cusparse" }, { name = "nvidia-cusparse", marker = "python_full_version >= '3.14' and sys_platform == 'linux'" },
] ]
nvjitlink = [ nvjitlink = [
{ name = "nvidia-nvjitlink" }, { name = "nvidia-nvjitlink", marker = "python_full_version >= '3.14' and sys_platform == 'linux'" },
] ]
nvrtc = [ nvrtc = [
{ name = "nvidia-cuda-nvrtc" }, { name = "nvidia-cuda-nvrtc", marker = "python_full_version >= '3.14' and sys_platform == 'linux'" },
] ]
nvtx = [ nvtx = [
{ name = "nvidia-nvtx" }, { name = "nvidia-nvtx", marker = "python_full_version >= '3.14' and sys_platform == 'linux'" },
] ]
[[package]] [[package]]
@@ -2180,7 +2180,7 @@ requires-dist = [
{ name = "ebooklib", specifier = ">=0.18" }, { name = "ebooklib", specifier = ">=0.18" },
{ name = "gewechat-client", specifier = ">=0.1.5" }, { name = "gewechat-client", specifier = ">=0.1.5" },
{ name = "html2text", specifier = ">=2024.2.26" }, { name = "html2text", specifier = ">=2024.2.26" },
{ name = "langbot-plugin", specifier = "==0.5.8" }, { name = "langbot-plugin", specifier = "==0.6.0b5" },
{ name = "langchain", specifier = ">=1.3.9" }, { name = "langchain", specifier = ">=1.3.9" },
{ name = "langchain-core", specifier = ">=1.3.3" }, { name = "langchain-core", specifier = ">=1.3.3" },
{ name = "langchain-text-splitters", specifier = ">=1.1.2" }, { name = "langchain-text-splitters", specifier = ">=1.1.2" },
@@ -2250,7 +2250,7 @@ dev = [
[[package]] [[package]]
name = "langbot-plugin" name = "langbot-plugin"
version = "0.5.8" version = "0.6.0b5"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "aiofiles" }, { name = "aiofiles" },
@@ -2271,9 +2271,9 @@ dependencies = [
{ name = "watchdog" }, { name = "watchdog" },
{ name = "websockets" }, { name = "websockets" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/d0/ab/8d8bd6b8355c5b30b4aab2b5322fd28d8f36158f36d6b4ee33f4df4bc861/langbot_plugin-0.5.8.tar.gz", hash = "sha256:46fbdf948f4a2d110607738ab35633c9ab22a30784edce3a4e684cd19bab84ff", size = 487972, upload-time = "2026-09-11T09:27:58.304Z" } sdist = { url = "https://files.pythonhosted.org/packages/7d/27/c23c5bab4137e755096f7ad32afcec834cb9b2e08129beb8bf797c1b8c5b/langbot_plugin-0.6.0b5.tar.gz", hash = "sha256:8cd1024ec1a8a131b8afe48dd3ff6c002626d6ab885ab99b8078985e4a8f53be", size = 613773, upload-time = "2026-09-20T10:18:18.171Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/c2/13/4939205e2f7922ec09113e390e35f9355ce6d93e1b380a4b3c49441130f5/langbot_plugin-0.5.8-py3-none-any.whl", hash = "sha256:4fbbcfa55f1dcb9af8392b48de8b7877ea79c880dfd268d651404702614d182e", size = 311552, upload-time = "2026-09-11T09:27:57.082Z" }, { url = "https://files.pythonhosted.org/packages/64/fc/b0c7c009e166650d82bcb58784e89a64ec04de5156f95da7fb7d59d2c4a3/langbot_plugin-0.6.0b5-py3-none-any.whl", hash = "sha256:15c5a60765db34e5a0216eab205178fb06c6447d286f648008aa89207e75d667", size = 408564, upload-time = "2026-09-20T10:18:16.967Z" },
] ]
[[package]] [[package]]
@@ -3301,7 +3301,7 @@ name = "nvidia-cublas"
version = "13.1.1.3" version = "13.1.1.3"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "nvidia-cuda-nvrtc" }, { name = "nvidia-cuda-nvrtc", marker = "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'" },
] ]
wheels = [ 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" }, { 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" },
@@ -3340,7 +3340,7 @@ name = "nvidia-cudnn-cu13"
version = "9.20.0.48" version = "9.20.0.48"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "nvidia-cublas" }, { name = "nvidia-cublas", marker = "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'" },
] ]
wheels = [ 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" }, { 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" },
@@ -3352,7 +3352,7 @@ name = "nvidia-cufft"
version = "12.0.0.61" version = "12.0.0.61"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "nvidia-nvjitlink" }, { name = "nvidia-nvjitlink", marker = "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'" },
] ]
wheels = [ 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" }, { 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" },
@@ -3382,9 +3382,9 @@ name = "nvidia-cusolver"
version = "12.0.4.66" version = "12.0.4.66"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "nvidia-cublas" }, { name = "nvidia-cublas", marker = "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'" },
{ name = "nvidia-cusparse" }, { name = "nvidia-cusparse", marker = "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'" },
{ name = "nvidia-nvjitlink" }, { name = "nvidia-nvjitlink", marker = "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'" },
] ]
wheels = [ 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" }, { 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" },
@@ -3396,7 +3396,7 @@ name = "nvidia-cusparse"
version = "12.6.3.3" version = "12.6.3.3"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "nvidia-nvjitlink" }, { name = "nvidia-nvjitlink", marker = "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'" },
] ]
wheels = [ 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" }, { 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" },
@@ -4489,7 +4489,7 @@ name = "pylibseekdb"
version = "1.4.0" version = "1.4.0"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "pymysql" }, { name = "pymysql", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" },
] ]
wheels = [ 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" }, { 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" },
@@ -5257,10 +5257,10 @@ name = "scikit-learn"
version = "1.8.0" version = "1.8.0"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "joblib" }, { name = "joblib", marker = "python_full_version >= '3.14'" },
{ name = "numpy" }, { name = "numpy", marker = "python_full_version >= '3.14'" },
{ name = "scipy" }, { name = "scipy", marker = "python_full_version >= '3.14'" },
{ name = "threadpoolctl" }, { 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" } 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 = [ wheels = [
@@ -5307,7 +5307,7 @@ name = "scipy"
version = "1.17.1" version = "1.17.1"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ 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" } 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 = [ wheels = [
@@ -5378,14 +5378,14 @@ name = "sentence-transformers"
version = "5.2.3" version = "5.2.3"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "huggingface-hub" }, { name = "huggingface-hub", marker = "python_full_version >= '3.14'" },
{ name = "numpy" }, { name = "numpy", marker = "python_full_version >= '3.14'" },
{ name = "scikit-learn" }, { name = "scikit-learn", marker = "python_full_version >= '3.14'" },
{ name = "scipy" }, { name = "scipy", marker = "python_full_version >= '3.14'" },
{ name = "torch" }, { name = "torch", marker = "python_full_version >= '3.14'" },
{ name = "tqdm" }, { name = "tqdm", marker = "python_full_version >= '3.14'" },
{ name = "transformers" }, { name = "transformers", marker = "python_full_version >= '3.14'" },
{ name = "typing-extensions" }, { 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" } 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 = [ wheels = [
@@ -5758,21 +5758,21 @@ name = "torch"
version = "2.12.1" version = "2.12.1"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "cuda-bindings", marker = "sys_platform == 'linux'" }, { 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 = "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" }, { name = "filelock", marker = "python_full_version >= '3.14'" },
{ name = "fsspec" }, { name = "fsspec", marker = "python_full_version >= '3.14'" },
{ name = "jinja2" }, { name = "jinja2", marker = "python_full_version >= '3.14'" },
{ name = "networkx" }, { name = "networkx", marker = "python_full_version >= '3.14'" },
{ name = "nvidia-cublas", marker = "sys_platform == 'linux'" }, { name = "nvidia-cublas", marker = "python_full_version >= '3.14' and sys_platform == 'linux'" },
{ name = "nvidia-cudnn-cu13", marker = "sys_platform == 'linux'" }, { name = "nvidia-cudnn-cu13", marker = "python_full_version >= '3.14' and sys_platform == 'linux'" },
{ name = "nvidia-cusparselt-cu13", marker = "sys_platform == 'linux'" }, { name = "nvidia-cusparselt-cu13", marker = "python_full_version >= '3.14' and sys_platform == 'linux'" },
{ name = "nvidia-nccl-cu13", marker = "sys_platform == 'linux'" }, { name = "nvidia-nccl-cu13", marker = "python_full_version >= '3.14' and sys_platform == 'linux'" },
{ name = "nvidia-nvshmem-cu13", marker = "sys_platform == 'linux'" }, { name = "nvidia-nvshmem-cu13", marker = "python_full_version >= '3.14' and sys_platform == 'linux'" },
{ name = "setuptools" }, { name = "setuptools", marker = "python_full_version >= '3.14'" },
{ name = "sympy" }, { name = "sympy", marker = "python_full_version >= '3.14'" },
{ name = "triton", marker = "sys_platform == 'linux'" }, { name = "triton", marker = "python_full_version >= '3.14' and sys_platform == 'linux'" },
{ name = "typing-extensions" }, { name = "typing-extensions", marker = "python_full_version >= '3.14'" },
] ]
wheels = [ 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" }, { 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" },
@@ -5814,15 +5814,15 @@ name = "transformers"
version = "5.3.0" version = "5.3.0"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "huggingface-hub" }, { name = "huggingface-hub", marker = "python_full_version >= '3.14'" },
{ name = "numpy" }, { name = "numpy", marker = "python_full_version >= '3.14'" },
{ name = "packaging" }, { name = "packaging", marker = "python_full_version >= '3.14'" },
{ name = "pyyaml" }, { name = "pyyaml", marker = "python_full_version >= '3.14'" },
{ name = "regex" }, { name = "regex", marker = "python_full_version >= '3.14'" },
{ name = "safetensors" }, { name = "safetensors", marker = "python_full_version >= '3.14'" },
{ name = "tokenizers" }, { name = "tokenizers", marker = "python_full_version >= '3.14'" },
{ name = "tqdm" }, { name = "tqdm", marker = "python_full_version >= '3.14'" },
{ name = "typer" }, { 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" } 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 = [ wheels = [
@@ -6083,9 +6083,9 @@ name = "valkey-glide"
version = "2.4.1" version = "2.4.1"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "anyio" }, { name = "anyio", marker = "sys_platform != 'win32'" },
{ name = "protobuf" }, { name = "protobuf", marker = "sys_platform != 'win32'" },
{ name = "sniffio" }, { 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" } 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 = [ wheels = [