This guide explains how to write feature and protocol plugins that preserve the Go proxy architecture. For the complete stage map, see docs/extension-points.md. For operator configuration and examples, see README.md and config/config.yaml. For YAML-only OpenAI/Anthropic-compatible provider wiring, see docs/custom-compatible-backends.md. For the no-key local stub maintainer workflow (check-config, routes, inventory, serve), see docs/dogfood-local.md.
- Frontend plugins decode a client protocol into
lipapi.Calland encode canonical events/errors back to that protocol. - Backend plugins translate
lipapi.Callinto upstream calls and emitlipapi.EventStreamvalues. - Feature plugins contribute hooks, observers, resolvers, gates, and lifecycles through
pkg/lipsdk/feature.FeatureBundle. - Store plugins provide persistence or continuity backends through composition-root wiring.
Only standard distribution packages (cmd/lipstd, internal/pluginreg, internal/infra/runtimebundle, internal/stdhttp) should import concrete bundled plugins. Core packages must remain plugin-agnostic.
A feature factory decodes opaque YAML and returns feature.FeatureBundle{SchemaVersion: feature.SchemaVersionV1, ...}. Empty slices mean the plugin does not occupy that stage.
Use SDK packages, not core internals:
pkg/lipsdk/hooksfor brownfield submit, part, and tool-reactor hooks;pkg/lipsdk/sessionfor session openers;pkg/lipsdk/workspacefor workspace resolvers;pkg/lipsdk/requestfor whole-call canonical transforms;pkg/lipsdk/toolcatalogfor tool definition filtering;pkg/lipsdk/toolpolicyfor allow/deny on canonical tool-call lifecycle events before tool reactors;pkg/lipsdk/routehintfor advisory route preferences;pkg/lipsdk/completionfor whole-completion gates;pkg/lipsdk/trafficfor observation, capture, and redaction;pkg/lipsdk/usagefor accounting-style observation over canonical usage deltas (non-mutating);pkg/lipsdk/statefor plugin-scoped state;pkg/lipsdk/auxiliaryfor controlled sub-calls.
- Keep handlers small, deterministic, and context-aware.
- Never retain or mutate canonical pointers after the handler returns unless the interface explicitly grants ownership.
- Use
context.Contextfor cancellation and deadlines; do not start per-request background goroutines from handlers. - Return classified errors instead of panicking. Core isolates panics, but a panic is still a plugin bug.
- Keep provider SDK types inside backend plugins only.
- Keep HTTP transport types inside frontends or stdhttp transport integrations only.
- Bind state with
state.BindPluginbefore sharing state between handlers in the same feature. - Redact before persistence. Raw capture is privileged and must be disabled unless explicitly configured.
- Use route hints only as hints; the core planner remains authoritative.
- Revalidate assumptions with focused tests whenever a handler mutates the canonical call or event stream.
Feature plugin configuration should be explicit and typed after YAML decode:
plugins:
features:
- id: ref-tool-policy
enabled: true
config:
block_names: ["dangerous_shell"]
block_prefixes: ["admin."]A plugin should reject invalid config at startup rather than fail during the first request.
Backend plugins must expose execbackend.Backend.ModelInventory with a pkg/lipsdk/modelinventory.Provider
and at least one execbackend.Backend.BackendPrefixes entry. Prefixes must match the backend factory id
(for example openai-responses, ollama, ollama-cloud) and must be unique across backend connector
kinds at runtime. Multiple instances of the same connector kind may reuse that kind's prefix.
Canonical model IDs must use the vendor/model form; do not publish
inventory rows whose canonical id uses a backend prefix qualifier such as ollama:google/gemma4.
The core model registry uses this provider at startup and during background refresh to answer fast routing
lookups for canonical model IDs such as openai/gpt-5.
Backend authors should choose one inventory source:
- Remote provider API, such as a
/modelsendpoint or provider SDK list operation. - Backend-specific static config file. File inventories should use an
items:list;models:is accepted as a compatibility alias whenitems:is absent. - Inline static config for fixed local/test backends.
Static providers should use modelinventory.StaticProvider, which also marks the inventory as non-refreshable.
Dynamic providers must respect context.Context; the runtime applies model_inventory.fetch_timeout per backend
inventory fetch.
Every feature plugin should have:
- unit tests for config decoding and invalid config;
- stage-runner tests for success, fail-open/fail-closed behavior, ordering, and canonical revalidation;
- integration tests through
runtimebundleorstdhttpwhen the plugin depends on session, routing, or stream behavior; - regression tests for security-sensitive behavior such as redaction, auth, tool blocking, or capture.
Protocol plugins should also include golden wire fixtures, streaming tests, cancellation tests, and fuzz tests for decoders where practical.
internal/plugins/features/REFERENCE_PLUGINS.md lists reference feature plugins registered by the standard bundle. They are proof plugins, not a license to put product logic into core. Prefer hardening or promoting a reference plugin when it already proves the seam you need.
Current proof areas include first-session auto append, tool policy, workspace guard, traffic transcript/capture, and verifier completion gates.
Allowed:
- feature plugin ->
pkg/lipapi,pkg/lipsdk, standard library, narrow external dependencies; - backend plugin -> provider SDK,
pkg/lipapi,pkg/lipsdk, internal backend-local helpers; - frontend plugin -> HTTP/wire helpers,
pkg/lipapi,pkg/lipsdk, runtime executor view contracts; - standard distribution -> concrete bundled plugins and core wiring packages.
Forbidden:
internal/coreimporting concrete plugins;pkg/lipapiorpkg/lipsdkimportinginternal/...packages;- provider SDK imports outside backend plugins, refclients, or tests explicitly designed for conformance;
- feature plugins importing executor, routing, or B2BUA internals to bypass SDK seams.
If a plugin cannot be implemented without a forbidden import, document the missing seam before implementing the feature.