OpenClaw provider plugin
In-process hook, not a sandbox. The OpenClaw provider plugin runs in the OpenClaw process. It is an enforcement hook, not a sandbox boundary. Operators should install it only in trusted OpenClaw deployments. Use D40a base-URL routing when the plugin API changes or when plugin installation is not acceptable.
@spendguard/openclaw-provider-plugin wraps an OpenClaw provider at the
provider runtime hook D40b pinned to wrapStreamFn(ctx). It reserves through
the SpendGuard sidecar before calling ctx.streamFn(params) or the upstream
provider wrapper, and it settles success, stream completion, provider errors,
timeouts, and aborts against the same reservation.
What Gets Gated
Section titled “What Gets Gated”| Path | Status |
|---|---|
| Non-stream OpenAI-compatible provider call | Reserved before dispatch, SUCCESS settled after response. |
| Streaming provider call | One reserve before first chunk, one SUCCESS settlement after iterator completion. |
| DENY / STOP | Throws before provider dispatch; upstream counter does not move. |
| Provider error | Settles PROVIDER_ERROR, then rethrows the provider error. |
| Client timeout / abort | Settles CLIENT_TIMEOUT or RUN_ABORTED. |
D40b does not claim to stabilize every historical OpenClaw plugin API. The
pin is openclaw@2026.6.2 at
openclaw/openclaw@d4819948f37d45fe8f1428401316eaae456cdf16.
Install
Section titled “Install”pnpm add @spendguard/sdk @spendguard/openclaw-provider-plugin openclawNode >=22.19.0 is required by the pinned OpenClaw package. ESM-only.
Configure
Section titled “Configure”Register the guarded provider through OpenClaw’s provider plugin registration surface. The local fixture used by the demo records the pinned install shape:
{ "plugins": { "spendguardProviderPlugin": { "package": "@spendguard/openclaw-provider-plugin", "factory": "createSpendGuardOpenClawProvider", "mode": "in-process-provider-wrapper", "hook": "wrapStreamFn", "trustBoundary": "trusted-openclaw-process" } }}Pass the sidecar identity and reserve tuple explicitly:
import { SpendGuardClient } from "@spendguard/sdk";import { createSpendGuardOpenClawProvider } from "@spendguard/openclaw-provider-plugin";
const client = new SpendGuardClient({ socketPath: "/var/run/spendguard/adapter.sock", tenantId: process.env.SPENDGUARD_TENANT_ID, runtimeKind: "openclaw-provider-plugin",});await client.connect();await client.handshake();
const guardedProvider = createSpendGuardOpenClawProvider(upstreamProvider, { client, tenantId: process.env.SPENDGUARD_TENANT_ID, budgetId: process.env.SPENDGUARD_BUDGET_ID, windowInstanceId: process.env.SPENDGUARD_WINDOW_INSTANCE_ID, unitId: process.env.SPENDGUARD_UNIT_ID, pricing: { pricingVersion: process.env.SPENDGUARD_PRICING_VERSION ?? "", pricingHash: Uint8Array.from( Buffer.from(process.env.SPENDGUARD_PRICE_SNAPSHOT_HASH_HEX ?? "", "hex"), ), fxRateVersion: process.env.SPENDGUARD_FX_RATE_VERSION ?? "", unitConversionVersion: process.env.SPENDGUARD_UNIT_CONVERSION_VERSION ?? "", },});unitId, windowInstanceId, and pricing are required from day 1. The
ledger rejects empty unit/window ids and rejects commits whose pricing tuple
does not match the reservation.
Failure Posture
Section titled “Failure Posture”There is no failOpen, degradeOnUnavailable, or disable flag. A DENY,
approval stop, sidecar outage, or malformed reserve tuple aborts before the
provider call. Provider errors are different: the provider has already been
called, so the wrapper settles PROVIDER_ERROR and rethrows the original
error to OpenClaw.
Run The Demo
Section titled “Run The Demo”make demo-downmake demo-up DEMO_MODE=openclaw_provider_pluginmake -C deploy/demo demo-verify-openclaw-provider-pluginSuccessful runner line:
[demo] openclaw_provider_plugin ALL 4 steps PASS (ALLOW + DENY + STREAM + PROVIDER_ERROR)The demo validates the committed OpenClaw plugin config fixture, stages the
workspace package into a Node 22.19 runner, talks to the real sidecar over
UDS, and uses a local OpenAI-compatible counting stub. The hard gate asserts
reserve=3, commit_estimated=2, release=1, denied_decision=1,
canonical decision=4, canonical outcome=3, outbox closure, and
counting-stub calls=3 (ALLOW + STREAM + PROVIDER_ERROR only).
D40a Fallback
Section titled “D40a Fallback”Use OpenClaw + SpendGuard base-URL routing when
you cannot install an in-process plugin or need a more durable config-only
path. D40a points OpenClaw’s custom provider baseUrl at the SpendGuard
egress proxy. D40b wraps the provider inside the OpenClaw process.
- npm package:
@spendguard/openclaw-provider-plugin - Demo overlay:
deploy/demo/openclaw_provider_plugin/ - Locked spec set:
docs/specs/coverage/D40b_openclaw_provider_plugin/