Skip to main content

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 like Tap, 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 (LLMProvider plugin 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 — see plugins/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

Two flavors of implementation:
  • 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.
Replaces 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 legacy social_adapters/ impls were retired 2026-06-29; distribution is now publishing.<name> handlers + Postiz)
  • Provider(Stage) — generates media (Pexels, image-gen, AI-generation, future Midjourney/Flux)
No workflow-engine adoption in this refactor. Pipeline stays hand-rolled. Temporal/Dagster revisit when managing multiple customer pipelines.

5. Pack — bundled prompts + styles + configs

Not code; data. Distribution via pip:
  • Free (community): poindexter-pack-community on public pypi, Apache-2.0
  • Premium: glad-labs-pack on private pypi, license-gated
pip already handles install/update/uninstall/version-pin. No custom overlay CLI needed.

6. LLMProvider — inference backend

Current state (2026-05-04): Three core providers shipped:
  1. OpenAICompatProvider — generic HTTP client with configurable base_url + model. Reaches Ollama (/v1 endpoint), llama.cpp server, vllm, SGLang, HuggingFace TGI, LM Studio, LocalAI by config.
  2. OllamaNativeProvider — keeps Ollama-specific features (electricity cost tracking, /api/embed, model pull). Default for the out-of-box experience.
  3. LiteLLMProvider (shipped 2026-05-04, poindexter#199 phase 1) — see services/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 setting plugin.llm_provider.primary.standard='litellm'. Phase 2 (delete services/model_router.py) waits on production validation.
Future plugins (lower priority): 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 their pyproject.toml:
Poindexter loads them via 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- or importlib.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:
One row per plugin instance. Enable/disable is a toggle. Inventory-ing installed plugins is one SELECT.

Architectural invariants

  1. Everything configurable — every plugin reads from app_settings (runtime) or repo files (infra rules); no hardcoded defaults that can’t be overridden.
  2. Everything an adapter of a core functionality — six Protocols; every feature is one of them.
  3. Everything in the database — runtime config, plugin state, secrets, metrics history. Only DATABASE_URL lives outside.
  4. Drop-in additionspip install + container restart picks up the new plugin. No central switch statements, no hardcoded plugin lists.
  5. 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.
  6. 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_logs table stays — cheapest source of truth for electricity + token counts.
One pane of glass (Grafana). No new vendor UIs.

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.
Deferred to Phase I (future, after refactor ships):
  • Plugin config schema versioning + migrations
  • Per-plugin resource limits (CPU/memory quotas)
  • Plugin-to-plugin dependency resolution
  • Full observability stack (OTel, Tempo, OpenLLMetry, Langfuse)
Other standing debt (becomes its own issue when needed):
  • Bare except Exception: sweep (125 swallowing blocks across services + routes)
  • memory_system.py dead code (966 lines — pyproject.toml marks it superseded by pgvector)
  • unified_orchestrator.py + task_executor.py vs content_router_service.py — resolved 2026-05-16: Prefect Stage 4 deleted task_executor.py and unified_orchestrator.py; content_router_service.py is now a thin TemplateRunner dispatcher.
  • Routes decomposition (task_publishing_routes.py 1049 lines, cms_routes.py 959 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