mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-09-30 05:16:52 +08:00
Merge remote-tracking branch 'origin/master' into feat/workspace-operation-traceability
This commit is contained in:
@@ -8,11 +8,12 @@ persistence, or a Plugin Runtime apply request. It calls the SDK public
|
||||
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.
|
||||
Core retains the artifact SHA-256, normalized digest
|
||||
(`normalized_zip_digest()`), verification state, declared shared-runtime
|
||||
profile, key ID, selected admission profile, and stable admission code in the
|
||||
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
|
||||
|
||||
@@ -80,13 +81,14 @@ 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.
|
||||
SDK 0.6.2 carries an installation-level execution mode in both apply and
|
||||
authoritative reconcile payloads. Core selects `shared-runtime-v1` only when
|
||||
the persisted certification record says verification was valid, both the
|
||||
certificate and admission profiles are `shared-runtime-v1`, the admission code
|
||||
is shared-eligible, and the record's artifact SHA-256 exactly matches the
|
||||
installation row. Missing, malformed, stale, invalid, or dedicated admission
|
||||
facts select `dedicated`. Install, upgrade, configuration revision, restart,
|
||||
and reconnect all use this same persisted-fact derivation.
|
||||
|
||||
The existing public plugin-log boundary already applies the immutable
|
||||
installation binding (including workspace UUID) through
|
||||
@@ -98,7 +100,5 @@ 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.
|
||||
Core pins `langbot-plugin==0.6.2`, the first published SDK release carrying the
|
||||
canonical installation execution-mode contract.
|
||||
|
||||
+1
-1
@@ -71,7 +71,7 @@ dependencies = [
|
||||
"langchain-text-splitters>=1.1.2",
|
||||
"chromadb>=1.0.0,<2.0.0",
|
||||
"qdrant-client (>=1.15.1,<2.0.0)",
|
||||
"langbot-plugin==0.6.1",
|
||||
"langbot-plugin==0.6.11",
|
||||
"asyncpg>=0.30.0",
|
||||
"line-bot-sdk>=3.19.0",
|
||||
"matrix-nio>=0.25.2",
|
||||
|
||||
+138
@@ -0,0 +1,138 @@
|
||||
"""Backfill raw artifact digests for legacy shared certificates.
|
||||
|
||||
Revision ID: 0032_cert_artifact_digest
|
||||
Revises: 0031_merge_totp_assistant
|
||||
|
||||
Older certified-plugin rows persisted every shared-admission fact except the raw
|
||||
archive digest. Placement now requires that digest to match the installation's
|
||||
immutable artifact digest, so this migration fills only records whose remaining
|
||||
facts already prove the exact legacy shared-admission shape.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
||||
revision = '0032_cert_artifact_digest'
|
||||
down_revision = '0031_merge_totp_assistant'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
_TABLE = 'plugin_settings'
|
||||
_SHARED_RUNTIME = 'shared-runtime-v1'
|
||||
_SHARED_ADMISSION_CODE = 'CERTIFIED_PLUGIN_SHARED_ELIGIBLE'
|
||||
_LOWER_HEX = frozenset('0123456789abcdef')
|
||||
_HEX = frozenset('0123456789abcdefABCDEF')
|
||||
|
||||
|
||||
def _is_digest(value: object, *, lowercase: bool) -> bool:
|
||||
allowed = _LOWER_HEX if lowercase else _HEX
|
||||
return isinstance(value, str) and len(value) == 64 and all(character in allowed for character in value)
|
||||
|
||||
|
||||
def _eligible_certification(install_info: object, artifact_digest: object) -> Mapping[object, object] | None:
|
||||
if not _is_digest(artifact_digest, lowercase=True) or not isinstance(install_info, Mapping):
|
||||
return None
|
||||
certification = install_info.get('_certification')
|
||||
if not isinstance(certification, Mapping) or 'artifact_digest' in certification:
|
||||
return None
|
||||
certificate_id = certification.get('certificate_id')
|
||||
if not isinstance(certificate_id, str) or not certificate_id.strip():
|
||||
return None
|
||||
if not _is_digest(certification.get('normalized_digest'), lowercase=False):
|
||||
return None
|
||||
required_facts = {
|
||||
'verification': 'valid',
|
||||
'certificate_runtime_profile': _SHARED_RUNTIME,
|
||||
'runtime_profile': _SHARED_RUNTIME,
|
||||
'admission_code': _SHARED_ADMISSION_CODE,
|
||||
}
|
||||
if not all(certification.get(key) == value for key, value in required_facts.items()):
|
||||
return None
|
||||
return certification
|
||||
|
||||
|
||||
def _suspend_postgres_rls(conn: sa.Connection) -> tuple[bool, bool]:
|
||||
if conn.dialect.name != 'postgresql':
|
||||
return False, False
|
||||
state = conn.execute(
|
||||
sa.text('SELECT relrowsecurity, relforcerowsecurity FROM pg_class WHERE oid = to_regclass(:table_name)'),
|
||||
{'table_name': _TABLE},
|
||||
).one()
|
||||
rls_enabled, rls_forced = bool(state.relrowsecurity), bool(state.relforcerowsecurity)
|
||||
if rls_forced:
|
||||
conn.execute(sa.text(f'ALTER TABLE {_TABLE} NO FORCE ROW LEVEL SECURITY'))
|
||||
if rls_enabled:
|
||||
conn.execute(sa.text(f'ALTER TABLE {_TABLE} DISABLE ROW LEVEL SECURITY'))
|
||||
return rls_enabled, rls_forced
|
||||
|
||||
|
||||
def _restore_postgres_rls(conn: sa.Connection, state: tuple[bool, bool]) -> None:
|
||||
if conn.dialect.name != 'postgresql':
|
||||
return
|
||||
rls_enabled, rls_forced = state
|
||||
if rls_enabled:
|
||||
conn.execute(sa.text(f'ALTER TABLE {_TABLE} ENABLE ROW LEVEL SECURITY'))
|
||||
if rls_forced:
|
||||
conn.execute(sa.text(f'ALTER TABLE {_TABLE} FORCE ROW LEVEL SECURITY'))
|
||||
|
||||
|
||||
def backfill_certification_artifact_digests(conn: sa.Connection) -> None:
|
||||
inspector = sa.inspect(conn)
|
||||
if _TABLE not in inspector.get_table_names():
|
||||
return
|
||||
columns = {column['name'] for column in inspector.get_columns(_TABLE)}
|
||||
required_columns = {
|
||||
'workspace_uuid',
|
||||
'plugin_author',
|
||||
'plugin_name',
|
||||
'artifact_digest',
|
||||
'install_info',
|
||||
}
|
||||
if not required_columns <= columns:
|
||||
return
|
||||
|
||||
plugin_settings = sa.table(
|
||||
_TABLE,
|
||||
sa.column('workspace_uuid', sa.String(36)),
|
||||
sa.column('plugin_author', sa.String(255)),
|
||||
sa.column('plugin_name', sa.String(255)),
|
||||
sa.column('artifact_digest', sa.String(64)),
|
||||
sa.column('install_info', sa.JSON()),
|
||||
)
|
||||
rows = conn.execute(sa.select(plugin_settings)).mappings().all()
|
||||
for row in rows:
|
||||
certification = _eligible_certification(row['install_info'], row['artifact_digest'])
|
||||
if certification is None:
|
||||
continue
|
||||
updated_certification = dict(certification)
|
||||
updated_certification['artifact_digest'] = row['artifact_digest']
|
||||
updated_install_info = dict(row['install_info'])
|
||||
updated_install_info['_certification'] = updated_certification
|
||||
conn.execute(
|
||||
plugin_settings.update()
|
||||
.where(plugin_settings.c.workspace_uuid == row['workspace_uuid'])
|
||||
.where(plugin_settings.c.plugin_author == row['plugin_author'])
|
||||
.where(plugin_settings.c.plugin_name == row['plugin_name'])
|
||||
.values(install_info=updated_install_info)
|
||||
)
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
conn = op.get_bind()
|
||||
if _TABLE not in sa.inspect(conn).get_table_names():
|
||||
return
|
||||
rls_state = _suspend_postgres_rls(conn)
|
||||
try:
|
||||
backfill_certification_artifact_digests(conn)
|
||||
finally:
|
||||
_restore_postgres_rls(conn, rls_state)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# The source digest cannot be distinguished from a digest persisted by
|
||||
# current Core, so downgrade intentionally preserves the safe enrichment.
|
||||
pass
|
||||
@@ -14,6 +14,7 @@ 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
|
||||
from langbot_plugin.entities.io.context import PluginExecutionMode
|
||||
|
||||
|
||||
SHARED_RUNTIME_V1 = 'shared-runtime-v1'
|
||||
@@ -270,3 +271,37 @@ def decide_plugin_log_visibility(facts: PluginCertificationFacts) -> PluginLogVi
|
||||
if facts.certificate.is_valid_shared_runtime:
|
||||
return PluginLogVisibility.TENANT_SCOPED
|
||||
return PluginLogVisibility.DETAILED_PROCESS
|
||||
|
||||
|
||||
def execution_mode_for_persisted_installation(
|
||||
*,
|
||||
artifact_digest: str,
|
||||
install_info: object,
|
||||
) -> PluginExecutionMode:
|
||||
"""Derive placement only from persisted facts bound to the exact artifact."""
|
||||
|
||||
if len(artifact_digest) != 64 or any(character not in '0123456789abcdef' for character in artifact_digest.lower()):
|
||||
return PluginExecutionMode.DEDICATED
|
||||
if not isinstance(install_info, Mapping):
|
||||
return PluginExecutionMode.DEDICATED
|
||||
certification = install_info.get('_certification')
|
||||
if not isinstance(certification, Mapping):
|
||||
return PluginExecutionMode.DEDICATED
|
||||
normalized_digest = certification.get('normalized_digest')
|
||||
if not isinstance(normalized_digest, str) or len(normalized_digest) != 64:
|
||||
return PluginExecutionMode.DEDICATED
|
||||
if any(character not in '0123456789abcdef' for character in normalized_digest.lower()):
|
||||
return PluginExecutionMode.DEDICATED
|
||||
certificate_id = certification.get('certificate_id')
|
||||
if not isinstance(certificate_id, str) or not certificate_id.strip():
|
||||
return PluginExecutionMode.DEDICATED
|
||||
shared_facts = {
|
||||
'artifact_digest': artifact_digest,
|
||||
'verification': CertificateVerification.VALID.value,
|
||||
'certificate_runtime_profile': SHARED_RUNTIME_V1,
|
||||
'runtime_profile': SHARED_RUNTIME_V1,
|
||||
'admission_code': AdmissionCode.SHARED_ELIGIBLE.value,
|
||||
}
|
||||
if all(certification.get(key) == value for key, value in shared_facts.items()):
|
||||
return PluginExecutionMode.SHARED_CERTIFIED
|
||||
return PluginExecutionMode.DEDICATED
|
||||
|
||||
@@ -34,6 +34,7 @@ from .certification import (
|
||||
PluginCertificationFacts,
|
||||
VerifiedArchiveCertificate,
|
||||
decide_plugin_admission,
|
||||
execution_mode_for_persisted_installation,
|
||||
verify_plugin_archive_certificate,
|
||||
)
|
||||
from .github import (
|
||||
@@ -62,6 +63,7 @@ from langbot_plugin.runtime.security import (
|
||||
)
|
||||
from langbot_plugin.entities.io.context import (
|
||||
InstallationBinding,
|
||||
PluginExecutionMode,
|
||||
PluginInstallationDesiredState,
|
||||
PluginWorkerPolicy,
|
||||
RuntimeIdentity,
|
||||
@@ -343,6 +345,13 @@ class PluginRuntimeConnector(ManagedRuntimeConnector):
|
||||
artifact_digest=setting.artifact_digest,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _execution_mode_from_setting(setting: persistence_plugin.PluginSetting) -> PluginExecutionMode:
|
||||
return execution_mode_for_persisted_installation(
|
||||
artifact_digest=setting.artifact_digest,
|
||||
install_info=setting.install_info,
|
||||
)
|
||||
|
||||
def _legacy_oss_bridge_binding(self, execution_context: ExecutionContext) -> InstallationBinding:
|
||||
seed = f'langbot:oss-plugin-bridge:{execution_context.instance_uuid}:{execution_context.workspace_uuid}'
|
||||
return InstallationBinding(
|
||||
@@ -511,6 +520,7 @@ class PluginRuntimeConnector(ManagedRuntimeConnector):
|
||||
PluginInstallationDesiredState(
|
||||
binding=binding,
|
||||
enabled=setting.enabled,
|
||||
execution_mode=self._execution_mode_from_setting(setting),
|
||||
)
|
||||
)
|
||||
return tuple(desired_states)
|
||||
@@ -526,6 +536,7 @@ class PluginRuntimeConnector(ManagedRuntimeConnector):
|
||||
desired.binding,
|
||||
artifact_package=artifact_package,
|
||||
enabled=desired.enabled,
|
||||
execution_mode=desired.execution_mode,
|
||||
)
|
||||
self._raise_apply_failure(desired, result)
|
||||
if result.get('state') != 'artifact_missing':
|
||||
@@ -551,6 +562,7 @@ class PluginRuntimeConnector(ManagedRuntimeConnector):
|
||||
desired.binding,
|
||||
artifact_package=persisted_package,
|
||||
enabled=desired.enabled,
|
||||
execution_mode=desired.execution_mode,
|
||||
)
|
||||
self._raise_apply_failure(desired, repaired)
|
||||
if repaired.get('state') == 'artifact_missing':
|
||||
@@ -1793,6 +1805,7 @@ class PluginRuntimeConnector(ManagedRuntimeConnector):
|
||||
}:
|
||||
raise ValueError(decision.code.value)
|
||||
certification_info = {
|
||||
'artifact_digest': hashlib.sha256(file_bytes).hexdigest(),
|
||||
'normalized_digest': facts.artifact_digest,
|
||||
'verification': facts.certificate.verification.value,
|
||||
'certificate_runtime_profile': facts.certificate.runtime_profile,
|
||||
@@ -1893,7 +1906,14 @@ class PluginRuntimeConnector(ManagedRuntimeConnector):
|
||||
install_info=install_info,
|
||||
artifact_digest=artifact_digest,
|
||||
)
|
||||
desired = PluginInstallationDesiredState(binding=binding, enabled=True)
|
||||
desired = PluginInstallationDesiredState(
|
||||
binding=binding,
|
||||
enabled=True,
|
||||
execution_mode=execution_mode_for_persisted_installation(
|
||||
artifact_digest=artifact_digest,
|
||||
install_info=install_info,
|
||||
),
|
||||
)
|
||||
runtime_handler.register_installation_binding(
|
||||
binding,
|
||||
plugin_author=plugin_author,
|
||||
@@ -2143,6 +2163,7 @@ class PluginRuntimeConnector(ManagedRuntimeConnector):
|
||||
desired = PluginInstallationDesiredState(
|
||||
binding=binding,
|
||||
enabled=setting.enabled,
|
||||
execution_mode=self._execution_mode_from_setting(setting),
|
||||
)
|
||||
is_legacy_oss = self.runtime_profile == 'oss_dev' and (
|
||||
not isinstance(setting.install_info, dict)
|
||||
|
||||
@@ -24,6 +24,7 @@ from langbot_plugin.entities.io.context import (
|
||||
ActionContext,
|
||||
ApplyPluginInstallationRequest,
|
||||
InstallationBinding,
|
||||
PluginExecutionMode,
|
||||
PluginInstallationDesiredState,
|
||||
PluginWorkerPolicy,
|
||||
ReconcilePluginInstallationsRequest,
|
||||
@@ -2793,6 +2794,7 @@ class RuntimeConnectionHandler(handler.Handler):
|
||||
*,
|
||||
artifact_package: bytes | None,
|
||||
enabled: bool,
|
||||
execution_mode: PluginExecutionMode = PluginExecutionMode.DEDICATED,
|
||||
) -> dict[str, Any]:
|
||||
with self.installation_scope(binding):
|
||||
artifact_file_key = None
|
||||
@@ -2801,6 +2803,7 @@ class RuntimeConnectionHandler(handler.Handler):
|
||||
request = ApplyPluginInstallationRequest(
|
||||
artifact_file_key=artifact_file_key,
|
||||
enabled=enabled,
|
||||
execution_mode=execution_mode,
|
||||
)
|
||||
return await self.call_action(
|
||||
LangBotToRuntimeAction.APPLY_PLUGIN_INSTALLATION,
|
||||
|
||||
@@ -16,6 +16,7 @@ import uuid
|
||||
import pytest
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.ext.asyncio import create_async_engine
|
||||
from langbot_plugin.entities.io.context import PluginExecutionMode
|
||||
|
||||
from langbot.pkg.entity import persistence
|
||||
from langbot.pkg.entity.persistence.base import Base
|
||||
@@ -25,6 +26,7 @@ from langbot.pkg.persistence.alembic_runner import (
|
||||
run_alembic_downgrade,
|
||||
run_alembic_upgrade,
|
||||
)
|
||||
from langbot.pkg.plugin.certification import execution_mode_for_persisted_installation
|
||||
from langbot.pkg.utils import importutil
|
||||
|
||||
|
||||
@@ -353,6 +355,143 @@ async def test_empty_database_startup_schema_then_real_migrations(convergence_en
|
||||
assert await get_alembic_current(engine) == get_alembic_head()
|
||||
|
||||
|
||||
def _legacy_shared_certification(**overrides):
|
||||
certification = {
|
||||
'normalized_digest': 'B' * 64,
|
||||
'verification': 'valid',
|
||||
'certificate_runtime_profile': 'shared-runtime-v1',
|
||||
'certificate_id': 'ed25519:trusted-issuer',
|
||||
'runtime_profile': 'shared-runtime-v1',
|
||||
'admission_code': 'CERTIFIED_PLUGIN_SHARED_ELIGIBLE',
|
||||
'preserved_certificate_key': {'nested': True},
|
||||
}
|
||||
certification.update(overrides)
|
||||
return certification
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_certification_artifact_digest_backfill_is_safe_and_enables_shared_placement(convergence_engine):
|
||||
engine = convergence_engine
|
||||
async with engine.begin() as conn:
|
||||
await conn.run_sync(Base.metadata.create_all)
|
||||
await run_alembic_upgrade(engine, '0031_merge_totp_assistant')
|
||||
|
||||
valid_digest = 'a' * 64
|
||||
rows = {
|
||||
'eligible-a': (_legacy_shared_certification(), valid_digest),
|
||||
'eligible-b': (_legacy_shared_certification(), valid_digest),
|
||||
'present-matching': (_legacy_shared_certification(artifact_digest=valid_digest), valid_digest),
|
||||
'present-mismatched': (_legacy_shared_certification(artifact_digest='c' * 64), valid_digest),
|
||||
'invalid': (_legacy_shared_certification(verification='invalid'), valid_digest),
|
||||
'incomplete': (_legacy_shared_certification(certificate_id=' '), valid_digest),
|
||||
'missing-fact': (
|
||||
{
|
||||
key: value
|
||||
for key, value in _legacy_shared_certification().items()
|
||||
if key != 'certificate_runtime_profile'
|
||||
},
|
||||
valid_digest,
|
||||
),
|
||||
'malformed-normalized': (_legacy_shared_certification(normalized_digest='g' * 64), valid_digest),
|
||||
'dedicated-certificate': (
|
||||
_legacy_shared_certification(certificate_runtime_profile='dedicated'),
|
||||
valid_digest,
|
||||
),
|
||||
'dedicated': (_legacy_shared_certification(runtime_profile='dedicated'), valid_digest),
|
||||
'wrong-admission': (_legacy_shared_certification(admission_code='SHARED_ELIGIBLE'), valid_digest),
|
||||
'uppercase-row-digest': (_legacy_shared_certification(), 'A' * 64),
|
||||
'nonhex-row-digest': (_legacy_shared_certification(), 'g' * 64),
|
||||
}
|
||||
plugin_settings = Base.metadata.tables['plugin_settings']
|
||||
workspaces = Base.metadata.tables['workspaces']
|
||||
async with engine.begin() as conn:
|
||||
for index, (name, (certification, artifact_digest)) in enumerate(rows.items(), start=1):
|
||||
workspace_uuid = f'41100000-0000-4000-8000-{index:012d}'
|
||||
await conn.execute(
|
||||
workspaces.insert().values(
|
||||
uuid=workspace_uuid,
|
||||
instance_uuid=f'cert-backfill-{index}',
|
||||
name=name,
|
||||
slug=name,
|
||||
)
|
||||
)
|
||||
await conn.execute(
|
||||
plugin_settings.insert().values(
|
||||
workspace_uuid=workspace_uuid,
|
||||
plugin_author='langbot',
|
||||
plugin_name=name,
|
||||
installation_uuid=f'51100000-0000-4000-8000-{index:012d}',
|
||||
artifact_digest=artifact_digest,
|
||||
runtime_revision=1,
|
||||
install_info={
|
||||
'_certification': certification,
|
||||
'preserved_install_key': ['keep', {'nested': True}],
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
await run_alembic_upgrade(engine, 'head')
|
||||
# Re-running the data revision itself must also be harmless.
|
||||
migration = __import__(
|
||||
'langbot.pkg.persistence.alembic.versions.0032_certification_artifact_digest_backfill',
|
||||
fromlist=['upgrade'],
|
||||
)
|
||||
async with engine.begin() as conn:
|
||||
await conn.run_sync(lambda sync: migration.backfill_certification_artifact_digests(sync))
|
||||
stored = {
|
||||
name: (artifact_digest, install_info)
|
||||
for name, artifact_digest, install_info in (
|
||||
await conn.execute(
|
||||
sa.select(
|
||||
plugin_settings.c.plugin_name,
|
||||
plugin_settings.c.artifact_digest,
|
||||
plugin_settings.c.install_info,
|
||||
)
|
||||
)
|
||||
).all()
|
||||
}
|
||||
|
||||
for name in ('eligible-a', 'eligible-b'):
|
||||
eligible_digest, eligible_info = stored[name]
|
||||
assert eligible_info['preserved_install_key'] == ['keep', {'nested': True}]
|
||||
assert eligible_info['_certification']['preserved_certificate_key'] == {'nested': True}
|
||||
assert eligible_info['_certification']['artifact_digest'] == eligible_digest == valid_digest
|
||||
assert (
|
||||
execution_mode_for_persisted_installation(
|
||||
artifact_digest=eligible_digest,
|
||||
install_info=eligible_info,
|
||||
)
|
||||
is PluginExecutionMode.SHARED_CERTIFIED
|
||||
)
|
||||
|
||||
for name, (original_certification, artifact_digest) in rows.items():
|
||||
if name in {'eligible-a', 'eligible-b'}:
|
||||
continue
|
||||
stored_digest, stored_info = stored[name]
|
||||
assert stored_digest == artifact_digest
|
||||
assert stored_info['_certification'] == original_certification
|
||||
if name != 'present-matching':
|
||||
assert (
|
||||
execution_mode_for_persisted_installation(
|
||||
artifact_digest=stored_digest,
|
||||
install_info=stored_info,
|
||||
)
|
||||
is PluginExecutionMode.DEDICATED
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_certification_artifact_digest_backfill_accepts_fresh_current_schema(convergence_engine):
|
||||
engine = convergence_engine
|
||||
async with engine.begin() as conn:
|
||||
await conn.run_sync(Base.metadata.create_all)
|
||||
|
||||
await run_alembic_upgrade(engine, 'head')
|
||||
await run_alembic_upgrade(engine, 'head')
|
||||
|
||||
assert await get_alembic_current(engine) == get_alembic_head()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_merge_only_downgrade_preserves_both_branch_schemas(convergence_engine):
|
||||
engine = convergence_engine
|
||||
|
||||
@@ -329,6 +329,76 @@ class TestPostgreSQLMigrationBaseline:
|
||||
rev = await get_alembic_current(postgres_engine)
|
||||
assert rev == '0001_baseline'
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_certification_artifact_digest_backfill_updates_only_complete_legacy_shared_rows(
|
||||
self,
|
||||
postgres_engine,
|
||||
clean_tables,
|
||||
clean_alembic_version,
|
||||
):
|
||||
async with postgres_engine.begin() as conn:
|
||||
await conn.run_sync(Base.metadata.create_all)
|
||||
await run_alembic_stamp(postgres_engine, '0031_merge_totp_assistant')
|
||||
|
||||
plugin_settings = Base.metadata.tables['plugin_settings']
|
||||
workspaces = Base.metadata.tables['workspaces']
|
||||
digest = 'a' * 64
|
||||
complete = {
|
||||
'normalized_digest': 'B' * 64,
|
||||
'verification': 'valid',
|
||||
'certificate_runtime_profile': 'shared-runtime-v1',
|
||||
'certificate_id': 'ed25519:trusted-issuer',
|
||||
'runtime_profile': 'shared-runtime-v1',
|
||||
'admission_code': 'CERTIFIED_PLUGIN_SHARED_ELIGIBLE',
|
||||
'preserved': {'key': True},
|
||||
}
|
||||
rows = {
|
||||
'eligible': complete,
|
||||
'invalid': {**complete, 'verification': 'invalid'},
|
||||
'present': {**complete, 'artifact_digest': 'c' * 64},
|
||||
}
|
||||
async with postgres_engine.begin() as conn:
|
||||
for index, (name, certification) in enumerate(rows.items(), start=1):
|
||||
workspace_uuid = f'61100000-0000-4000-8000-{index:012d}'
|
||||
await conn.execute(
|
||||
workspaces.insert().values(
|
||||
uuid=workspace_uuid,
|
||||
instance_uuid=f'postgres-cert-backfill-{index}',
|
||||
name=name,
|
||||
slug=name,
|
||||
)
|
||||
)
|
||||
await conn.execute(
|
||||
plugin_settings.insert().values(
|
||||
workspace_uuid=workspace_uuid,
|
||||
plugin_author='langbot',
|
||||
plugin_name=name,
|
||||
installation_uuid=f'71100000-0000-4000-8000-{index:012d}',
|
||||
artifact_digest=digest,
|
||||
runtime_revision=1,
|
||||
install_info={'_certification': certification, 'preserved': ['install-info']},
|
||||
)
|
||||
)
|
||||
|
||||
await run_alembic_upgrade(postgres_engine, 'head')
|
||||
|
||||
async with postgres_engine.connect() as conn:
|
||||
stored = dict(
|
||||
(await conn.execute(sa.select(plugin_settings.c.plugin_name, plugin_settings.c.install_info))).all()
|
||||
)
|
||||
assert stored['eligible'] == {
|
||||
'_certification': {**complete, 'artifact_digest': digest},
|
||||
'preserved': ['install-info'],
|
||||
}
|
||||
assert stored['invalid'] == {
|
||||
'_certification': rows['invalid'],
|
||||
'preserved': ['install-info'],
|
||||
}
|
||||
assert stored['present'] == {
|
||||
'_certification': rows['present'],
|
||||
'preserved': ['install-info'],
|
||||
}
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_fresh_postgres_schema_accepts_application_casefold_identity(
|
||||
self,
|
||||
|
||||
@@ -17,6 +17,7 @@ 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.entities.io.context import PluginExecutionMode
|
||||
from langbot_plugin.runtime.plugin.mgr import PluginInstallSource
|
||||
|
||||
|
||||
@@ -57,11 +58,19 @@ async def test_install_plugin_admits_archive_before_persistence_and_applies_sele
|
||||
)
|
||||
persisted_info = connector._persist_installation_package.await_args.kwargs['install_info']
|
||||
assert persisted_info['_certification']['runtime_profile'] == expected_profile
|
||||
assert persisted_info['_certification']['artifact_digest'] == hashlib.sha256(package).hexdigest()
|
||||
assert persisted_info['_certification']['normalized_digest'] == _normalized_digest(package)
|
||||
if archive_kind == 'signed_shared':
|
||||
assert persisted_info['_certification']['certificate_id'] == 'ephemeral'
|
||||
connector.handler.apply_plugin_installation.assert_awaited_once_with(
|
||||
binding,
|
||||
artifact_package=package,
|
||||
enabled=True,
|
||||
execution_mode=(
|
||||
PluginExecutionMode.SHARED_CERTIFIED
|
||||
if expected_profile == 'shared-runtime-v1'
|
||||
else PluginExecutionMode.DEDICATED
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@@ -124,6 +133,7 @@ async def test_oss_admits_unresolvable_declaration_on_the_dedicated_profile() ->
|
||||
binding,
|
||||
artifact_package=package,
|
||||
enabled=True,
|
||||
execution_mode=PluginExecutionMode.DEDICATED,
|
||||
)
|
||||
|
||||
|
||||
@@ -183,7 +193,10 @@ async def test_marketplace_version_selection_keeps_certificate_gate_and_single_a
|
||||
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
|
||||
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
|
||||
|
||||
@@ -6,6 +6,8 @@ import zipfile
|
||||
|
||||
import pytest
|
||||
|
||||
from langbot_plugin.entities.io.context import PluginExecutionMode
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
('deployment', 'certificate', 'certificate_id', 'force', 'expected_disposition', 'expected_code'),
|
||||
@@ -187,6 +189,162 @@ def test_log_visibility_policy_only_scopes_valid_shared_certifications(
|
||||
assert visibility.value == expected_visibility
|
||||
|
||||
|
||||
def _complete_persisted_certification() -> dict[str, object]:
|
||||
return {
|
||||
'artifact_digest': 'a' * 64,
|
||||
'normalized_digest': 'b' * 64,
|
||||
'verification': 'valid',
|
||||
'certificate_runtime_profile': 'shared-runtime-v1',
|
||||
'certificate_id': 'ed25519:trusted-issuer',
|
||||
'runtime_profile': 'shared-runtime-v1',
|
||||
'admission_code': 'CERTIFIED_PLUGIN_SHARED_ELIGIBLE',
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
('certification', 'expected_mode'),
|
||||
[
|
||||
(_complete_persisted_certification(), PluginExecutionMode.SHARED_CERTIFIED),
|
||||
(None, PluginExecutionMode.DEDICATED),
|
||||
({}, PluginExecutionMode.DEDICATED),
|
||||
(
|
||||
{
|
||||
**_complete_persisted_certification(),
|
||||
'verification': 'invalid',
|
||||
},
|
||||
PluginExecutionMode.DEDICATED,
|
||||
),
|
||||
(
|
||||
{
|
||||
**_complete_persisted_certification(),
|
||||
'artifact_digest': 'b' * 64,
|
||||
},
|
||||
PluginExecutionMode.DEDICATED,
|
||||
),
|
||||
(
|
||||
{
|
||||
**_complete_persisted_certification(),
|
||||
'runtime_profile': 'dedicated',
|
||||
},
|
||||
PluginExecutionMode.DEDICATED,
|
||||
),
|
||||
(
|
||||
{
|
||||
**_complete_persisted_certification(),
|
||||
'admission_code': 'CERTIFIED_PLUGIN_OSS_FORCED_DEDICATED',
|
||||
},
|
||||
PluginExecutionMode.DEDICATED,
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_persisted_certification_selects_shared_execution_only_for_exact_admitted_artifact(
|
||||
certification: dict[str, object] | None,
|
||||
expected_mode: PluginExecutionMode,
|
||||
) -> None:
|
||||
from langbot.pkg.plugin.certification import execution_mode_for_persisted_installation
|
||||
|
||||
install_info = {} if certification is None else {'_certification': certification}
|
||||
|
||||
assert (
|
||||
execution_mode_for_persisted_installation(
|
||||
artifact_digest='a' * 64,
|
||||
install_info=install_info,
|
||||
)
|
||||
is expected_mode
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'missing_field',
|
||||
[
|
||||
'artifact_digest',
|
||||
'normalized_digest',
|
||||
'verification',
|
||||
'certificate_runtime_profile',
|
||||
'certificate_id',
|
||||
'runtime_profile',
|
||||
'admission_code',
|
||||
],
|
||||
)
|
||||
def test_persisted_certification_requires_every_shared_admission_fact(missing_field: str) -> None:
|
||||
from langbot.pkg.plugin.certification import execution_mode_for_persisted_installation
|
||||
|
||||
certification = _complete_persisted_certification()
|
||||
del certification[missing_field]
|
||||
|
||||
assert (
|
||||
execution_mode_for_persisted_installation(
|
||||
artifact_digest='a' * 64,
|
||||
install_info={'_certification': certification},
|
||||
)
|
||||
is PluginExecutionMode.DEDICATED
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
('field', 'malformed_value'),
|
||||
[
|
||||
('artifact_digest', None),
|
||||
('artifact_digest', 123),
|
||||
('artifact_digest', ''),
|
||||
('normalized_digest', None),
|
||||
('normalized_digest', 123),
|
||||
('normalized_digest', ''),
|
||||
('normalized_digest', ' ' * 64),
|
||||
('normalized_digest', 'b' * 63),
|
||||
('normalized_digest', 'g' * 64),
|
||||
('certificate_id', None),
|
||||
('certificate_id', 123),
|
||||
('certificate_id', ''),
|
||||
('certificate_id', ' '),
|
||||
('verification', None),
|
||||
('verification', 123),
|
||||
('verification', ''),
|
||||
('certificate_runtime_profile', None),
|
||||
('certificate_runtime_profile', 123),
|
||||
('certificate_runtime_profile', ''),
|
||||
('runtime_profile', None),
|
||||
('runtime_profile', 123),
|
||||
('runtime_profile', ''),
|
||||
('admission_code', None),
|
||||
('admission_code', 123),
|
||||
('admission_code', ''),
|
||||
],
|
||||
)
|
||||
def test_persisted_certification_rejects_malformed_shared_admission_fact(
|
||||
field: str,
|
||||
malformed_value: object,
|
||||
) -> None:
|
||||
from langbot.pkg.plugin.certification import execution_mode_for_persisted_installation
|
||||
|
||||
certification = _complete_persisted_certification()
|
||||
certification[field] = malformed_value
|
||||
|
||||
assert (
|
||||
execution_mode_for_persisted_installation(
|
||||
artifact_digest='a' * 64,
|
||||
install_info={'_certification': certification},
|
||||
)
|
||||
is PluginExecutionMode.DEDICATED
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('malformed_digest', ['', 'a' * 63, 'g' * 64])
|
||||
def test_persisted_certification_rejects_malformed_matching_raw_digest(malformed_digest: str) -> None:
|
||||
from langbot.pkg.plugin.certification import execution_mode_for_persisted_installation
|
||||
|
||||
certification = _complete_persisted_certification()
|
||||
certification['artifact_digest'] = malformed_digest
|
||||
|
||||
assert (
|
||||
execution_mode_for_persisted_installation(
|
||||
artifact_digest=malformed_digest,
|
||||
install_info={'_certification': certification},
|
||||
)
|
||||
is PluginExecutionMode.DEDICATED
|
||||
)
|
||||
|
||||
|
||||
def _archive_bytes(manifest: dict[str, object]) -> bytes:
|
||||
buffer = io.BytesIO()
|
||||
with zipfile.ZipFile(buffer, 'w') as archive:
|
||||
|
||||
@@ -17,7 +17,7 @@ from unittest.mock import AsyncMock, Mock
|
||||
from importlib import import_module
|
||||
|
||||
from tests.factories import text_query
|
||||
from langbot_plugin.entities.io.context import InstallationBinding
|
||||
from langbot_plugin.entities.io.context import InstallationBinding, PluginExecutionMode
|
||||
|
||||
from langbot.pkg.api.http.context import ExecutionContext
|
||||
from langbot.pkg.workspace.errors import WorkspaceNotFoundError
|
||||
@@ -758,7 +758,18 @@ class TestSetPluginConfig:
|
||||
runtime_revision=1,
|
||||
artifact_digest=TEST_INSTALLATION_BINDING.artifact_digest,
|
||||
enabled=True,
|
||||
install_info={'_artifact_storage': 'tenant_binary_storage_v1'},
|
||||
install_info={
|
||||
'_artifact_storage': 'tenant_binary_storage_v1',
|
||||
'_certification': {
|
||||
'artifact_digest': TEST_INSTALLATION_BINDING.artifact_digest,
|
||||
'normalized_digest': 'b' * 64,
|
||||
'verification': 'valid',
|
||||
'certificate_runtime_profile': 'shared-runtime-v1',
|
||||
'certificate_id': 'ed25519:trusted-issuer',
|
||||
'runtime_profile': 'shared-runtime-v1',
|
||||
'admission_code': 'CERTIFIED_PLUGIN_SHARED_ELIGIBLE',
|
||||
},
|
||||
},
|
||||
)
|
||||
connector._setting_for_plugin = AsyncMock(return_value=(TEST_EXECUTION_CONTEXT, setting))
|
||||
connector.ap.persistence_mgr.execute_async = AsyncMock(return_value=SimpleNamespace(rowcount=1))
|
||||
@@ -777,6 +788,7 @@ class TestSetPluginConfig:
|
||||
applied_binding,
|
||||
artifact_package=None,
|
||||
enabled=True,
|
||||
execution_mode=PluginExecutionMode.SHARED_CERTIFIED,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ from types import SimpleNamespace
|
||||
from unittest.mock import AsyncMock, Mock
|
||||
|
||||
import pytest
|
||||
from langbot_plugin.entities.io.context import InstallationBinding
|
||||
from langbot_plugin.entities.io.context import InstallationBinding, PluginExecutionMode
|
||||
from langbot_plugin.runtime.plugin.mgr import PluginInstallSource
|
||||
|
||||
from langbot.pkg.api.http.context import ExecutionContext
|
||||
@@ -45,7 +45,18 @@ def mock_archive_admission(connector: PluginRuntimeConnector, digest: str) -> No
|
||||
# is exercised by integration/plugin/test_certified_plugin_admission.py.
|
||||
connector._admit_plugin_archive = Mock(
|
||||
side_effect=lambda _package, info: (
|
||||
{**info, '_certification': {'normalized_digest': digest}},
|
||||
{
|
||||
**info,
|
||||
'_certification': {
|
||||
'artifact_digest': digest,
|
||||
'normalized_digest': digest,
|
||||
'verification': 'valid',
|
||||
'certificate_runtime_profile': 'shared-runtime-v1',
|
||||
'certificate_id': 'ed25519:trusted-issuer',
|
||||
'runtime_profile': 'shared-runtime-v1',
|
||||
'admission_code': 'CERTIFIED_PLUGIN_SHARED_ELIGIBLE',
|
||||
},
|
||||
},
|
||||
SimpleNamespace(for_installation=lambda _uuid: SimpleNamespace(artifact_digest=digest)),
|
||||
)
|
||||
)
|
||||
@@ -56,6 +67,7 @@ def plugin_setting(
|
||||
artifact_digest: str,
|
||||
*,
|
||||
durable: bool = True,
|
||||
certification: dict[str, str] | None = None,
|
||||
) -> SimpleNamespace:
|
||||
return SimpleNamespace(
|
||||
plugin_author='author',
|
||||
@@ -67,7 +79,10 @@ def plugin_setting(
|
||||
priority=0,
|
||||
created_at=datetime.datetime(2026, 1, 1),
|
||||
install_source='local',
|
||||
install_info={'_artifact_storage': 'tenant_binary_storage_v1'} if durable else {},
|
||||
install_info={
|
||||
**({'_artifact_storage': 'tenant_binary_storage_v1'} if durable else {}),
|
||||
**({'_certification': certification} if certification is not None else {}),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@@ -163,6 +178,45 @@ async def test_shared_reconnect_replays_two_workspaces_and_removes_missing_proje
|
||||
assert set(connector._known_desired_states) == {setting_a.installation_uuid}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_reconcile_reload_projects_certified_exact_artifact_to_shared_execution():
|
||||
binding = execution_binding('workspace-a')
|
||||
digest = 'a' * 64
|
||||
setting = plugin_setting(
|
||||
'01',
|
||||
digest,
|
||||
certification={
|
||||
'artifact_digest': digest,
|
||||
'normalized_digest': 'b' * 64,
|
||||
'verification': 'valid',
|
||||
'certificate_runtime_profile': 'shared-runtime-v1',
|
||||
'certificate_id': 'ed25519:trusted-issuer',
|
||||
'runtime_profile': 'shared-runtime-v1',
|
||||
'admission_code': 'CERTIFIED_PLUGIN_SHARED_ELIGIBLE',
|
||||
},
|
||||
)
|
||||
connector = shared_connector([[binding]], {'workspace-a': [setting]})
|
||||
connector.handler = runtime_handler()
|
||||
|
||||
await connector._prepare_connected_runtime()
|
||||
|
||||
desired = connector.handler.reconcile_plugin_installations.await_args.args[0][0]
|
||||
assert desired.execution_mode is PluginExecutionMode.SHARED_CERTIFIED
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_reconcile_reload_defaults_legacy_installation_to_dedicated_execution():
|
||||
binding = execution_binding('workspace-a')
|
||||
setting = plugin_setting('01', 'a' * 64)
|
||||
connector = shared_connector([[binding]], {'workspace-a': [setting]})
|
||||
connector.handler = runtime_handler()
|
||||
|
||||
await connector._prepare_connected_runtime()
|
||||
|
||||
desired = connector.handler.reconcile_plugin_installations.await_args.args[0][0]
|
||||
assert desired.execution_mode is PluginExecutionMode.DEDICATED
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_empty_projected_workspaces_do_not_retain_installation_sets():
|
||||
binding_a = execution_binding('workspace-a')
|
||||
@@ -223,6 +277,7 @@ async def test_fresh_shared_runtime_cache_replays_persisted_local_package():
|
||||
desired.binding,
|
||||
artifact_package=package,
|
||||
enabled=True,
|
||||
execution_mode=PluginExecutionMode.DEDICATED,
|
||||
)
|
||||
|
||||
|
||||
@@ -304,6 +359,7 @@ async def test_local_install_persists_verified_package_before_runtime_apply():
|
||||
binding,
|
||||
artifact_package=package,
|
||||
enabled=True,
|
||||
execution_mode=PluginExecutionMode.DEDICATED,
|
||||
)
|
||||
|
||||
|
||||
@@ -396,6 +452,9 @@ async def test_marketplace_upgrade_reports_multistep_progress():
|
||||
'download_current': 0,
|
||||
'download_speed': 0,
|
||||
}
|
||||
assert connector.handler.apply_plugin_installation.await_args.kwargs['execution_mode'] is (
|
||||
PluginExecutionMode.SHARED_CERTIFIED
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -430,7 +489,13 @@ async def test_workspace_reads_do_not_wait_for_an_installation_apply():
|
||||
connector._persist_installation_package = AsyncMock(return_value=(binding, None, False))
|
||||
connector._wait_for_installed_plugin_ready = AsyncMock()
|
||||
connector._load_workspace_desired_states = AsyncMock(
|
||||
return_value=[PluginInstallationDesiredState(binding=binding, enabled=True)]
|
||||
return_value=[
|
||||
PluginInstallationDesiredState(
|
||||
binding=binding,
|
||||
enabled=True,
|
||||
execution_mode=PluginExecutionMode.SHARED_CERTIFIED,
|
||||
)
|
||||
]
|
||||
)
|
||||
apply_started = asyncio.Event()
|
||||
release_apply = asyncio.Event()
|
||||
|
||||
@@ -10,7 +10,12 @@ from unittest.mock import AsyncMock, MagicMock, Mock
|
||||
import pytest
|
||||
|
||||
from langbot_plugin.entities.io.actions.enums import LangBotToRuntimeAction, PluginToRuntimeAction
|
||||
from langbot_plugin.entities.io.context import ActionContext, InstallationBinding, PluginInstallationDesiredState
|
||||
from langbot_plugin.entities.io.context import (
|
||||
ActionContext,
|
||||
InstallationBinding,
|
||||
PluginExecutionMode,
|
||||
PluginInstallationDesiredState,
|
||||
)
|
||||
|
||||
|
||||
def make_handler(app):
|
||||
@@ -90,7 +95,25 @@ async def test_reconcile_plugin_installations_accepts_configured_cold_start_time
|
||||
|
||||
await runtime_handler.reconcile_plugin_installations((desired,), timeout=900)
|
||||
|
||||
assert runtime_handler.call_action.await_args.kwargs["timeout"] == 900
|
||||
assert runtime_handler.call_action.await_args.kwargs['timeout'] == 900
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_apply_plugin_installation_serializes_certified_shared_execution_mode():
|
||||
runtime_handler = make_handler(SimpleNamespace())
|
||||
runtime_handler.send_file = AsyncMock(return_value='artifact-file')
|
||||
runtime_handler.call_action = AsyncMock(return_value={'state': 'starting'})
|
||||
binding = next(iter(runtime_handler._installation_bindings.values()))[0]
|
||||
|
||||
await runtime_handler.apply_plugin_installation(
|
||||
binding,
|
||||
artifact_package=b'package',
|
||||
enabled=True,
|
||||
execution_mode=PluginExecutionMode.SHARED_CERTIFIED,
|
||||
)
|
||||
|
||||
assert runtime_handler.call_action.await_args.args[0] == LangBotToRuntimeAction.APPLY_PLUGIN_INSTALLATION
|
||||
assert runtime_handler.call_action.await_args.args[1]['execution_mode'] == 'shared-runtime-v1'
|
||||
|
||||
|
||||
class TestHandlerQueryVariables:
|
||||
|
||||
@@ -1066,7 +1066,7 @@ name = "cuda-bindings"
|
||||
version = "13.3.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "cuda-pathfinder" },
|
||||
{ name = "cuda-pathfinder", marker = "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'" },
|
||||
]
|
||||
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" },
|
||||
@@ -1099,34 +1099,34 @@ wheels = [
|
||||
|
||||
[package.optional-dependencies]
|
||||
cudart = [
|
||||
{ name = "nvidia-cuda-runtime" },
|
||||
{ name = "nvidia-cuda-runtime", marker = "python_full_version >= '3.14' and sys_platform == 'linux'" },
|
||||
]
|
||||
cufft = [
|
||||
{ name = "nvidia-cufft" },
|
||||
{ name = "nvidia-cufft", marker = "python_full_version >= '3.14' and sys_platform == 'linux'" },
|
||||
]
|
||||
cufile = [
|
||||
{ name = "nvidia-cufile" },
|
||||
{ name = "nvidia-cufile", marker = "python_full_version >= '3.14' and sys_platform == 'linux'" },
|
||||
]
|
||||
cupti = [
|
||||
{ name = "nvidia-cuda-cupti" },
|
||||
{ name = "nvidia-cuda-cupti", marker = "python_full_version >= '3.14' and sys_platform == 'linux'" },
|
||||
]
|
||||
curand = [
|
||||
{ name = "nvidia-curand" },
|
||||
{ name = "nvidia-curand", marker = "python_full_version >= '3.14' and sys_platform == 'linux'" },
|
||||
]
|
||||
cusolver = [
|
||||
{ name = "nvidia-cusolver" },
|
||||
{ name = "nvidia-cusolver", marker = "python_full_version >= '3.14' and sys_platform == 'linux'" },
|
||||
]
|
||||
cusparse = [
|
||||
{ name = "nvidia-cusparse" },
|
||||
{ name = "nvidia-cusparse", marker = "python_full_version >= '3.14' and sys_platform == 'linux'" },
|
||||
]
|
||||
nvjitlink = [
|
||||
{ name = "nvidia-nvjitlink" },
|
||||
{ name = "nvidia-nvjitlink", marker = "python_full_version >= '3.14' and sys_platform == 'linux'" },
|
||||
]
|
||||
nvrtc = [
|
||||
{ name = "nvidia-cuda-nvrtc" },
|
||||
{ name = "nvidia-cuda-nvrtc", marker = "python_full_version >= '3.14' and sys_platform == 'linux'" },
|
||||
]
|
||||
nvtx = [
|
||||
{ name = "nvidia-nvtx" },
|
||||
{ name = "nvidia-nvtx", marker = "python_full_version >= '3.14' and sys_platform == 'linux'" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2185,7 +2185,7 @@ requires-dist = [
|
||||
{ name = "gewechat-client", specifier = ">=0.1.5" },
|
||||
{ name = "html2text", specifier = ">=2024.2.26" },
|
||||
{ name = "httpx", extras = ["socks"], specifier = ">=0.28.1" },
|
||||
{ name = "langbot-plugin", specifier = "==0.6.1" },
|
||||
{ name = "langbot-plugin", specifier = "==0.6.11" },
|
||||
{ name = "langchain", specifier = ">=1.3.9" },
|
||||
{ name = "langchain-core", specifier = ">=1.3.3" },
|
||||
{ name = "langchain-text-splitters", specifier = ">=1.1.2" },
|
||||
@@ -2255,7 +2255,7 @@ dev = [
|
||||
|
||||
[[package]]
|
||||
name = "langbot-plugin"
|
||||
version = "0.6.1"
|
||||
version = "0.6.11"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "aiofiles" },
|
||||
@@ -2276,9 +2276,9 @@ dependencies = [
|
||||
{ name = "watchdog" },
|
||||
{ name = "websockets" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/1f/70/ade1e2e71e666a80acfffde0fc753173e74b7a528ef495a1705f53c40ea8/langbot_plugin-0.6.1.tar.gz", hash = "sha256:214e415c2cd3c286f4b07cfbbe11e6b865839596ad500c2100944b00b3ba5f2b", size = 625357, upload-time = "2026-09-24T14:55:08.202Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/41/13/7f913f88494205abb1a6390746e88de4e1148fbf6d0a273bbe2450f5fb0c/langbot_plugin-0.6.11.tar.gz", hash = "sha256:2ecddc2ef3a2eb92925c08cf0f19887e9be573b93e7d5de49494c01c083cfac7", size = 655797, upload-time = "2026-09-26T18:15:19.81Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/42/06/4d8a889b0c28dcbf7ccf5ca40bcc7125ac9639942f7d05340768b28d159c/langbot_plugin-0.6.1-py3-none-any.whl", hash = "sha256:5607b4c787a92259b4a1f7beb4bdca9f371e37b316eaf8f316d2448e888da77e", size = 413828, upload-time = "2026-09-24T14:55:06.517Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/25/10/00e3d5d985d82aa08dd5f2f01aa2b85e8b46880dcfa2aa9b923f7be285da/langbot_plugin-0.6.11-py3-none-any.whl", hash = "sha256:85458919c943894780db85bbd96c0a61354e231bc396a3f74072a8eddc978b07", size = 427659, upload-time = "2026-09-26T18:15:18.003Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3306,7 +3306,7 @@ name = "nvidia-cublas"
|
||||
version = "13.1.1.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "nvidia-cuda-nvrtc" },
|
||||
{ name = "nvidia-cuda-nvrtc", marker = "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'" },
|
||||
]
|
||||
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" },
|
||||
@@ -3345,7 +3345,7 @@ name = "nvidia-cudnn-cu13"
|
||||
version = "9.20.0.48"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "nvidia-cublas" },
|
||||
{ name = "nvidia-cublas", marker = "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'" },
|
||||
]
|
||||
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" },
|
||||
@@ -3357,7 +3357,7 @@ name = "nvidia-cufft"
|
||||
version = "12.0.0.61"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "nvidia-nvjitlink" },
|
||||
{ name = "nvidia-nvjitlink", marker = "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'" },
|
||||
]
|
||||
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" },
|
||||
@@ -3387,9 +3387,9 @@ name = "nvidia-cusolver"
|
||||
version = "12.0.4.66"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "nvidia-cublas" },
|
||||
{ name = "nvidia-cusparse" },
|
||||
{ name = "nvidia-nvjitlink" },
|
||||
{ name = "nvidia-cublas", marker = "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'" },
|
||||
{ name = "nvidia-cusparse", marker = "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'" },
|
||||
{ name = "nvidia-nvjitlink", marker = "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'" },
|
||||
]
|
||||
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" },
|
||||
@@ -3401,7 +3401,7 @@ name = "nvidia-cusparse"
|
||||
version = "12.6.3.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "nvidia-nvjitlink" },
|
||||
{ name = "nvidia-nvjitlink", marker = "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'" },
|
||||
]
|
||||
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" },
|
||||
@@ -4494,7 +4494,7 @@ name = "pylibseekdb"
|
||||
version = "1.4.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "pymysql" },
|
||||
{ name = "pymysql", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" },
|
||||
]
|
||||
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" },
|
||||
@@ -5262,10 +5262,10 @@ name = "scikit-learn"
|
||||
version = "1.8.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "joblib" },
|
||||
{ name = "numpy" },
|
||||
{ name = "scipy" },
|
||||
{ name = "threadpoolctl" },
|
||||
{ name = "joblib", marker = "python_full_version >= '3.14'" },
|
||||
{ name = "numpy", marker = "python_full_version >= '3.14'" },
|
||||
{ name = "scipy", marker = "python_full_version >= '3.14'" },
|
||||
{ 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" }
|
||||
wheels = [
|
||||
@@ -5312,7 +5312,7 @@ name = "scipy"
|
||||
version = "1.17.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
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" }
|
||||
wheels = [
|
||||
@@ -5383,14 +5383,14 @@ name = "sentence-transformers"
|
||||
version = "5.2.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "huggingface-hub" },
|
||||
{ name = "numpy" },
|
||||
{ name = "scikit-learn" },
|
||||
{ name = "scipy" },
|
||||
{ name = "torch" },
|
||||
{ name = "tqdm" },
|
||||
{ name = "transformers" },
|
||||
{ name = "typing-extensions" },
|
||||
{ name = "huggingface-hub", marker = "python_full_version >= '3.14'" },
|
||||
{ name = "numpy", marker = "python_full_version >= '3.14'" },
|
||||
{ name = "scikit-learn", marker = "python_full_version >= '3.14'" },
|
||||
{ name = "scipy", marker = "python_full_version >= '3.14'" },
|
||||
{ name = "torch", marker = "python_full_version >= '3.14'" },
|
||||
{ name = "tqdm", marker = "python_full_version >= '3.14'" },
|
||||
{ name = "transformers", marker = "python_full_version >= '3.14'" },
|
||||
{ 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" }
|
||||
wheels = [
|
||||
@@ -5772,21 +5772,21 @@ name = "torch"
|
||||
version = "2.12.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "cuda-bindings", marker = "sys_platform == 'linux'" },
|
||||
{ name = "cuda-toolkit", extra = ["cudart", "cufft", "cufile", "cupti", "curand", "cusolver", "cusparse", "nvjitlink", "nvrtc", "nvtx"], marker = "sys_platform == 'linux'" },
|
||||
{ name = "filelock" },
|
||||
{ name = "fsspec" },
|
||||
{ name = "jinja2" },
|
||||
{ name = "networkx" },
|
||||
{ name = "nvidia-cublas", marker = "sys_platform == 'linux'" },
|
||||
{ name = "nvidia-cudnn-cu13", marker = "sys_platform == 'linux'" },
|
||||
{ name = "nvidia-cusparselt-cu13", marker = "sys_platform == 'linux'" },
|
||||
{ name = "nvidia-nccl-cu13", marker = "sys_platform == 'linux'" },
|
||||
{ name = "nvidia-nvshmem-cu13", marker = "sys_platform == 'linux'" },
|
||||
{ name = "setuptools" },
|
||||
{ name = "sympy" },
|
||||
{ name = "triton", marker = "sys_platform == 'linux'" },
|
||||
{ name = "typing-extensions" },
|
||||
{ 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 = "python_full_version >= '3.14' and sys_platform == 'linux'" },
|
||||
{ name = "filelock", marker = "python_full_version >= '3.14'" },
|
||||
{ name = "fsspec", marker = "python_full_version >= '3.14'" },
|
||||
{ name = "jinja2", marker = "python_full_version >= '3.14'" },
|
||||
{ name = "networkx", marker = "python_full_version >= '3.14'" },
|
||||
{ name = "nvidia-cublas", marker = "python_full_version >= '3.14' and sys_platform == 'linux'" },
|
||||
{ name = "nvidia-cudnn-cu13", marker = "python_full_version >= '3.14' and sys_platform == 'linux'" },
|
||||
{ name = "nvidia-cusparselt-cu13", marker = "python_full_version >= '3.14' and sys_platform == 'linux'" },
|
||||
{ name = "nvidia-nccl-cu13", marker = "python_full_version >= '3.14' and sys_platform == 'linux'" },
|
||||
{ name = "nvidia-nvshmem-cu13", marker = "python_full_version >= '3.14' and sys_platform == 'linux'" },
|
||||
{ name = "setuptools", marker = "python_full_version >= '3.14'" },
|
||||
{ name = "sympy", marker = "python_full_version >= '3.14'" },
|
||||
{ name = "triton", marker = "python_full_version >= '3.14' and sys_platform == 'linux'" },
|
||||
{ name = "typing-extensions", marker = "python_full_version >= '3.14'" },
|
||||
]
|
||||
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" },
|
||||
@@ -5828,15 +5828,15 @@ name = "transformers"
|
||||
version = "5.3.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "huggingface-hub" },
|
||||
{ name = "numpy" },
|
||||
{ name = "packaging" },
|
||||
{ name = "pyyaml" },
|
||||
{ name = "regex" },
|
||||
{ name = "safetensors" },
|
||||
{ name = "tokenizers" },
|
||||
{ name = "tqdm" },
|
||||
{ name = "typer" },
|
||||
{ name = "huggingface-hub", marker = "python_full_version >= '3.14'" },
|
||||
{ name = "numpy", marker = "python_full_version >= '3.14'" },
|
||||
{ name = "packaging", marker = "python_full_version >= '3.14'" },
|
||||
{ name = "pyyaml", marker = "python_full_version >= '3.14'" },
|
||||
{ name = "regex", marker = "python_full_version >= '3.14'" },
|
||||
{ name = "safetensors", marker = "python_full_version >= '3.14'" },
|
||||
{ name = "tokenizers", marker = "python_full_version >= '3.14'" },
|
||||
{ name = "tqdm", marker = "python_full_version >= '3.14'" },
|
||||
{ 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" }
|
||||
wheels = [
|
||||
@@ -6097,9 +6097,9 @@ name = "valkey-glide"
|
||||
version = "2.4.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "anyio" },
|
||||
{ name = "protobuf" },
|
||||
{ name = "sniffio" },
|
||||
{ name = "anyio", marker = "sys_platform != 'win32'" },
|
||||
{ name = "protobuf", marker = "sys_platform != 'win32'" },
|
||||
{ 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" }
|
||||
wheels = [
|
||||
|
||||
Reference in New Issue
Block a user