mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-09-29 12:56:38 +08:00
afe700f1d8
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.
105 lines
5.3 KiB
Markdown
105 lines
5.3 KiB
Markdown
# 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.
|
|
|
|
The ring may also be supplied out-of-band, which is how hosted deployments
|
|
provision it:
|
|
|
|
```bash
|
|
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.
|