Expand description
plecto-control — the control plane (ADR 000007 / 000008).
A single declarative TOML manifest pins filters by OCI digest and declares one chain. An
ArtifactStore resolves each filter from a local, offline OCI image-layout (remote
registry fetch via wkg is an out-of-band operator step, kept out of the runtime), the
Host loads it through the ADR 000006 provenance gate, and a chain dispatcher drives a
request through the loaded filters. reload rebuilds the set and swaps it atomically
(ArcSwap): new requests see the new set, in-flight holders keep the old one until it
drops. Single-node-first (ADR 000008).
The trust policy lives on the Host and is fixed at construction; changing trust roots
requires a new Control. reload swaps only the filter set + chain (same Host, same
epoch ticker), so a runaway filter stays bounded across reloads.
Modules§
- oci
- Offline OCI image-layout
ArtifactStore(ADR 000007). Reads a local image-layout directory — no registry, no network — per the CNCF Wasm OCI Artifact layout, verifies the image-manifest digest against the manifest pin, and extracts the component plus its bundled signature / SBOM layers (custom mediaTypes). Remote fetch (wkg) is an out-of-band operator step that produces such a layout. - otlp
- ADR 000040: the OTLP trace-export half of host observability. Two pieces, both dependency-less (the same reasoning as the hand-written Prometheus exposition — the trace protos are Stable/frozen in opentelemetry-proto, so field numbers never change):
Structs§
- Chain
- The single ordered chain for v0.1 (named chains / route matching are deferred to the fast-path server).
- Circuit
Breaker - Per-upstream circuit-breaker thresholds (ADR 000028). Overload protection that is deliberately SEPARATE from outlier detection / health (ADR 000017): health answers “is this instance up?”, the breaker answers “is this upstream saturated?”. A request rejected by the breaker is the upstream shedding load, not an instance failing — so it never demotes an instance.
- Compression
Config - A route’s compiled
[route.compression](ADR 000074): the offered codings in server-preference order, the min-length floor, and the content-type allowlist (lower-cased at build so the per-response check is an allocation-free case-insensitive scan over a short list). - Config
Snapshot - A configuration pinned for one request transaction. Obtain via
crate::Control::snapshot; runon_requestthen (later)on_responseagainst the same snapshot so a reload cannot desync the two halves. - Conformance
Check - One named property, pass/fail, with a human-readable detail (the failure reason, or a short confirmation on success).
- Conformance
Report - The full battery’s result.
plecto conformanceexits non-zero unlessConformanceReport::is_conformantis true. - Control
- The control plane: owns the
Host(and thus the trust policy + epoch ticker) and the artifact store, and holds the active filter set behind anArcSwapfor lock-free reads and atomic reload.manifest_pathisSomeonly when the plane was built from an on-disk manifest — that is whatreload_from_disk(and the SIGHUP loop) re-reads. - DevSigner
- A persistent ECDSA P-256 / cosign-scheme signer. Holds one key pair; the same key signs
both a filter component and its SBOM, matching a
TrustPolicythat trusts exactly that key. - Diagnostic
- A stable four-part diagnostic.
docsis a repo-relative path (ADR 000065: “docs 参照は当面 リポジトリ内相対” — no dedicated docs domain yet), printed as a hint, not resolved from disk. - Endpoints
- The swappable endpoint set of a group: the instances plus the LB state compiled from them
(a Maglev table indexes into
instances, so the two must swap together). Behind anArcSwapon the group so periodic DNS re-resolution (the standard periodic-DNS endpoint-discovery technique — the shape of nginxresolve/ Envoy STRICT_DNS) can replace the set in place while routes keep holding theirArc<UpstreamGroup>. - FanOut
Sink - Send every span to several sinks (e.g. export + aggregate at once). The host holds one sink; this composes many behind it.
- Filter
Entry - One filter to load, pinned by OCI digest.
- Filter
Span - One filter execution, as a span in the OTel data model. The host builds and emits one of
these per
on_request/on_responsecall; aTelemetrySinkreceives it. - Header
- A single HTTP header field.
- Health
Config - Active-health-check policy (ADR 000017). A background prober issues
GET {path}to each instance everyinterval_ms— on the instance’s own traffic port, unlessportnames a dedicated health-check port — and a 2xx withintimeout_msis a success, anything else (non-2xx, timeout, connect error) a failure.unhealthy_thresholdconsecutive failures eject a healthy instance from the rotation;healthy_thresholdconsecutive successes restore an ejected one. Instances start pessimistic (unhealthy); the FIRST successful probe alone promotes a never-yet-healthy instance (cold-start fast path), after which the fullhealthy_thresholdgoverns re-entry. Onlypathis required; the rest default.PartialEqlets a reload detect a changed policy and re-probe the upstream’s instances from scratch (ADR 000017 reconcile). - Host
- The wasmtime host: two engines (one per isolation mode) plus the shared state backend. One per process/worker.
- Http
Request - The request as seen by a filter. Header-only in v0.1.0 (no body).
- Http
Response - A response a filter synthesises when it short-circuits the chain.
- InMemory
Sink - A reference / test sink that retains up to
MAX_RETAINED_SPANSspans in memory. - Manifest
- A parsed manifest. Deserialised from TOML; no I/O happens here (key files and artifacts
are resolved by
Control).Serializeexists only to derive the semantic content hash (content_hash) — the canonical, representation-independent identity of the config. - Memory
Store - An in-memory
ArtifactStorekeyed by source name — the reference / test store. Its content digest issha256over the component bytes, so the pin mechanism is exercised without an on-disk OCI layout. - Metrics
Sink - Host-aggregated RED-style metrics (ADR 000009 “metrics are host-aggregated”), derived from
the span stream in-process: a sink that tallies rather than retains. Errors are the
RunErroroutcomes (trap / deadline / instantiate / unavailable); short-circuits are counted separately (a filter blocking is not a fault). - Metrics
Snapshot - A point-in-time read of
MetricsSink. - Noop
Sink - The default: observability off, zero cost.
- Observability
- Operational observability config (
[observability], ADR 000009 Stage A): a separate admin listener exposing Prometheus metrics + liveness/readiness, and an opt-in structured access log. Off by default — Plecto stays quiet and exposes nothing extra unless asked (operational simplicity). Captured at construction; a reload does not re-bind the admin listener. - Outlier
Detection - Per-upstream outlier-detection policy (ADR 000032). A THIRD resilience axis: active health (ADR 000017) asks “is the instance reachable?”, the circuit breaker (ADR 000028) “is the upstream saturated?”, and this “is the instance misbehaving on live traffic?”. An ejected instance leaves the rotation for a (backing-off) ejection window, independent of the health bit; a shed request (circuit breaker) never feeds it, only real upstream 5xx responses do.
- Pick
- A chosen instance plus a guard tracking its load (ADR 000035). For
least_requestthe guard holds the selected instance’s incremented active-request count and decrements it on drop — on EVERY forward return path (success, retry, transport error) and on each retry hand-off, because replacing thePickdrops the previous guard. For round-robin / maglev the guard is a no-op.Derefs to the instance so callers readaddress()/is_healthy()directly. - Proxy
Protocol Trust - The parsed, runtime form of
[listen.proxy_protocol]: the trusted networks behind a containment check. The fast path askscontains(peer.ip())and never re-parses CIDRs; keeping the match here keeps the canonicalisation rule in one place. - Request
Trace - The host-owned trace context for one request transaction. Created by the chain driver (the
ConfigSnapshot/ fast-path server), it parents every filter span so the whole chain — request side and response side — shares one trace. Cheap to copy. - Resolved
Artifact - The verified bytes a filter loads from: the component plus its cosign signature, and the (mandatory) SBOM plus its signature (ADR 000006). Produced after the digest-pin check.
- Route
- One routing rule (ADR 000013 / 000034): match a request by the
[route.match]dimensions, run an inline chain offilters, and forward to a singleupstream(shorthand) or a weighted set ofbackends(traffic split / canary).strip_prefixis a host-native path rewrite applied to the forwarded request only (the chain still sees the original path) — the common reverse-proxy prefix-strip, without aplecto:filtercontract change.filters/strip_prefix/rate_limitare per-route: they apply identically across every backend (ADR 000034 keeps a backend a pure{upstream, weight}pair; a route needing different policy per target uses a separate route). - Route
Compression - A route’s compression declaration (
[route.compression], ADR 000074). Every field has a safe default, so the bare block header is the whole opt-in; the fields exist to narrow it. - Route
Info - What
crate::ConfigSnapshot::find_routehands the fast-path server: which route matched (index, used to dispatch its chain) plus the data needed to forward — the weighted backends (the server picks a group, then a healthy instance from it) and the optional prefix strip. Owned /Arc-shared so it survives a move intospawn_blocking. - Route
Rate Limit - Native per-route rate-limit spec (ADR 000033), declared as
[route.rate_limit]. A coarse token bucket the fast path consults before forwarding — the Tier-0 baseline a filterless route otherwise lacks.rate/burstmap onto the same token-bucket math ashost-ratelimit(capacity = burst, refillratetokens every second), but the surface is deliberately the friendlier two-knob (rate + burst) form, since this is a blunt floor, not a policy limiter. - Signal
Reload Source - The real trigger: SIGHUP, the operator’s “I edited the manifest, pick it up” signal. Unix
only — signals are a unix concept. Construct once, then hand to
serve_reloads. - State
- The host state backend (
[state], ADR 000041): the single knob choosing where the three state capabilities (host-kv/host-counter/host-ratelimit) keep their bytes.memory(the default) keeps zero-config startup — state dies with the process;redbmakes it durable atpath, so counters and rate-limit windows survive a restart (fail-closed direction, ADR 000004). One backend serves all three capabilities; fixed at construction like[trust](PartialEqbacks the reload rejection — restart to apply). - TlsCert
- One TLS server certificate (ADR 000014). The fast path terminates TLS with rustls and selects
a cert by SNI:
hostnames the SNI this cert serves (case-insensitive);Noneis the default cert presented when no SNI matches.cert_path/key_pathare manifest-relative PEM files (a cert chain and its private key). Only the paths ride the manifest content hash, so a path change reloads but an in-place file edit does not (ADR 000014). - TlsClient
Config - The rustls TLS client config the fast path re-encrypts upstream forward legs with
(ADR 000042), re-exported for the same reason as
TlsServerConfig. Common configuration for (typically) all connections made by a program. - TlsServer
Config - The rustls TLS server config the fast path terminates with (ADR 000014), re-exported so
plecto-servernames the samerustlstype the control plane built. Common configuration for a set of server sessions. - Trust
- Trust roots: paths (manifest-relative) to trusted signer public keys, PEM (ADR 000006).
PartialEqletsreloaddetect a trust-section change (which it rejects — trust is fixed at construction, f000004 #1). - Trust
Policy - The set of public keys the operator trusts to sign filters (ADR 000006 provenance). A
filter loads only if a trusted key verifies BOTH its component signature and its SBOM
signature (keyed cosign, offline — no Fulcio / Rekor / network). An empty policy
trusts no one, so nothing loads: deny-by-default / fail-closed, with no “allow unsigned”
escape hatch in the production API. The keys live on the
Host, not on eachloadcall, so the operator manages one trust root. - Upgrade
Config - A route’s compiled
[route.upgrade](ADR 000048): the lower-cased token allowlist and the tunnel idle timeout. Pre-normalised at build so the per-request check is a scan over a (typically one-element) list. - Upstream
- A named upstream the fast-path server forwards a matched request to (ADR 000013 / 000017).
One or more
addresses(host:port, no scheme) are the upstream’s instances; the fast path balances across the healthy ones perlb_algorithm. The forward leg is plain HTTP/1.1 unless[upstream.tls]is declared, which re-encrypts with rustls and lets ALPN negotiate h2 / http/1.1 (ADR 000042). Every upstream carries an active-health-check policy (health) — required, because instances start pessimistic (unhealthy) and only a passing probe puts one into rotation (ADR 000017). - Upstream
Group - A named upstream: its endpoint set, the round-robin cursor, and the health policy (ADR 000017).
- Upstream
Instance - One backend instance (
host:port) of an upstream, with its health state (ADR 000017). - Upstream
Registry - The live set of upstreams, keyed by name. Owned by
Control, OUTSIDE the swappedActiveConfig, so health state survives a reload (ADR 000017). TheMutexis contended only byreconcile(on reload) and the prober supervisor (groups) / a config build (group) — never the per-request hot path, which holds anArc<UpstreamGroup>resolved at build time. - Validate
Outcome validate_manifest/validate_manifest_path’s success value: the manifest’s config version plus any non-fatalDiagnosticwarnings (ADR 000065 decision 5) — currently justDEV_KEY_IN_TRUST, raised when a[trust]key file carriesplecto_host::DEV_KEY_MARKER. A warning never fails validation: aplecto dev-generated manifest is SUPPOSED to trip it.
Enums§
- Chain
Outcome - The result of driving a request through the chain.
- Compression
Algorithm - A content coding Plecto can produce (ADR 000074: gzip baseline + zstd / brotli). The serde
spelling is the wire token (
Accept-Encoding/Content-Encoding), so the manifest reads exactly like the negotiation it configures. - Control
Error - DevKey
Error - Typed dev-key errors (bp-rust: a library’s public surface stays
thiserror;anyhowis for binary entry points). The CLI callers absorb this intoanyhowat their edge. - Hash
Input - A request attribute to hash for Maglev affinity (ADR 000035), borrowed so the hot path allocates
nothing: a header value’s bytes (borrowed from the request) or the peer IP (hashed as its
canonical octets, not a string). The fast path builds this from a group’s
HashKeySource.Copyso it can be passed to the initialpickand each retry’spick_excludingunchanged. - Hash
KeySource - The request attribute a Maglev upstream hashes for affinity (ADR 000035), resolved from the
manifest
[upstream.hash]. The fast path turns this into aHashInputper request. - Isolation
Kind - Manifest spelling of the host’s
Isolation. Defaults tountrusted(fail-closed). - Rate
Limit Decision - The fast path’s view of a rate-limit consult (ADR 000033).
- Rate
Limit KeyKind - The dimension a native route rate limit counts against (ADR 000033).
- Reload
Outcome - Outcome of a single disk reload. A failed reload is not represented here — it is the
Errarm ofreload_from_disk, and leaves the running set untouched (all-or-nothing, fail-closed). The success arm distinguishes “nothing to do” from “swapped”. - Request
Body Outcome - The result of driving a buffered request body through the chain’s
on-request-bodyhooks (ADR 000025). - Response
Outcome - The result of driving a response back through the chain (ADR 000073): the typed successor of the old in-band “non-empty body means synthetic” signal.
- Span
Outcome - What a filter execution resulted in — the union of its intentional
decisionand theRunErrorfailure modes, so a span records traps and deadlines as faithfully as acontinue. Maps to an OTelStatus(the decisions areOk; the faults areError). - State
Backend Kind - Manifest spelling of the state backend (ADR 000041). Defaults to
memory.
Constants§
- DEV_
KEY_ IN_ TRUST - A manifest
[trust]key is marked as a dev key (ADR 000065 decision 2) — warned, not rejected, because aplecto dev-generated manifest is SUPPOSED to reference one; this only flags the case for a human to double check when it shows up somewhere they did not expect. - DEV_
KEY_ MARKER - Prefixed onto a persisted dev public-key file, before the PEM block. Plain text ahead of
-----BEGIN...is not part of the PEM grammar (parsers skip straight to the marker), so this survives being read back as a normal SPKI PEM while staying grep-able — the hookplecto validateuses to warn when a dev key ends up in a production manifest’s[trust](ADR 000065 decision 5). - FILTER_
WIT - The canonical
plecto:filter@0.3.0contract text, byte-identical to the vendoredwit/world.witthis module’scrate::bindingsresolves — so a consumer that needs the raw WIT source (e.g.plecto new-filter’s scaffold, ADR 000072) can never drift from what this binary’s own host actually runs. Re-exported viaplecto-controlforplecto-server, which takes no directplecto-hostproduction dependency. - PATH_
NORMALIZATION_ REJECTED - Request path failed normalization — the third wall.
- QUOTA_
EXCEEDED - Host-native rate-limit bucket exhausted — the second wall.
- SIGNATURE_
VERIFICATION_ FAILED - Signature verification failure — the first wall (ADR 000006 provenance gate).
Traits§
- Artifact
Store - Resolves a manifest
sourceinto aResolvedArtifact, verifying that the artifact’s content digest equalspinned_digest(sha256:...). Implementations are fail-closed on any mismatch or malformed artifact. - Reload
Source - A source of reload triggers.
recvblocks until the next trigger fires, or returnsNonewhen the source is exhausted / shut down (the loop then ends). Implementors carry no config: a trigger only ever means “re-read the manifest now”. - Telemetry
Sink - Where the host sends each
FilterSpan. Deliberately sync (the OTel SDK’sSpanExporteris async and would pull tokio — ADR 000009 keeps that named-deferred): a sink must not block the data plane.NoopSinkis the default; an OTLP-mapping sink is added when network export lands.
Functions§
- bound_
sbom - A minimal in-toto-style SBOM statement that binds
component: itssubjectdigest issha256(component), satisfying the load gate’s SBOM↔component binding (review f000003 #1). The predicate is empty (content policy is deferred). Production helper — this crate’s ownHost::loadverifies the binding it produces; a real supply chain gets its attestations fromcosign attest. Also re-exported (unchanged) fromtest_supportfor the existing test suites. - diagnose
- Map a
ControlErrorto itsDiagnostic, if the plain error message needs one.Nonemeans the thiserror message already stands on its own — most variants do. - diagnosed_
message - Render
errfor a human-facing surface (startup,plecto dev), appending the four-part diagnostic when one is registered — the piece that actually puts PLECTO-E0001 in front of a newcomer whose filter failed the signature gate (ADR 000065 decision 5). - manifest_
json_ schema - The manifest’s JSON Schema (ADR 000049), derived from the very serde model
from_tomlparses with — the schema cannot drift from the structs, anddeny_unknown_fieldssurfaces asadditionalProperties: false, so editor validation rejects exactly whatvalidaterejects. draft-07 output: the level taplo / Even Better TOML consume (schemars’ 2020-12 default is outside taplo’s documented support). Serialising a just-generated schema cannot fail, but if it ever did, the error surfaces to the CLI caller — a silent"{}"fallback would be a schema that validates EVERYTHING, fail-open for the editor validation this exists to provide. - normalize_
path - Normalize a request target’s PATH for routing and forwarding so the proxy and the origin agree
on it (CWE-22 Path Traversal / CWE-436 Interpretation Conflict). Per-route filter chains
are an access-control boundary, so route selection IS access control: a
..segment that selects a laxer route here but resolves to a stricter path at the upstream would bypass that route’s filters. The fast path normalizes once at ingress and then routes, runs the chain, and forwards on the SAME normalized path, so the origin cannot re-derive a different path. - public_
key_ path_ for - The public-key sibling path
load_or_createwrites next to a private-key path (<path>.pub). Exposed so a caller (e.g.plecto new-filterwriting a dev manifest’s[trust]) can name it without re-deriving the convention. - run_
conformance - Run the generic conformance battery against raw component bytes.
- serve_
reloads - Drive
controlfrom aReloadSource: on every trigger, re-read the on-disk manifest and reload. Returns when the source is exhausted. A reload failure (parse / resolve / verify / load) is logged and the current set stays live (fail-closed) — a bad edit never takes the proxy down. An unchanged manifest is a cheap no-op. - validate_
manifest - Statically validate
manifest— theplecto validatecore (thenginx -tshape): every check the server would fail closed on at startup that needs no artifact and mutates nothing. Covers the strict parse (the caller already ran it),[trust]key files,[state]coherence, per-filter metering/rate-limit ranges, duplicate ids, chain and route references, the weighted split,[[tls]]cert/key loads, and[[upstream]](LB config +[upstream.tls]CA loads). Returns the manifest’s config version (semantic content hash, ADR 000008) on success. - validate_
manifest_ path validate_manifestfor an on-disk manifest: reads + strictly parsespath, resolving relative paths against the manifest’s own directory (the same rule the server applies).