Local Hooks + WASM Plugin E2E
This runbook exercises the real local stack:
- control-plane on
:10010 - gateway on
:10000 - shared Postgres metadata
- shared local filesystem artifact storage
- a custom WASM plugin artifact created locally
- a real signed proxied request through the gateway
1. Start Postgres
Section titled “1. Start Postgres”From repo root:
just --justfile llm-gateway/justfile audit-db-up2. Generate a trusted plugin signer
Section titled “2. Generate a trusted plugin signer”SIGNER_SEED=$(just --justfile llm-gateway/justfile gen-key | awk -F= '/seed_b64url/{print $2; exit}')SIGNER_PUB=$(just --justfile llm-gateway/justfile pubkey-from-seed "$SIGNER_SEED" | awk -F= '/^public_key_b64url=/{print $2; exit}')ARTIFACT_ROOT="$PWD/.tmp/plugin-artifacts"PUBLISHER_SEED_FILE="$PWD/.tmp/plugin-publisher.seed"mkdir -p "$ARTIFACT_ROOT"printf '%s' "$SIGNER_SEED" > "$PUBLISHER_SEED_FILE"3. Configure control-plane and gateway
Section titled “3. Configure control-plane and gateway”Initialize local config files if you have not already:
just --justfile control-plane/justfile runtime-config-local-initjust --justfile llm-gateway/justfile runtime-config-local-initUpdate runtime.local.toml:
[server]listen = ":10010"db_dsn = "postgres://guardian:guardian@127.0.0.1:15432/guardian_gateway?sslmode=disable"cors_allow_all = true
[auth]enabled = falseregistration_token_secret = "guardian-local-bootstrap-secret"registration_token_issuer = "control-plane"
[plugins.publisher_signer]key_id = "signer-local"seed_file = "<PUBLISHER_SEED_FILE>"
[plugins.artifact_storage]provider = "file"bucket = "<ABS_ARTIFACT_ROOT>"prefix = "development"max_upload_bytes = 536870912Update runtime.local.toml:
[server]listen = ":10000"deployment_environment = "development"
[registry]backend = "postgres"db_dsn = "postgres://guardian:guardian@127.0.0.1:15432/guardian_gateway?sslmode=disable"registration_enabled = trueregistration_token_secret = "guardian-local-bootstrap-secret"registration_token_issuer = "control-plane"
[audit]enabled = truedb_dsn = "postgres://guardian:guardian@127.0.0.1:15432/guardian_gateway?sslmode=disable"
[upstreams]anthropic_url = "http://127.0.0.1:18080"openai_url = "http://127.0.0.1:18080"chatgpt_backend_url = "http://127.0.0.1:18080"
[plugins]enabled = truepoll_interval = "2s"authorizer_shadow_enabled = falseend_queue_dir = "data/plugin-end-queue"end_queue_max_bytes = 134217728artifact_cache_dir = "data/plugin-artifacts"
[plugins.trusted_signer_keys]signer-local = "<SIGNER_PUB>"
[plugins.artifact_storage]provider = "file"bucket = "<ABS_ARTIFACT_ROOT>"prefix = "development"Replace:
<SIGNER_PUB>with$SIGNER_PUB<ABS_ARTIFACT_ROOT>with the absolute value of$ARTIFACT_ROOT<PUBLISHER_SEED_FILE>with the absolute value of$PUBLISHER_SEED_FILE
4. Start the local services
Section titled “4. Start the local services”Terminal 1:
just --justfile llm-gateway/justfile mock-upstreamTerminal 2:
just --justfile control-plane/justfile run-configTerminal 3:
just --justfile llm-gateway/justfile gateway-config5. Build and deploy a custom WASM plugin
Section titled “5. Build and deploy a custom WASM plugin”The MVP developer path is the guardian-plugin CLI plus a guardian.plugin.toml
manifest. The sample Rust allow-all plugin is in
test-plugins/rust-allow-all:
cargo run -p guardian-plugin -- \ --manifest test-plugins/rust-allow-all/guardian.plugin.toml \ deploy --activateThis command builds the Rust guest, validates the core Guardian WASM ABI,
uploads the artifact without client-side signatures, lets the control-plane sign
the artifact/snapshot, creates a snapshot, and activates it because
--activate is present.
To create a draft and activate separately:
cargo run -p guardian-plugin -- \ --manifest test-plugins/rust-allow-all/guardian.plugin.toml \ deploy
cargo run -p guardian-plugin -- \ --manifest test-plugins/rust-allow-all/guardian.plugin.toml \ activate <SNAPSHOT_ID>The manifest owns the hook chain. The sample includes builtin.authorization
at proxy.pre order 0 and the Rust WASM plugin at order 1.
Host capability probe sample
Section titled “Host capability probe sample”To demo CID resolution plus host-managed outbound HTTP, deploy the Rust host capability probe instead:
cargo run -p guardian-plugin -- \ --manifest test-plugins/rust-host-capability-probe/guardian.plugin.toml \ deploy --activateThe sample stays in observability mode. It reads X-Viper-State-CID, requests
the full inbound body, extracts http(s) URLs from the prompt, and POSTs a
summary payload to the local mock upstream at http://127.0.0.1:18080/scan.
CID resolution uses the host-managed cid.resolve grant to read from the
gateway blob store; request_referenced and verified_by_host are returned as
metadata on the resolution result.
Legacy demo helpers
Section titled “Legacy demo helpers”The older fixture publisher is still useful when debugging the raw control-plane
API. It signs from the CLI instead of using [plugins.publisher_signer]:
just --justfile control-plane/justfile plugin-demo-publish-wasm \ "$PWD/test-plugins/artifacts/allow-all.wasm" \ "$SIGNER_SEED" \ "http://localhost:10010" \ "development" \ "signer-local" \ "custom.test.allow_all"The helper handles the two easy-to-miss pieces correctly:
- artifact signature is over the artifact SHA-256 digest
- snapshot manifest is signed over the hydrated entry, including
plugin_idandruntime, not justplugin_definition_id
Optional check:
curl -s "http://localhost:10010/v1/plugin-snapshots/current?environment=development" | jq .6. Register an agent and send a signed gateway request
Section titled “6. Register an agent and send a signed gateway request”TOKEN=$(just --justfile llm-gateway/justfile reg-token | awk -F= '/^token=/{print $2; exit}')SEED=$(just --justfile llm-gateway/justfile gen-key | awk -F= '/seed_b64url/{print $2; exit}')
just --justfile llm-gateway/justfile e2e-prepare-registry "$TOKEN" "$SEED"just --justfile llm-gateway/justfile e2e-successExpected outcome:
- gateway returns
200 - the mock upstream logs a POST to
/v1/messages - the active plugin snapshot remains
development
If you published the authorization-then-deny snapshot instead, the expected outcome is:
- gateway returns
403 - the audit record shows
plugin_denied_by = custom.test.deny_all - no upstream request is sent
Host capability probe request through Viper
Section titled “Host capability probe request through Viper”This variant uses the real Viper proxy so the request carries
X-Viper-State-CID and the plugin can resolve it.
Make sure Viper already has:
- a configured
llm_gateway_api_urlpointing athttp://127.0.0.1:10000/v1 - a registered DID for the
proxyapp
Reset any prior mock scanner observations:
just --justfile llm-gateway/justfile mock-upstream-scan-resetStart Viper in a fourth terminal:
cargo run -p viper -- proxy \ --app proxy \ --port 18081 \ --gateway-endpoint http://127.0.0.1:10000/v1/openai_api \ --stdout-traffic summarySend an OpenAI-style request with URLs in the prompt:
curl -s http://127.0.0.1:18081/v1/chat/completions \ -H 'Content-Type: application/json' \ --data @test-plugins/rust-host-capability-probe/demo-request.openai.jsonExpected outcome:
- Viper forwards the request with
X-Viper-State-CID - the gateway returns
200 - the plugin emits an observability event and remains allow-only
- the mock scanner receives a POST to
/scan
Inspect the recorded scanner payload:
just --justfile llm-gateway/justfile mock-upstream-scan-logsThe response includes the plugin’s outbound payload, including:
state_cid- CID resolution metadata under
cid, includingrequest_referencedandverified_by_host - the extracted prompt URLs under
request.urls
7. Verify the plugin actually executed
Section titled “7. Verify the plugin actually executed”Most recent hook events:
psql "postgres://guardian:guardian@127.0.0.1:15432/guardian_gateway?sslmode=disable" \ -c "SELECT plugin_id, hook, outcome, latency_ms, created_at FROM gateway_plugin_hook_events ORDER BY event_id DESC LIMIT 10;"Most recent audit rows with plugin summary:
psql "postgres://guardian:guardian@127.0.0.1:15432/guardian_gateway?sslmode=disable" \ -c "SELECT plugin_snapshot_version, plugin_denied_by, plugin_decision_code, plugin_summary_json FROM gateway_audit_exchanges ORDER BY exchange_id DESC LIMIT 5;"Most recent hook events for the host capability probe:
psql "postgres://guardian:guardian@127.0.0.1:15432/guardian_gateway?sslmode=disable" \ -c "SELECT plugin_id, hook, outcome, capability_summary_json, created_at FROM gateway_plugin_hook_events WHERE plugin_id = 'custom.test.rust_host_capability_probe' ORDER BY event_id DESC LIMIT 10;"8. Notes
Section titled “8. Notes”- The local artifact provider uses
bucketas a shared filesystem root. - Use an absolute path for
bucketso both services resolve the same files. - The shared fixtures are described in README.md.