Plugin Architecture
⚠️ This is a design specification, not a shipping status document. It describes how Poindexter is evolving architecturally. Some sections describe shipped work; others describe future phases. For current deployment state, see CLAUDE.md.
Last Updated: 2026-05-13
Status: 📐 Design — umbrella #64 v3 locked; child phases #65-#72 rewritten to match. Layered on top of the plugin substrate is Module v1 (#490, spec at module-v1.md) — the unit of business-function composition (one Module = one business: content / finance / customer-support / ops). The first four Module v1 phases shipped 2026-05-13.
Scope: Canonical in-repo reference for how Poindexter is evolving from a handful of god-files into a plugin-shaped system.
Capability plugins vs business modules. The Phases A–J on this page describe capability plugins — atoms likeTap,Stage,Provider,LLMProvider. Module v1 is the next layer up: a Module bundles several capability plugins into a manifested, install-shaped business function. They’re orthogonal axes, not competing decompositions. Most extensions slot into the capability layer; you only reach for a Module when you’re adding a new business surface that needs its own DB migrations, jobs, routes, dashboards.
v3 locked decisions (2026-04-19):
- Secrets encryption-at-rest is core scope (Phase A delivers via pgcrypto), not “known gap.”
- Tracing + observability instrumentation (OTel, Tempo, OpenLLMetry) is deferred to a post-refactor Phase I. We’ll know what to monitor after the refactor is done.
- Phase J (
LLMProviderplugin family) confirmed as core scope. Swap Ollama → vllm/llama.cpp/TGI/LocalAI by one app_settings row.- Phase A0 (integration harness, #21) shipped 2026-04-19. Phase A is unblocked.
- Other v2 gaps (config schema versioning, resource limits, plugin-to-plugin deps) → Phase I.
Problem
Poindexter keeps inventing “feature parent + sources as children” informally in config (enabled_topic_sources, image_primary_source, qa_workflow_*) but the code doesn’t match. The result is god files that make every new integration a painful edit:
Every new Tap (Slack, Notion, Gmail), Provider (Midjourney, Flux), Reviewer (plagiarism check), or Adapter (new social platform) requires editing a 1000+ line file instead of dropping in a file. That blocks the plugin ecosystem vision and the Pro subscription overlay.
Core insight: adopt standards, don’t invent
Every custom system originally proposed has a mature, free, OSS equivalent already in-stack or trivially added. The refactor does not build a custom plugin framework. Each Protocol wraps a standard.
Net result: ~3000 lines of god-file code deleted, Singer catalog unlocked, zero new services to run, observability consolidated in the Grafana stack.
The original six plugin Protocols
The Phase A–J design described below locks in the original six plugin Protocols. Since this design landed, the substrate has grown beyond that initial set — seeplugins/registry.py::ENTRY_POINT_GROUPS
for the canonical list (currently 18 groups, including specialised
Provider variants like image_providers, video_providers,
audio_gen_providers, tts_providers, caption_providers,
media_compositors, publish_adapters, and the Module v1 group
that bundles capability plugins into business-function units).
All live under src/cofounder_agent/plugins/ as the canonical contracts. All discovered via importlib.metadata.entry_points.
1. Tap — data ingestion
- Native Python Taps — for internal sources (our own posts, our own memory files, audit log). Fast, no subprocess overhead.
- SingerTap wrapper — wraps any Singer binary (tap-github, tap-slack, tap-gmail, tap-google-analytics). Subprocess + stdout JSON-lines. Unlocks the public Singer catalog with zero per-source code.
scripts/auto-embed.py’s six hardcoded phases.
2. Probe — state checking
Probes emit Prometheus metrics. Alerting moves to Alertmanager rules (YAML in infrastructure/prometheus/alerts/). The brain daemon becomes an Alertmanager webhook consumer — it interprets alerts and decides on auto-remediation versus human escalation; it no longer runs the check loop itself.
This is the biggest conceptual shift in the refactor. Phase D handles it additive-first: expose metrics alongside existing probes, verify Alertmanager fires identically, then delete the legacy.
3. Job — scheduled maintenance
PluginScheduler uses inspect.signature to forward site_config
only to jobs that declare it in their run() kwargs — jobs that
don’t need app_settings can keep the two-arg shape (Phase H, GH#95).
apscheduler is the runner. Each Job registers via entry_points. idle_worker.py becomes a thin bootstrap that hands jobs to apscheduler’s async scheduler. State (last_run_at) persisted in Postgres — survival across restarts is free.
4. Stage — pipeline transformer
Promotes the existing services/phases/base_phase.py contract. Stage specializations already partly shipped:
Reviewer(Stage)— scores content (programmatic_validator, llm_critic, seo_checker, url_verifier)Adapter(Stage)— publishes to a platform (the legacysocial_adapters/impls were retired 2026-06-29; distribution is nowpublishing.<name>handlers + Postiz)Provider(Stage)— generates media (Pexels, image-gen, AI-generation, future Midjourney/Flux)
5. Pack — bundled prompts + styles + configs
Not code; data. Distribution via pip:
- Free (community):
poindexter-pack-communityon public pypi, Apache-2.0 - Premium:
glad-labs-packon private pypi, license-gated
pip already handles install/update/uninstall/version-pin. No custom overlay CLI needed.
6. LLMProvider — inference backend
OpenAICompatProvider— generic HTTP client with configurablebase_url+model. Reaches Ollama (/v1endpoint), llama.cpp server, vllm, SGLang, HuggingFace TGI, LM Studio, LocalAI by config.OllamaNativeProvider— keeps Ollama-specific features (electricity cost tracking,/api/embed, model pull). Default for the out-of-box experience.LiteLLMProvider(shipped 2026-05-04, poindexter#199 phase 1) — seeservices/litellm_provider.md. Wraps the LiteLLM SDK so one plugin covers 100+ providers (Ollama, OpenAI, Anthropic, Gemini, Bedrock, Vertex, OpenRouter, etc.) with authoritative cost tracking + retries-with-backoff via mature OSS. Activates by settingplugin.llm_provider.primary.standard='litellm'. Phase 2 (deleteservices/model_router.py) waits on production validation.
HuggingFaceProvider for on-machine transformers hosting without a separate server.
Core ships OSS-only. Community plugins can wrap paid providers (Anthropic, OpenAI, Google Gemini, Groq, OpenRouter, etc.) and distribute via pypi. The OpenAICompatProvider already reaches some paid vendors by config (OpenRouter, Groq, Together, Fireworks; Anthropic has an OpenAI-compat mode). “No paid APIs” is a default shipping policy, not a contract constraint.
Plugin discovery: setuptools entry_points
Plugins declare themselves in theirpyproject.toml:
importlib.metadata.entry_points(group="poindexter.taps"). No pkgutil scan, no custom registry, no decorators. This is the same pattern pytest, click, flask use.
- Install:
pip install poindexter-tap-slack - Uninstall:
pip uninstall poindexter-tap-slack - Update:
pip install -U poindexter-tap-slack - List:
pip list | grep poindexter-orimportlib.metadata.entry_points()
Config boundary: DB vs file
Poindexter’s rule is “everything in the database,” with a practical exception for long-standing infra rules.
This split matches Prometheus/Grafana community conventions. Customers forking the repo inherit the canonical setup.
Plugin config shape in
app_settings:
Architectural invariants
- Everything configurable — every plugin reads from
app_settings(runtime) or repo files (infra rules); no hardcoded defaults that can’t be overridden. - Everything an adapter of a core functionality — six Protocols; every feature is one of them.
- Everything in the database — runtime config, plugin state, secrets, metrics history. Only
DATABASE_URLlives outside. - Drop-in additions —
pip install+ container restart picks up the new plugin. No central switch statements, no hardcoded plugin lists. - Core is OSS-only, community can extend — default shipping policy ships free/OSS backends (Ollama, Singer community taps, Prometheus). Community plugins can wrap paid providers and ship on their own pypi packages.
- Standards over inventions — when an OSS standard exists (Singer, OpenAI-compat, apscheduler, Prometheus, entry_points), we wrap it, not reinvent.
Migration phases
Refer to GitHub issues for the actionable scope. Suggested execution order:
Phase A must be fully shipped before any other phase merges. A half-done plugin framework is worse than the current god files because it creates ambiguity about where new code lives.
Observability strategy
In-scope for the refactor (Phase D): Prometheus metrics + Alertmanager rules + existing Grafana dashboards. Brain daemon pivots from running a probe loop to consuming Alertmanager webhooks. Zero new services to run — everything leans on infra we already have. Deferred to post-refactor Phase I: OpenTelemetry instrumentation, Tempo for distributed traces, OpenLLMetry conventions for LLM-specific spans, Langfuse for prompt playground + eval tooling. Rationale (Matt’s 2026-04-19 decision): we’ll know what to trace after the refactor is done and we can see where the pain actually lives. Adding tracing before that risks building dashboards for pre-refactor shapes. In the meantime:- Logs: structlog (already in stack). Request-ID propagation added as part of Phase D.
- LLM cost: native
cost_logstable stays — cheapest source of truth for electricity + token counts.
Dependency audit
Removed in prior sessions: Anthropic, OpenAI, Google Gemini, Railway, Woodpecker. dlvr.it was retired for Mastodon (which used a direct adapter until distribution moved to Postiz 2026-06-29, GH-36; Bluesky/atproto was also a direct adapter until it was dropped 2026-06-17 to unblock a cryptography CVE fix). The dlvr.it free tier still bridges the RSS feed to LinkedIn today; it remains the intended bridge for X/Twitter if/when the paid subscription is reactivated (the X API itself is $100/mo — not worth it). Sentry is NOT removed — it’s still active in the public-site and worker as the error-tracking layer.
Known gaps (tracked, not blocking)
Moved into core scope (Phase A):Secrets encryption-at-rest— now core (pgcrypto) per 2026-04-19 lock-in.
- Plugin config schema versioning + migrations
- Per-plugin resource limits (CPU/memory quotas)
- Plugin-to-plugin dependency resolution
- Full observability stack (OTel, Tempo, OpenLLMetry, Langfuse)
- Bare
except Exception:sweep (125 swallowing blocks across services + routes) memory_system.pydead code (966 lines — pyproject.toml marks it superseded by pgvector)— resolved 2026-05-16: Prefect Stage 4 deletedunified_orchestrator.py+task_executor.pyvscontent_router_service.pytask_executor.pyandunified_orchestrator.py;content_router_service.pyis now a thin TemplateRunner dispatcher.- Routes decomposition (
task_publishing_routes.py1049 lines,cms_routes.py959 lines) - 50-migration consolidation pass
- Dependency license audit
- Swap Google Analytics for Plausible/Matomo (only remaining non-OSS surface)
- Dependency license audit
- Swap GA for Plausible/Matomo
Further reading
- GitHub #64 — umbrella (v2)
- GitHub #53 — original plugin ecosystem vision (Taps / Probes / Prompt Packs)
- GitHub #20 — content_router split
- GitHub #56 — brain daemon pluggable watchdog
- Brain memory under
plugin refactor v2tag —mcp__poindexter__search_memory