The certified-archive admission gate treated any declared certificate it could not resolve as an untrusted archive and rejected the install with CERTIFIED_PLUGIN_OSS_FORCE_REQUIRED. OSS ships an empty plugin.certification.trusted_public_keys ring and the marketplace signs every package with its own issuer key, so all certified marketplace packages (for example langbot-team/RunnerDemo and langbot-team/LocalAgent) failed at the 'validating plugin package' step before artifact storage. Admission now distinguishes an unresolvable declaration from a configured trust decision that fails: - record certificate_id only when the key_id is actually present in the configured ring, so an empty ring yields an unresolvable declaration; - OSS degrades an unresolvable declaration to the existing oss_dev dedicated profile with CERTIFIED_PLUGIN_OSS_UNTRUSTED_DEDICATED instead of blocking; - a resolvable declaration that still fails keeps requiring the explicit administrator force (CERTIFIED_PLUGIN_OSS_FORCE_REQUIRED); - Cloud stays fail-closed and rejects before storage. No shared-runtime privilege is granted when the issuer is not trusted, so this withholds isolation rather than escalating it.
5.3 KiB
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:
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.
The ring may also be supplied out-of-band, which is how hosted deployments provision it:
PLUGIN__CERTIFICATION__TRUSTED_PUBLIC_KEYS_JSON='{"ed25519:issuer":"<base64>"}'
An empty ring is a supported state, not a misconfiguration. OSS defaults to it, so a self-hosted instance that has not provisioned any issuer key still installs packages (see the admission matrix below). Configure the ring to grant the shared-runtime profile; leave it empty to keep every package on the dedicated profile.
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 | 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 |
The OSS row that matters for availability is the last one. Marketplace
packages are signed by the marketplace issuer and declare
shared-runtime-v1, while OSS ships an empty key ring by default. Treating that
as a rejection made every certified marketplace package uninstallable with
CERTIFIED_PLUGIN_OSS_FORCE_REQUIRED before artifact storage. Because the
certificate is signed by an issuer the instance does not declare trusted, no
shared-runtime privilege may be granted, so admission degrades the install to
the existing oss_dev dedicated profile and records
CERTIFIED_PLUGIN_OSS_UNTRUSTED_DEDICATED. This is not an escalation: it
withholds the shared profile rather than granting it.
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.
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.