Plugin host
How out-of-process gRPC plugins are launched, handshaken, sandboxed and mediated by the host, with no ambient authority.
PluginService and HostService are public contracts. The host that serves them is part of the proprietary runtime and is designed.
Build once against the contract. Bring your own capabilities. The plugin describes what it needs; Numinance decides what it gets.
Invocation sequence
Plugin invocation sequence
In what order do host and provider talk, and who decides what?
Policy allows the action; the provider runs it and every privileged step goes through the host.
Host: Plugin host + policy decision point · Provider: Plugin subprocess · Egress: Broker / egress proxy · Evidence: Evidence writer (runner identity)
Host launches the subprocess
exec + NUMINANCE_PLUGIN_COOKIE
- Message and fields
- NUMINANCE_PLUGIN_COOKIE = MagicCookie (plugin.CookieEnv)
- NUMINANCE_PLUGIN_PROTOCOLS (offer)
- host address env (plugin.HostAddrEnv)
- Who holds authority
- Host: it starts the process and offers the protocol versions. The host identifies the plugin by the connection it established at launch, never by request content.
- ✕ The provider can not
- The cookie is not a secret and not authority: it only stops a user from running the provider binary by hand. A provider with a wrong cookie refuses to serve.
Allowed. Step 1 of 14: Host launches the subprocess. exec + NUMINANCE_PLUGIN_COOKIE. Host: it starts the process and offers the protocol versions. The host identifies the plugin by the connection it established at launch, never by request content.
Text description
Illustrative and simulated: nothing here talks to a running host. RPC and field names are the ones in proto/numinance/plugin/v1 and sdk/go/plugin.
- The host starts the provider subprocess with
NUMINANCE_PLUGIN_COOKIEset to the magic cookie, then callsHandshake,GetManifestandGetDescriptorsonPluginService. - The policy decision and the grant happen on the host, outside the model and before the provider sees the action.
- If allowed, the host calls
Invoke(server-streaming) with anEnvelopewhoseActionContextthe host sets. The provider may call back onHostService:RequestCredentialreturns an opaque handle that only works through the egress proxy, which injects the real token;EmitEvidencesubmits evidence that the host appends under the runner identity;ReportProgressreports progress. - The stream ends with an
ActionResult; the host records the outcome, may callHealth, and finallyShutdown, which first revokes handles and egress grants. - If policy denies,
Invokeis never sent and the denial is recorded as evidence. If the provider requests a credential intent that is not in its declaredcredential_intents, the host rejects it and records the call.
The provider never holds authority: it cannot approve, widen a grant, see a credential, or append evidence.
Roles
| Role | Implements | Responsibility |
|---|---|---|
| Plugin | PluginService | Performs capability verbs for one or more ports |
| Host | HostService | Mediates every privileged request the plugin makes |
| Engine | n/a | Decides when to call a plugin and obtains the policy decision first |
PluginService has Handshake, GetManifest, GetDescriptors, Invoke (server streaming), Health and Shutdown. HostService has Log, EmitEvidence, RequestCredential, ResolveReference (server streaming) and ReportProgress.
Handshake and manifest
The host offers its spec version, the protocol versions it will speak, the ports of the installed manifest and the non-core capabilities it knows. The plugin answers with one protocol version from that offer and a subset of capabilities present in both the offer and its manifest. Anything else aborts the handshake. The negotiated version is recorded at install and only that version is offered afterwards, so a plugin cannot later downgrade the protocol.
GetManifest must equal the manifest decoded from the installed package, whose digest the lockfile pins. A mismatch means the host refuses to start the plugin. The manifest is never a source of authority by itself; the host enforces the declared network, filesystem and credential intents.
Sandbox boundaries
- Each plugin is a separate process in its own container, never co-located with the executor.
- Egress is limited to the destinations in its manifest, through the egress proxy.
- The host identifies the calling plugin by the connection it launched, not by request content, and rejects an
invocation_idthat is not in flight for that plugin. RequestCredentialreturns an opaque handle bound to the invocation and intent, usable only through the proxy, expiring no later than the invocation deadline. It never returns credential material.- Every dispatch carries a host-set
ActionContextwith apolicy_decision_id. The host never dispatches without one. - A plugin that cannot accept an idempotency key declares it, and steps calling it need a compensation or approval.
Governed install
Install verifies signature and provenance, computes a permission difference against the lockfile, runs the conformance suite in a sandbox, evaluates policy (widening always needs approval) and pins digest and signer identity. Versions are locked for the duration of a change; nothing is installed dynamically during a run.
Boundary table
| Party | Holds | Does not hold |
|---|---|---|
| Plugin | Its own logic and a handle for the current invocation | Credentials, the right to widen its manifest |
| Host | Enforcement and the evidence emit path | The plugin's business logic |
| Broker and proxy | Credential material and injection | Decisions |
Failure cases
- Handshake offers a lower protocol than locked: aborted.
- Manifest differs from the pinned package: plugin not started.
- Call without a policy decision: not dispatched.
- Request to an undeclared host: denied by the proxy and recorded.
- Provider unhealthy:
Healthfails and capability invocations are denied until recovery.