| name | extension-boundaries |
| description | Use when touching Pawrrtal channels, providers, tools, plugins, subagents, context providers, turn orchestration, or code that decides where an integration should live. Enforces the split between generic kernel code, manifest plugins, trusted runtime adapters, provider adapters, channel adapters, and agent runtime primitives. |
Pawrrtal Extension Boundaries
Use this before changing backend extension code. The goal is a thin generic
kernel with optional pieces installed as plugins.
Channel Adapters
A channel adapter talks to one user surface, such as Telegram or Google Chat.
Channel-specific code belongs under backend/app/channels/<channel>/ or
behind a channel plugin capability.
Channel adapters should:
- Normalize inbound/outbound messages through the channel base types.
- Choose a model and call the generic turn runner.
- Keep provider internals, SDK payloads, and tool factories out of channel code.
- Move reusable formatting to channel runtime helpers or a channel adapter.
If a new surface can send a message and receive a response, treat it as a
channel unless it is purely a local CLI/operator concern.
Kernel Rules
The kernel owns auth, workspaces, persistence, turn orchestration, plugin
discovery, enabled state, env resolution, and stable interfaces.
Directory rules:
- Keep
backend/app/chat/ provider-agnostic and channel-agnostic.
- Keep turn orchestration generic in
backend/app/turns/pipeline/; do not
add provider, channel, or tool special cases to the pipeline.
- Tool selection happens in the tool-surface/plugin layer, not inside providers
or route handlers.
- Share optional behavior through kernel interfaces, slots, or runtime
adapters instead of direct plugin-to-plugin imports.
Smells to fix:
| Smell | Fix |
|---|
chat/ imports a provider, MCP adapter, or channel module | Move the behavior behind a generic interface or plugin capability. |
| Turn orchestration checks for one provider, tool, or channel by name | Emit/handle a generic event or add an adapter hook. |
| A subagent is implemented as a plugin | Move orchestration to the agent runtime and let plugins contribute profiles or invoke it. |
Conversation Boundaries
backend/app/conversations/ owns provider-agnostic conversation data and
workflows. It must not grow provider-specific history helpers, thread stores,
or channel formatting code.
Smells to fix:
| Smell | Fix |
|---|
conversations/ contains gemini_*, codex_*, or another provider name | Move it to the provider package and expose a generic history adapter. |
| Conversation code formats Telegram or Google Chat output | Move it to the channel adapter. |
Plugin Platform
A plugin manifest is backend/plugins/<plugin_id>/plugin.json. It is
declarative metadata: capabilities, env requirements, validation commands,
permissions, slots, and entrypoints.
Plugin runtime code lives in backend/app/plugins/. It is trusted host code
plus bundled Python implementations that a manifest may activate.
A slot is a named extension point where multiple plugins can fit, such as
tasks, web_search, channel:telegram, provider:models, or
conversation_memory.
Before adding optional behavior:
- Name the capability type:
provider, channel, cli_tool, python_tool,
turn_context_provider, conversation_memory, agent_runtime,
agent_profile, router, settings, or scheduler.
- Name the slot if users may choose among multiple implementations.
- Decide whether the code is generic kernel code or a trusted bundled
adapter.
- Add or update the manifest first when the behavior is optional.
- Declare env requirements in the manifest. Use
user_workspace or
workspace scope when values must be overridable per workspace/user.
- Keep enabled/disabled behavior explicit. Disabled plugins must not
advertise tools, providers, channels, or context providers.
- Add tests at the interface: manifest validation, registry/slot resolution,
enabled-state behavior, env gating, and the runtime adapter.
- Verify through Paw when the behavior affects a user-visible flow.
Plugin smells to fix:
| Smell | Fix |
|---|
Plugin code reads os.environ directly for user/workspace settings | Declare env in the manifest and use the plugin env resolver. |
| A plugin is always on because it exists on disk | Add enabled-state tests and make the manifest default intentional. |
| Plugins import each other directly | Share behavior through kernel interfaces, slots, or runtime adapters. |
Provider Adapters
A provider adapter talks to one model provider or provider CLI. It belongs
under backend/app/providers/ or behind a provider plugin capability.
Provider adapters should:
- Translate generic
AgentTool objects into SDK-specific formats.
- Avoid importing tool modules or tool factories directly.
- Keep channel-specific formatting and command output out of providers.
- Keep provider-specific history/session code inside the provider package and
expose generic adapters to the rest of the app.
Smells to fix:
| Smell | Fix |
|---|
| A provider imports tool factories | Pass generic AgentTool values into the provider and translate there. |
| A channel imports provider internals | Route through the generic turn runner or provider factory. |
Verification
Use Paw as the operator surface for the supported CLI slice. From the repo root,
prefer just paw.
just paw doctor --json
just paw context --json
For runtime changes, add focused tests and then run the relevant gates:
cd backend && uv run pytest tests/test_plugin_discovery.py tests/test_plugin_tools.py
cd backend && uv run pytest tests/test_channel_plugins.py tests/test_provider_plugins.py
Run broader CI gates before claiming the PR is ready.