You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Add an opt-in, permissioned OpenAI exchange lifecycle contract for installable mesh-llm plugins.
A plugin that declares this contract must be able to:
inspect the original OpenAI request envelope before model dispatch;
return a bounded pre-dispatch policy decision;
observe the effective request and selected serving route before execution;
observe the terminal response, error, denial, or cancellation;
correlate every phase without becoming an HTTP reverse proxy.
The contract must cover chat completions, legacy completions, and Responses, including streaming requests. It must preserve ordinary OpenAI streaming rather than forcing the host to buffer a response.
Motivation
The current plugin architecture is primarily provider-oriented: plugins contribute operations, HTTP/MCP surfaces, external inference endpoints, and mesh capabilities. It does not let an installable plugin observe or make a policy decision about traffic arriving at mesh-llm's own OpenAI API.
The native serving plugin ABI exposes generation token lifecycle data, but not the original OpenAI request or the client-visible response. It therefore cannot compute a receipt over the API request/response pair.
OpenAiHookPolicy is a useful in-process seam, but it is compiled into the backend wrapper, currently invokes only the before-chat hook, and is not available to an installable out-of-process plugin.
The Capsule Emit experiment consequently had to run as a reverse-proxy sidecar. That proved the record format but introduced a second port, changed the request topology, buffered/re-synthesized SSE, and could only describe itself honestly as an external collector. Other policy, audit, DLP, guardrail, routing, and compliance plugins will hit the same limitation.
Goals
Make the request envelope available to explicitly authorized plugins.
Allow an authorized plugin to allow, abstain, or deny before dispatch.
Expose both what the client submitted and what mesh-llm actually dispatched.
Expose what mesh-llm actually returned to the client.
Support streaming without changing response bytes, chunk order, or latency characteristics beyond bounded hook overhead.
Make evidence absence, hook failure, policy denial, backend error, cancellation, and successful completion distinct terminal states.
Keep the contract generic; Capsule Emit is one consumer, not a core special case.
Let explicitly authorized plugins consume the node's public identity/attestation bundle and obtain a narrowly scoped owner-signed delegation for a plugin signing key.
Preserve existing plugins and mixed plugin-protocol deployments.
Non-goals
Arbitrary request mutation in v1.
Giving a plugin raw Authorization, Cookie, API-key, or owner-control headers.
Giving a plugin owner-key, node-key, keystore, passphrase, keychain, or other private-key material.
Providing an arbitrary-byte signing oracle backed by the owner or node transport key.
Claiming that an API-boundary observation proves model execution.
Replacing the native generation lifecycle or future TEE/runtime attestation.
Making every plugin an observer by default.
Requiring an HTTP sidecar or sending core inference through a plugin endpoint.
Exposing these host-private lifecycle operations through MCP or public plugin HTTP projection.
Proposed contract
Declaration and authorization
Add an optional manifest declaration for an OpenAI exchange hook. A possible shape is:
The manifest is a request for access, not the grant itself.
Host configuration must explicitly grant:
endpoint and phase subscriptions;
request/response body access;
sanitized header names;
permission to make admission decisions;
permission to add response metadata;
failure policy and deadline.
Installing an ordinary plugin must not implicitly expose prompts or completions. The console/CLI should clearly warn when a plugin requests body access.
Prefer an additive manifest plus host-private service invocations over a new parallel plugin runtime. Existing InvokeService and side-stream transport can carry the control messages and large bodies. If protobuf additions are needed, they must be additive and feature-negotiated.
Lifecycle phases
1. request_received
Runs after mesh-llm has accepted and bounded the HTTP body but before routing, guardrail execution, or backend dispatch.
The envelope includes:
exchange_id: stable for the full exchange;
request_id / trace_id where available;
endpoint kind and API version;
HTTP method and normalized path;
observation point and origin class: local ingress, tunneled ingress, internal retry, or other explicit value;
sanitized allowlisted headers;
exact decoded request body bytes or a side-stream reference, according to the grant;
SHA-256 of those exact bytes, computed by the host;
parsed OpenAI request JSON when parsing succeeded;
parse status and content type;
receipt/correlation metadata already attached by the host.
Allowed synchronous result:
abstain: no policy opinion;
allow: explicitly permit this plugin's policy;
deny: stop before dispatch and return a stable OpenAI-shaped error;
annotations: plugin-namespaced, bounded metadata carried to later phases;
response metadata reservation: bounded, allowlisted response headers such as a provisional receipt ID.
V1 does not allow a plugin to rewrite the request. Mutation would require separate original/effective commitments, ordering semantics between multiple plugins, and a clearer security model.
A plugin's allow result never bypasses mesh-llm's own validation or another plugin's denial.
2. backend_selected
Runs after routing and core request transformation, but before the selected backend is invoked.
It includes:
the original exchange_id;
effective parsed request and its host-computed digest;
selected public model ID;
provider/backend kind;
local or remote serving target;
retry/attempt number;
available model/package/runtime identity descriptors;
core transformations that materially changed the request;
accumulated namespaced annotations.
A plugin with admission permission may deny here as well, allowing policies to require a particular model identity, provider, peer, or evidence level before bytes reach a backend.
The contract must distinguish a route claim from verified artifact or runtime evidence. Missing identity fields are represented as unavailable, never inferred.
3. exchange_finished
Exactly one logical terminal event is attempted for every accepted request:
completed;
policy_denied;
request_invalid;
backend_error;
transport_error;
client_cancelled;
timed_out;
evidence_unavailable/internal_hook_failure.
It includes:
exchange_id and attempt history;
HTTP status;
exact response-body bytes for buffered responses, or an ordered response transcript/side-stream for streamed responses;
SHA-256 of exact client-facing bytes computed at the final host emitter;
parsed/reassembled semantic response when available;
usage and timing fields;
selected route and model descriptors;
whether any bytes were sent to the client;
terminal evidence completeness and any dropped observer data.
For streaming, the byte tap must sit after all host transformations and immediately before socket emission. The plugin receives a copy; it is not placed inline between the emitter and client.
A streamed transcript must define whether it covers SSE frame payloads, complete encoded SSE frames, or both. The format and digest rules require published test vectors.
Streaming and payload transport
Large request bodies, multimodal inputs, and streaming outputs must use the existing negotiated side-stream mechanism rather than the long-lived control connection.
Requirements:
control traffic remains responsive;
every stream is correlated to exchange_id and phase;
byte order is preserved;
cancellation closes the observer stream;
bounded queues report truncation or loss explicitly;
best-effort observation never silently claims a complete transcript;
required mode applies backpressure only when the operator selected it;
no hook may force the host to buffer a complete SSE response merely to compute a receipt.
The host should compute wire digests while forwarding, so an observer can verify completeness without being trusted to report the digest correctly.
Multiple plugins and failure policy
Pre-dispatch decisions should execute concurrently under one host-owned deadline because v1 has no mutation ordering.
Aggregation rules:
any successful deny wins;
allow and abstain do not override a deny;
annotations are namespaced by plugin ID;
a required plugin timeout/disconnect produces a fail-closed service error;
a best-effort plugin timeout/disconnect allows execution and records evidence unavailable;
required/best-effort is host policy, not something a plugin can impose on installation;
repeated failures trigger a circuit breaker and visible degraded state.
No plugin may block health checks or the control plane while a request hook is running.
Response metadata
A plugin may request bounded response metadata only through an explicit grant.
Header names must use an approved namespace.
Security, hop-by-hop, CORS, content-length, content-type, and authentication headers cannot be replaced.
Streaming plugins may reserve a provisional correlation/receipt ID at request_received because final headers are sent before the final capsule exists.
Non-streaming hooks may optionally return final metadata before headers are committed, within the same deadline.
A response header is a reference, not evidence by itself.
Privacy and security
Never expose Authorization, Proxy-Authorization, Cookie, Set-Cookie, owner-control credentials, or transport secrets.
Header access is allowlist-only and case-normalized.
Inline data URLs and multimodal request bodies count as sensitive body access.
Logs and plugin summaries must not contain raw prompt/completion bodies.
Body limits, stream limits, timeouts, and concurrency limits are host-owned.
Host API/status surfaces must show which plugins currently have request/response access.
Plugin removal or permission revocation takes effect without leaving a hidden traffic path.
Request digests are commitments, not confidentiality; documentation must warn about dictionary attacks on low-entropy content.
Observation points and duplicate records
The envelope must state where it was observed. At minimum:
gateway_ingress;
serving_host_ingress;
backend_dispatch;
client_egress.
A routed mesh request may be visible at more than one node. exchange_id, hop_id, attempt, local_peer_id, and observation_point must make those records distinguishable. The host must not present a gateway observation as proof from the serving target.
Host identity and signing delegation
Receipt/audit plugins need to connect their statements to mesh-llm's existing identity hierarchy without reading private keys.
mesh-llm currently keeps these concepts separate:
owner identity: the Ed25519 identity created by mesh-llm auth init;
node identity: the QUIC/iroh endpoint key;
SignedNodeOwnership: a short-lived owner-signed certificate binding owner identity to node endpoint identity;
release build attestation: trusted release-signer evidence about the packaged mesh-llm executable, separate from ownership and remote runtime attestation.
Add an explicitly permissioned host service with two narrow operations.
ReadIdentityBundle
Return public, bounded evidence already held by the host:
local node endpoint ID;
SignedNodeOwnership and its local verification summary;
owner ID and owner signing public key when present;
release build attestation and verification summary when present;
host version/build/artifact digest carried by the release attestation;
installed plugin ID, version, source, and artifact/package digest when available;
freshness and evidence-source metadata.
The response must preserve distinctions between missing, present-unverified, verified, expired, revoked, and invalid. It must not forward a boolean such as owner_verified without the signed evidence needed to verify it.
DelegatePluginSigningKey
The plugin supplies an Ed25519 public key and requests one registered signing scope. The host constructs and signs a fixed-shape PluginSigningDelegation; it does not sign plugin-provided arbitrary bytes.
For Capsule Emit, the initial scope is mesh.inference.capsule.sign.v1.
Requirements:
authenticate plugin_id from the live plugin connection, not request JSON;
bind installed plugin version/artifact identity from host-owned metadata;
bind the current node endpoint ID and owner certificate ID;
use canonical serialization with a fixed domain tag such as mesh-llm-plugin-signing-delegation-v1:;
sign only when the owner key is already available to the running host;
cap validity and scope through host policy;
make delegation issuance/renewal an explicit manifest permission;
return unavailable when no owner identity is loaded rather than prompting during inference;
never expose owner signing bytes, node secret bytes, keystore paths, passphrases, or keychain material;
never allow a plugin to choose arbitrary claims, owner/node IDs, plugin identity, validity beyond policy, or signing domain;
renew before expiry outside the request path;
invalidate/reissue on plugin artifact, plugin key, node identity, owner identity, or relevant certificate change;
include a delegation ID so revocation can be added or enforced explicitly;
expose bounded status and audit events without logging public keys or IDs into OTLP metrics.
The existing owner key signs delegations only at plugin startup/renewal. Per-request statements are signed by the delegated plugin key, avoiding high-frequency use of the owner key.
A verifier chain is:
verify the receipt/COSE statement with the delegated plugin public key;
verify PluginSigningDelegation with the owner public key;
verify owner_id is derived from that owner public key;
verify SignedNodeOwnership binds the same owner and node endpoint ID;
apply expiry and local owner/node/delegation revocation policy;
verify release build attestation independently when supplied.
The release attestation remains build provenance. Linking it into the evidence bundle does not convert it into remote runtime attestation.
A software plugin key remains exportable and can be misused until delegation expiry. The delegation improves attribution, scoping, rotation, and revocation; it does not prove that the key was used only on the named node. A future host-managed, TPM/TEE-backed, or non-exportable delegated key can strengthen the same chain.
Fallback and policy
Identity mode is host policy:
self_attested: plugin uses an independent key and labels it accurately;
owner_delegated: require a valid PluginSigningDelegation and SignedNodeOwnership;
owner_delegated_required: fail plugin readiness/required evidence when delegation is unavailable;
hardware_delegated: reserved for future attested/non-exportable keys.
No mode silently upgrades its assurance label.
Compatibility requirements
The plugin protocol is currently generation 2 and initialization checks exact equality. This feature must not strand existing plugins.
Acceptable approaches include:
express the hook through existing v2 capability/service invocation and side streams, with a new optional manifest field and a package-level minimum mesh version; or
introduce protocol generation 3 while changing negotiation so a new host continues to run generation-2 plugins.
Whichever path is chosen:
old plugins continue to run without modification;
a new hook plugin fails startup clearly on a host that cannot provide the declared contract;
unknown phases/fields are ignored only when the plugin marked them optional;
required permissions cannot degrade silently to no observation;
conformance tests cover new-host/old-plugin and unsupported-host/new-plugin behavior.
Local ingress and inbound tunneled serving paths are covered and identify their observation point.
Existing generation-2 plugins continue to initialize and operate.
An unsupported host rejects a plugin that requires this contract with an actionable error.
A maintained exemplar demonstrates observe-only and admission-policy modes.
Plugin architecture and security documentation are updated.
Alternatives considered
Keep using a reverse-proxy sidecar
Works without host changes, but changes ports/topology, only sees wire traffic outside the trust boundary, complicates streaming, and cannot distinguish internal routing/dispatch facts.
Use the native serving plugin ABI
Useful for authoritative token-generation evidence, but it does not contain the original API envelope or final client representation. It is complementary, not interchangeable.
Compile OpenAiHookPolicy into custom binaries
Provides an in-process seam, but is not installable and requires product forks. The goal is a stable capability contract.
Send every request through a plugin-provided inference endpoint
Reverses ownership and makes an observer become the backend/proxy. It also prevents the plugin from observing native and remotely routed backends generically.
Follow-up
The first consumer should be a separately scoped Capsule Emit prototype linked to #1233. That prototype must describe its evidence as API-boundary, self-attested collector evidence until a future compute-boundary or hardware-bound receipt is linked.
Summary
Add an opt-in, permissioned OpenAI exchange lifecycle contract for installable mesh-llm plugins.
A plugin that declares this contract must be able to:
The contract must cover chat completions, legacy completions, and Responses, including streaming requests. It must preserve ordinary OpenAI streaming rather than forcing the host to buffer a response.
Motivation
The current plugin architecture is primarily provider-oriented: plugins contribute operations, HTTP/MCP surfaces, external inference endpoints, and mesh capabilities. It does not let an installable plugin observe or make a policy decision about traffic arriving at mesh-llm's own OpenAI API.
The native serving plugin ABI exposes generation token lifecycle data, but not the original OpenAI request or the client-visible response. It therefore cannot compute a receipt over the API request/response pair.
OpenAiHookPolicy is a useful in-process seam, but it is compiled into the backend wrapper, currently invokes only the before-chat hook, and is not available to an installable out-of-process plugin.
The Capsule Emit experiment consequently had to run as a reverse-proxy sidecar. That proved the record format but introduced a second port, changed the request topology, buffered/re-synthesized SSE, and could only describe itself honestly as an external collector. Other policy, audit, DLP, guardrail, routing, and compliance plugins will hit the same limitation.
Goals
Non-goals
Proposed contract
Declaration and authorization
Add an optional manifest declaration for an OpenAI exchange hook. A possible shape is:
The manifest is a request for access, not the grant itself.
Host configuration must explicitly grant:
Installing an ordinary plugin must not implicitly expose prompts or completions. The console/CLI should clearly warn when a plugin requests body access.
Prefer an additive manifest plus host-private service invocations over a new parallel plugin runtime. Existing InvokeService and side-stream transport can carry the control messages and large bodies. If protobuf additions are needed, they must be additive and feature-negotiated.
Lifecycle phases
1. request_received
Runs after mesh-llm has accepted and bounded the HTTP body but before routing, guardrail execution, or backend dispatch.
The envelope includes:
Allowed synchronous result:
V1 does not allow a plugin to rewrite the request. Mutation would require separate original/effective commitments, ordering semantics between multiple plugins, and a clearer security model.
A plugin's allow result never bypasses mesh-llm's own validation or another plugin's denial.
2. backend_selected
Runs after routing and core request transformation, but before the selected backend is invoked.
It includes:
A plugin with admission permission may deny here as well, allowing policies to require a particular model identity, provider, peer, or evidence level before bytes reach a backend.
The contract must distinguish a route claim from verified artifact or runtime evidence. Missing identity fields are represented as unavailable, never inferred.
3. exchange_finished
Exactly one logical terminal event is attempted for every accepted request:
It includes:
For streaming, the byte tap must sit after all host transformations and immediately before socket emission. The plugin receives a copy; it is not placed inline between the emitter and client.
A streamed transcript must define whether it covers SSE frame payloads, complete encoded SSE frames, or both. The format and digest rules require published test vectors.
Streaming and payload transport
Large request bodies, multimodal inputs, and streaming outputs must use the existing negotiated side-stream mechanism rather than the long-lived control connection.
Requirements:
The host should compute wire digests while forwarding, so an observer can verify completeness without being trusted to report the digest correctly.
Multiple plugins and failure policy
Pre-dispatch decisions should execute concurrently under one host-owned deadline because v1 has no mutation ordering.
Aggregation rules:
No plugin may block health checks or the control plane while a request hook is running.
Response metadata
A plugin may request bounded response metadata only through an explicit grant.
Privacy and security
Observation points and duplicate records
The envelope must state where it was observed. At minimum:
A routed mesh request may be visible at more than one node. exchange_id, hop_id, attempt, local_peer_id, and observation_point must make those records distinguishable. The host must not present a gateway observation as proof from the serving target.
Host identity and signing delegation
Receipt/audit plugins need to connect their statements to mesh-llm's existing identity hierarchy without reading private keys.
mesh-llm currently keeps these concepts separate:
Add an explicitly permissioned host service with two narrow operations.
ReadIdentityBundle
Return public, bounded evidence already held by the host:
The response must preserve distinctions between missing, present-unverified, verified, expired, revoked, and invalid. It must not forward a boolean such as owner_verified without the signed evidence needed to verify it.
DelegatePluginSigningKey
The plugin supplies an Ed25519 public key and requests one registered signing scope. The host constructs and signs a fixed-shape PluginSigningDelegation; it does not sign plugin-provided arbitrary bytes.
Suggested claim:
For Capsule Emit, the initial scope is mesh.inference.capsule.sign.v1.
Requirements:
The existing owner key signs delegations only at plugin startup/renewal. Per-request statements are signed by the delegated plugin key, avoiding high-frequency use of the owner key.
A verifier chain is:
The release attestation remains build provenance. Linking it into the evidence bundle does not convert it into remote runtime attestation.
A software plugin key remains exportable and can be misused until delegation expiry. The delegation improves attribution, scoping, rotation, and revocation; it does not prove that the key was used only on the named node. A future host-managed, TPM/TEE-backed, or non-exportable delegated key can strengthen the same chain.
Fallback and policy
Identity mode is host policy:
No mode silently upgrades its assurance label.
Compatibility requirements
The plugin protocol is currently generation 2 and initialization checks exact equality. This feature must not strand existing plugins.
Acceptable approaches include:
Whichever path is chosen:
Implementation areas
Expected ownership:
Do not special-case Capsule Emit by plugin name.
Acceptance criteria
Alternatives considered
Keep using a reverse-proxy sidecar
Works without host changes, but changes ports/topology, only sees wire traffic outside the trust boundary, complicates streaming, and cannot distinguish internal routing/dispatch facts.
Use the native serving plugin ABI
Useful for authoritative token-generation evidence, but it does not contain the original API envelope or final client representation. It is complementary, not interchangeable.
Compile OpenAiHookPolicy into custom binaries
Provides an in-process seam, but is not installable and requires product forks. The goal is a stable capability contract.
Send every request through a plugin-provided inference endpoint
Reverses ownership and makes an observer become the backend/proxy. It also prevents the plugin from observing native and remotely routed backends generically.
Follow-up
The first consumer should be a separately scoped Capsule Emit prototype linked to #1233. That prototype must describe its evidence as API-boundary, self-attested collector evidence until a future compute-boundary or hardware-bound receipt is linked.