mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-09-29 21:06:41 +08:00
feat: stateless certified shared worker
Implements stateless singleton component model for certified shared Workers, integrating with SDK 0.7.0.
This commit is contained in:
@@ -10,7 +10,7 @@ ZIP digest without extracting the payload.
|
||||
|
||||
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
|
||||
profile, `stateless-v1` component model, 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.
|
||||
@@ -48,11 +48,11 @@ dedicated profile.
|
||||
|
||||
| Deployment | SDK verification | Explicit `administrator_force` | Result |
|
||||
| --- | --- | --- | --- |
|
||||
| Cloud | valid envelope declaring `shared-runtime-v1` | any | admitted to the shared profile |
|
||||
| Cloud | valid envelope declaring `shared-runtime-v1` + `stateless-v1` | any | admitted to the shared singleton profile |
|
||||
| Cloud | absent | any | reject before storage with `CERTIFIED_PLUGIN_CLOUD_CERTIFICATE_REQUIRED` |
|
||||
| Cloud | malformed, untrusted, invalid, or non-shared | any | reject before storage with `CERTIFIED_PLUGIN_CLOUD_CERTIFICATE_INVALID` |
|
||||
| OSS | absent legacy envelope | any | admitted to the dedicated profile |
|
||||
| OSS | valid envelope declaring `shared-runtime-v1` | any | selected shared profile |
|
||||
| OSS | valid envelope declaring `shared-runtime-v1` + `stateless-v1` | any | selected shared singleton profile |
|
||||
| OSS | declaration signed by a **key this instance resolves** | false | reject with `CERTIFIED_PLUGIN_OSS_FORCE_REQUIRED` |
|
||||
| OSS | declaration signed by a **key this instance resolves** | true | admitted to the dedicated profile |
|
||||
| OSS | declaration this instance **cannot resolve** (empty ring) | any | admitted to the dedicated profile |
|
||||
@@ -69,9 +69,10 @@ the existing `oss_dev` dedicated profile and records
|
||||
withholds the shared profile rather than granting it.
|
||||
|
||||
A declaration is "resolvable" only when its `key_id` is present in the
|
||||
configured ring. When the ring is configured and the declaration still fails
|
||||
(malformed, `signature_invalid`, `digest_mismatch`, `unsupported_schema`, ...),
|
||||
admission stays explicit and requires `administrator_force`.
|
||||
configured ring. A parseable declaration from a resolved key that fails
|
||||
signature, digest, identity, profile, or component-model validation requires
|
||||
`administrator_force` in OSS. Malformed or unsupported envelopes without a
|
||||
resolved certificate identity are treated as untrusted and stay dedicated.
|
||||
|
||||
`administrator_force` is deliberately strict: it is recognized only when the
|
||||
install request carries boolean `true`. The local upload endpoint accepts the
|
||||
@@ -81,8 +82,9 @@ protects those endpoints. A force never creates a Cloud dedicated fallback.
|
||||
|
||||
## Runtime and logs
|
||||
|
||||
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 first stateless-compatible SDK release will carry the v2 certificate and
|
||||
singleton component contract; its final version is assigned only at release.
|
||||
Core must pin that release before sending or selecting the new contract. 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
|
||||
@@ -90,6 +92,13 @@ 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 certificate must also bind `component_model=stateless-v1`. This profile
|
||||
creates one `BasePlugin` and one instance of each component per digest Worker.
|
||||
Installation slots contain immutable config snapshots and authority only;
|
||||
task-local invocation context selects the active slot. Older certificates that
|
||||
do not bind the component model are not eligible for shared placement. Their
|
||||
source packages remain compatible on dedicated Workers under OSS policy.
|
||||
|
||||
The existing public plugin-log boundary already applies the immutable
|
||||
installation binding (including workspace UUID) through
|
||||
`RuntimeConnectionHandler.installation_scope()` before requesting logs. This is
|
||||
@@ -100,5 +109,7 @@ same existing installation scope.
|
||||
|
||||
## SDK versioning
|
||||
|
||||
Core pins `langbot-plugin==0.6.2`, the first published SDK release carrying the
|
||||
canonical installation execution-mode contract.
|
||||
Ship in dependency order: SDK v2 certificate support, then Core exact pin and
|
||||
lockfile, then Space signing, then Runtime/Core rollout. Legacy v1 envelopes
|
||||
remain verifiable but do not carry `stateless-v1`, so they never select shared
|
||||
singleton placement without re-review and v2 re-signing.
|
||||
|
||||
+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.20",
|
||||
"langbot-plugin==0.7.0",
|
||||
"asyncpg>=0.30.0",
|
||||
"line-bot-sdk>=3.19.0",
|
||||
"matrix-nio>=0.25.2",
|
||||
|
||||
@@ -18,6 +18,7 @@ from langbot_plugin.entities.io.context import PluginExecutionMode
|
||||
|
||||
|
||||
SHARED_RUNTIME_V1 = 'shared-runtime-v1'
|
||||
STATELESS_COMPONENT_MODEL_V1 = 'stateless-v1'
|
||||
DEDICATED_RUNTIME = 'dedicated'
|
||||
|
||||
|
||||
@@ -66,11 +67,16 @@ class CertificateFacts:
|
||||
|
||||
verification: CertificateVerification
|
||||
runtime_profile: str | None = None
|
||||
component_model: 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
|
||||
return (
|
||||
self.verification is CertificateVerification.VALID
|
||||
and self.runtime_profile == SHARED_RUNTIME_V1
|
||||
and self.component_model == STATELESS_COMPONENT_MODEL_V1
|
||||
)
|
||||
|
||||
@property
|
||||
def is_declared(self) -> bool:
|
||||
@@ -153,6 +159,7 @@ def verify_plugin_archive_certificate(
|
||||
verification = verify_archive(archive, key_ring.get)
|
||||
envelope = verification.envelope
|
||||
runtime_profile = envelope.shared_runtime if envelope is not None else None
|
||||
component_model = envelope.component_model if envelope is not None else None
|
||||
# Record the issuer identity only when this instance actually resolved it
|
||||
# through the configured ring. When the ring is empty (for example a
|
||||
# self-hosted deployment that never configured
|
||||
@@ -170,6 +177,7 @@ def verify_plugin_archive_certificate(
|
||||
certificate=CertificateFacts(
|
||||
verification=state,
|
||||
runtime_profile=runtime_profile,
|
||||
component_model=component_model,
|
||||
certificate_id=certificate_id,
|
||||
),
|
||||
)
|
||||
@@ -299,6 +307,7 @@ def execution_mode_for_persisted_installation(
|
||||
'artifact_digest': artifact_digest,
|
||||
'verification': CertificateVerification.VALID.value,
|
||||
'certificate_runtime_profile': SHARED_RUNTIME_V1,
|
||||
'certificate_component_model': STATELESS_COMPONENT_MODEL_V1,
|
||||
'runtime_profile': SHARED_RUNTIME_V1,
|
||||
'admission_code': AdmissionCode.SHARED_ELIGIBLE.value,
|
||||
}
|
||||
|
||||
@@ -1809,6 +1809,7 @@ class PluginRuntimeConnector(ManagedRuntimeConnector):
|
||||
'normalized_digest': facts.artifact_digest,
|
||||
'verification': facts.certificate.verification.value,
|
||||
'certificate_runtime_profile': facts.certificate.runtime_profile,
|
||||
'certificate_component_model': facts.certificate.component_model,
|
||||
'certificate_id': facts.certificate.certificate_id,
|
||||
'runtime_profile': decision.runtime_profile,
|
||||
'admission_code': decision.code.value,
|
||||
|
||||
@@ -370,7 +370,7 @@ def _legacy_shared_certification(**overrides):
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_certification_artifact_digest_backfill_is_safe_and_enables_shared_placement(convergence_engine):
|
||||
async def test_certification_artifact_digest_backfill_is_safe_and_keeps_legacy_dedicated(convergence_engine):
|
||||
engine = convergence_engine
|
||||
async with engine.begin() as conn:
|
||||
await conn.run_sync(Base.metadata.create_all)
|
||||
@@ -461,7 +461,7 @@ async def test_certification_artifact_digest_backfill_is_safe_and_enables_shared
|
||||
artifact_digest=eligible_digest,
|
||||
install_info=eligible_info,
|
||||
)
|
||||
is PluginExecutionMode.SHARED_CERTIFIED
|
||||
is PluginExecutionMode.DEDICATED
|
||||
)
|
||||
|
||||
for name, (original_certification, artifact_digest) in rows.items():
|
||||
|
||||
@@ -249,7 +249,10 @@ def _connector(deployment: str, trusted_public_keys: dict[str, str]):
|
||||
def _archive(kind: str) -> tuple[bytes, dict[str, str]]:
|
||||
manifest = {
|
||||
'metadata': {'author': 'certified', 'name': 'example', 'version': '1.0.0'},
|
||||
'execution': {'sharedRuntime': 'shared-runtime-v1'},
|
||||
'execution': {
|
||||
'sharedRuntime': 'shared-runtime-v1',
|
||||
'componentModel': 'stateless-v1',
|
||||
},
|
||||
}
|
||||
if kind == 'legacy':
|
||||
manifest.pop('execution')
|
||||
|
||||
@@ -85,6 +85,11 @@ def test_admission_policy_enforces_certification_matrix(
|
||||
certificate=CertificateFacts(
|
||||
verification=CertificateVerification(verification),
|
||||
runtime_profile=runtime_profile,
|
||||
component_model=(
|
||||
'stateless-v1'
|
||||
if verification == 'valid' and runtime_profile == 'shared-runtime-v1'
|
||||
else None
|
||||
),
|
||||
certificate_id=certificate_id,
|
||||
),
|
||||
)
|
||||
@@ -102,6 +107,33 @@ def test_admission_policy_enforces_certification_matrix(
|
||||
)
|
||||
|
||||
|
||||
def test_legacy_shared_certificate_without_stateless_contract_is_not_shared() -> None:
|
||||
from langbot.pkg.plugin.certification import (
|
||||
AdmissionDisposition,
|
||||
CertificateFacts,
|
||||
CertificateVerification,
|
||||
DeploymentMode,
|
||||
PluginCertificationFacts,
|
||||
decide_plugin_admission,
|
||||
)
|
||||
|
||||
decision = decide_plugin_admission(
|
||||
deployment=DeploymentMode.CLOUD,
|
||||
facts=PluginCertificationFacts(
|
||||
installation_uuid='00000000-0000-4000-8000-000000000001',
|
||||
artifact_digest='a' * 64,
|
||||
certificate=CertificateFacts(
|
||||
verification=CertificateVerification.VALID,
|
||||
runtime_profile='shared-runtime-v1',
|
||||
component_model=None,
|
||||
certificate_id='legacy-issuer',
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
assert decision.disposition is AdmissionDisposition.REJECTED
|
||||
|
||||
|
||||
def test_archive_inspection_preserves_legacy_tuple_and_exposes_certificate_facts() -> None:
|
||||
from langbot.pkg.plugin.archive import (
|
||||
ArchiveCertificateState,
|
||||
@@ -181,6 +213,7 @@ def test_log_visibility_policy_only_scopes_valid_shared_certifications(
|
||||
certificate=CertificateFacts(
|
||||
verification=CertificateVerification(verification),
|
||||
runtime_profile='shared-runtime-v1',
|
||||
component_model='stateless-v1',
|
||||
),
|
||||
)
|
||||
|
||||
@@ -195,6 +228,7 @@ def _complete_persisted_certification() -> dict[str, object]:
|
||||
'normalized_digest': 'b' * 64,
|
||||
'verification': 'valid',
|
||||
'certificate_runtime_profile': 'shared-runtime-v1',
|
||||
'certificate_component_model': 'stateless-v1',
|
||||
'certificate_id': 'ed25519:trusted-issuer',
|
||||
'runtime_profile': 'shared-runtime-v1',
|
||||
'admission_code': 'CERTIFIED_PLUGIN_SHARED_ELIGIBLE',
|
||||
@@ -261,6 +295,7 @@ def test_persisted_certification_selects_shared_execution_only_for_exact_admitte
|
||||
'normalized_digest',
|
||||
'verification',
|
||||
'certificate_runtime_profile',
|
||||
'certificate_component_model',
|
||||
'certificate_id',
|
||||
'runtime_profile',
|
||||
'admission_code',
|
||||
@@ -303,6 +338,9 @@ def test_persisted_certification_requires_every_shared_admission_fact(missing_fi
|
||||
('certificate_runtime_profile', None),
|
||||
('certificate_runtime_profile', 123),
|
||||
('certificate_runtime_profile', ''),
|
||||
('certificate_component_model', None),
|
||||
('certificate_component_model', 123),
|
||||
('certificate_component_model', ''),
|
||||
('runtime_profile', None),
|
||||
('runtime_profile', 123),
|
||||
('runtime_profile', ''),
|
||||
|
||||
@@ -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.20" },
|
||||
{ name = "langbot-plugin", specifier = "==0.7.0" },
|
||||
{ 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.20"
|
||||
version = "0.7.0"
|
||||
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/dd/bb/08bfa37c29a9b6823678e0b837adddacab162b982b36737e0b5e7bea6c40/langbot_plugin-0.6.20.tar.gz", hash = "sha256:86b3803fb83e67379db4a6cf166dec67a8233fbfbc1dbbf744b2b765d0615879", size = 663741, upload-time = "2026-09-27T08:56:36.473Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/43/0a/b06aea51c8fe2611952e920002933046888ca20f040e27ef5d14531376ec/langbot_plugin-0.7.0.tar.gz", hash = "sha256:4709cbeedf5abea8b0bd1cccf8449857c1b22e81f843c5ac0db349795f216732", size = 669519, upload-time = "2026-09-28T04:35:18.244Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/48/28/f537c212f67afd3820bb56920b9b29783b68096fb6b2ca1a388d268dd27c/langbot_plugin-0.6.20-py3-none-any.whl", hash = "sha256:b110878d880b24ac739b26b3eb158067fb4d0be8b55ba3baf97d2092da920f10", size = 429929, upload-time = "2026-09-27T08:56:34.995Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b9/1e/2177341166347aa7e594eaf5cdf7cae56eb1087b63c696edcdae9e598eb2/langbot_plugin-0.7.0-py3-none-any.whl", hash = "sha256:5c3c9a22b0dc7072467fb5901abdeb2f24a6a07d53662ce3eaf666b1bf4c3c59", size = 432251, upload-time = "2026-09-28T04:35:16.744Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
Reference in New Issue
Block a user