Skip to content
Back to skills

735 Provider Architecture 2a5ff347

ASecurity

**Status**: Accepted (staleness model superseded by [ADR 017](017-resource-and-ref-architecture.md)) **Date**: 2025-12-29 **Updated**: 2026-01-01

  • 9 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added October 11, 2026
developmentpythongogitapidatabasebackend

Works with

  • api
  • mcp

Security analysis

A100/100

Scanned October 11, 2026

npx -y skills add tools-only/X-Skills --skill 735-provider-architecture_2a5ff347 --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of 735 Provider Architecture 2a5ff347?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for 735 Provider Architecture 2a5ff347
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/tools-only-735-provider-architecture-2a5ff347/badge)](https://www.skillsdirectory.com/skills/tools-only-735-provider-architecture-2a5ff347)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

SKILL.md
# ADR 012: Provider Architecture

**Status**: Accepted (staleness model superseded by [ADR 017](017-resource-and-ref-architecture.md))
**Date**: 2025-12-29
**Updated**: 2026-01-01

## Context

Colin's current architecture is tightly coupled to the filesystem. Models are discovered from directories, the manifest is a JSON file, and outputs are written to a target directory. This works well for local development but prevents Colin from supporting other backends like databases, object stores, or knowledge graphs.

We want Colin to support scenarios like:
- Models in git (filesystem), compiled outputs pushed to S3
- Templates that reference external data from MCP servers or APIs
- Custom template functions provided by domain-specific integrations

## Goals

1. Separate storage backends from the compilation engine
2. Allow templates to reference external data sources
3. Support providers with varying capabilities (read-only vs read-write)
4. Keep zero-config filesystem behavior as the default

## Key Insight: Provider = Low-Level I/O

A provider is fundamentally a low-level I/O handler. The base capability is fetching content; storage providers extend this with writing.

**Provider** - Fetches content, returns Resource objects. Examples: MCP servers, HTTP APIs, S3.

**Storage** - Provider that can also write. Used for project/artifact storage. Examples: filesystem, S3.

**ref()** - User-facing function that tracks dependencies. Accepts string paths (project refs) or Resource objects (provider refs).

**Renderer** - Separate from providers entirely. Pure content transformation (not URI-based). Lives in `renders/` alongside `providers/`.

This separation is critical:
- Provider template functions return `Resource` objects (content + ref + version)
- `ref()` tracks the resource's Ref for staleness checking
- This allows reading without dependency tracking when needed (skip `ref()` wrapper)

> **Note:** The staleness/tracking model has evolved. See [ADR 017](017-resource-and-ref-architecture.md) for the current Resource/Ref architecture.

The directory structure:
```
colin/
  providers/
    base.py              # Provider base class (read returns str)
    project.py           # ProjectProvider (wraps provider for project:// scheme)
    mcp.py               # MCPProvider - read-only
    storage/
      base.py            # Storage base class (extends Provider, adds write)
      file.py            # FileStorage - local filesystem
  renders/
    base.py              # Renderer base class with validate()
    json.py              # JSON validation
    yaml.py              # YAML validation
    markdown.py          # Markdown passthrough
```

## Decision

### Base Classes

Providers use abstract base classes (ABC):

```python
# providers/base.py
class Provider(ABC):
    """Low-level I/O for a URI scheme. Returns raw content."""
    scheme: str

    @abstractmethod
    async def read(self, path: str) -> str:
        """Read content from path.

        Path has scheme stripped (e.g., 'greeting.md' not 'project://greeting.md').
        """
        ...

# providers/storage/base.py
class Storage(Provider):
    """Provider that also supports writing. Used for artifacts."""

    @abstractmethod
    async def write(self, path: str, content: str) -> None:
        """Write content to relative path."""
        ...

# providers/project.py
class ProjectProvider(Provider):
    """Provider for project:// URIs. Wraps artifact storage."""
    scheme: str = "project"

    def __init__(self, provider: Provider) -> None:
        self._provider = provider

    async def read(self, path: str) -> str:
        return await self._provider.read(path)
```

Key design choices:
- `read()` returns `str`, not `RefResult` - the ref() function handles wrapping
- `write()` returns `None` - we know where we requested the write
- Method names are intentionally minimal: `read` and `write`
- Paths have scheme stripped - routing already happened

### Storage with Base Path

Storage implementations know their base location:

```python
class FileStorage(Storage):
    scheme: str = "file"

    def __init__(self, base_path: Path) -> None:
        self.base_path = base_path.resolve()

    async def read(self, path: str) -> str:
        full_path = self.base_path / path
        return full_path.read_text(encoding="utf-8")

    async def write(self, path: str, content: str) -> None:
        full_path = self.base_path / path
        full_path.parent.mkdir(parents=True, exist_ok=True)
        full_path.write_text(content, encoding="utf-8")
```

This means:
- Storage receives relative paths only
- Storage resolves to full paths internally
- No URI parsing in storage - that's already done

### ProjectConfig as Source of Truth

`colin.toml` is the source of truth. Config stores resolved absolute paths:

```python
class ProjectConfig(BaseModel):
    project_root: Path     # Where colin.toml lives
    model_path: Path       # Absolute path to models
    output_path: Path      # Absolute path to output
    manifest_path: Path    # Absolute path to manifest.json
```

Paths are resolved at load time by `load_project()`, stored as absolute paths. The engine and storage don't need to do path resolution.

### Engine Takes Config

The engine takes config and artifact storage, loads manifest internally:

```python
def __init__(
    self,
    config: ProjectConfig,
    artifact_storage: Storage,
    default_model: str,
) -> None:
    self.config = config
    self.artifact_storage = artifact_storage
    self.manifest = self._load_manifest()  # From config.manifest_path
    self._project_provider = ProjectProvider(artifact_storage)
```

This means:
- Engine doesn't take separate paths - config has everything
- Engine loads manifest from config.manifest_path
- Engine creates ProjectProvider internally

### The ref() Function

`ref()` in context.py handles:
1. Parsing URI to extract scheme and path
2. Routing to provider for scheme
3. Calling `provider.read(path)` → `str`
4. Creating `RefResult` from content
5. Tracking dependency

This separation allows:
- Providers to be simple I/O handlers
- Dependency tracking in one place
- Reading without tracking when needed

### Renderers with validate()

Renderers have a `validate()` method for format checking:

```python
class Renderer(ABC):
    name: str
    extension: str = ".md"

    @abstractmethod
    def render(self, document: CompiledDocument) -> RenderResult: ...

    def validate(self, content: str) -> None:
        """Override to validate output format. Raises if invalid."""
        pass

class JSONRenderer(Renderer):
    def validate(self, content: str) -> None:
        json.loads(content)  # Raises JSONDecodeError if invalid

    def render(self, document: CompiledDocument) -> RenderResult:
        self.validate(document.output)
        return RenderResult(...)
```

### MCP as a Provider

MCP servers are providers that return raw content from `read()`, plus template functions accessible via `colin.mcp.<name>`:

```python
class MCPProvider(Provider):
    scheme: str  # Set to "mcp" or "mcp.<instance>"

    async def read(self, path: str) -> str:
        """Parse path, fetch resource/prompt, return content."""
        ...

    def get_functions(self) -> dict[str, Callable[..., Awaitable[object]]]:
        return {
            "resource": self._template_resource,
            "prompt": self._template_prompt,
        }
```

## Rationale

**Why does read() return str, not RefResult?** Separation of concerns. Provider handles I/O. ref() handles dependency tracking and wrapping. This allows reading without tracking when needed.

**Why does write() return None?** We know where we requested the write. No need for confirmation.

**Why does Storage take base_path?** Storage works with relative paths. Base path is set at construction. No path resolution at write time.

**Why does Engine take config?** Config is the source of truth. Engine shouldn't assemble paths itself. This eliminates the previous problem of taking abstract Storage but concrete Path arguments.

**Why does Engine load manifest internally?** Manifest location is in config. Engine is responsible for its own state.

**Why is ProjectProvider in providers/project.py?** It's a Provider (not Storage) that wraps a Provider. It doesn't belong in the storage directory even though it's typically instantiated with Storage.

## Consequences

- Provider.read() returns str, not RefResult
- ref() creates RefResult and tracks dependencies
- Storage receives relative paths, knows its base
- Engine takes config, loads manifest internally
- ProjectProvider is a Provider wrapping a Provider (not Storage-specific)
- Renderers have validate() for format checking

## Implementation Status

Completed:
1. ✅ Provider base class with `read()` returning str
2. ✅ Storage base class extending Provider with `write()` returning None
3. ✅ FileStorage with base_path constructor
4. ✅ ProjectProvider wrapping a Provider
5. ✅ MCPProvider with read() returning str
6. ✅ Renderer base class with validate()
7. ✅ JSON and YAML renderers with validation
8. ✅ Engine taking config, loading manifest internally
9. ✅ ProjectConfig with resolved absolute paths

Future:
- S3Storage for object storage
- HTTPProvider for web APIs

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.