# Extension Boundaries

> 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.

- Skill: `octaviantocan/extension-boundaries` (Agent Skill)
- Install (CLI): `npx skillmds@latest add octaviantocan/extension-boundaries`
- Raw SKILL.md: https://api.skillmd.com/api/skills/octaviantocan/extension-boundaries/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: AI & ML
- Author: octaviantocan (https://skillmd.com/u/octaviantocan)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/octaviantocan/extension-boundaries

---


<!-- AUTO-GENERATED by skill-gen -- DO NOT EDIT -->
<!-- Source: backend/app/channels/__init__.py -->
<!-- Source: backend/app/chat/__init__.py -->
<!-- Source: backend/app/conversations/__init__.py -->
<!-- Source: backend/app/plugins/__init__.py -->
<!-- Source: backend/app/providers/__init__.py -->

# 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:

1. Name the capability type: `provider`, `channel`, `cli_tool`, `python_tool`,
   `turn_context_provider`, `conversation_memory`, `agent_runtime`,
   `agent_profile`, `router`, `settings`, or `scheduler`.
2. Name the slot if users may choose among multiple implementations.
3. Decide whether the code is generic kernel code or a trusted bundled
   adapter.
4. Add or update the manifest first when the behavior is optional.
5. Declare env requirements in the manifest. Use `user_workspace` or
   `workspace` scope when values must be overridable per workspace/user.
6. Keep enabled/disabled behavior explicit. Disabled plugins must not
   advertise tools, providers, channels, or context providers.
7. Add tests at the interface: manifest validation, registry/slot resolution,
   enabled-state behavior, env gating, and the runtime adapter.
8. 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`.

```bash
just paw doctor --json
just paw context --json
```

For runtime changes, add focused tests and then run the relevant gates:

```bash
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.

