Metadata-Version: 2.4
Name: stallari-adapter-framework
Version: 0.1.0
Summary: Generic adapter mechanics for Stallari domain adapters — config loading, scope routing, MCP server scaffold, write-gate enforcement, conformance validation, child-MCP session management with DD-242 env scrub.
Project-URL: Homepage, https://github.com/Groupthink-dev/stallari-adapter-framework
Project-URL: Repository, https://github.com/Groupthink-dev/stallari-adapter-framework
Project-URL: Issues, https://github.com/Groupthink-dev/stallari-adapter-framework/issues
Author: Piers Dawson-Damer
License-Expression: MIT
License-File: LICENSE
Keywords: adapter,framework,mcp,stallari
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.12
Requires-Dist: fastmcp>=2.0.0
Requires-Dist: mcp>=1.20.0
Requires-Dist: pyyaml>=6.0
Description-Content-Type: text/markdown

# stallari-adapter-framework

Generic adapter mechanics for [Stallari](https://stallari.app) domain adapters. Extracted from `stallari-email-adapter` so per-domain shims (calendar, tasks, messages, vault, …) don't re-implement ~95% of the same boilerplate.

**Status:** `v0.1.0` — first PyPI publish. API stabilises at `v1.0.0` after `stallari-email-adapter` migrates onto it (DD-266 Phase B).

**Design decision:** [DD-266 — Adapter framework + thin per-domain shims](https://github.com/Groupthink-dev/stallari/blob/main/docs/dd/DD-266.md).

## What it owns

- **Contract loading** — bundles all 38 contract definitions from `stallari-pack-spec 3.0.0-alpha.1` (`accounting-v1`, `calendar-v1`, `email-v1`, …, `vault-v1`).
- **Conformance validation** — at server start, asserts every `required` op declared by the contract has a transform method on every configured provider. Refuses to start otherwise.
- **Scope routing** — `(operation, scope) → backend session` via `ScopeRouter`. `work` scope → Gmail; `personal` scope → Fastmail.
- **Write-gate enforcement** — `PolicyGate` wraps every `gated` op from the contract spec; returns `PolicyDeniedError` envelope when the gate is off.
- **Child-MCP session lifecycle** — `ProviderSession` manages stdio child MCPs in an `AsyncExitStack`. Spawns with `mcp.client.stdio.stdio_client`.
- **DD-242 env scrub** — every spawned child has `MCP_TRANSPORT`, `*_MCP_HOST`, `*_MCP_PORT` removed from its environment, both inherited and provider-resolved. Stdio-only enforcement.
- **Manifest synthesis** — emit a `services:` block for `stallari-plugin.yaml` from registered transforms (build-time, deterministic).
- **Typed error envelopes** — `OkResult`, `ProviderErrorResult`, `PolicyDeniedResult`, `ToolUnavailableResult` with `to_json()`.

## What the shim owns

- Contract ID declaration (one line)
- Per-provider transform classes (one class per backend, one method per operation)
- Provider command lookup (`["uvx", "fastmail-blade-mcp"]`)
- `stallari-plugin.yaml` (mostly inherited; `services:` block generated)

**Target shim size:** ≤200 LOC per (contract × N providers), of which ≥80% is transform logic.

## Quickstart for adapter authors

```python
from stallari_adapter_framework import (
    AdapterServer, BaseTransform, envelopes,
)

class FastmailEmailTransform(BaseTransform):
    contract_operations = {"search", "read", "mailboxes", "send", ...}

    async def search(self, query: str, **kwargs) -> dict:
        result = await self._call("mail_search", {"query": query, **kwargs})
        return envelopes.ok(result)

server = AdapterServer(
    contract_id="email-v1",
    transforms={
        "fastmail-blade": FastmailEmailTransform,
        "google-workspace": GmailEmailTransform,
    },
    name="stallari-email-adapter",
)
server.run()
```

## Configuration

Adapter config at `~/.config/stallari/adapters/<domain>.yaml`:

```yaml
domain: email
default_provider: fastmail-blade
scopes:
  work: google-workspace
  personal: fastmail-blade
write_enabled: false
providers:
  fastmail-blade:
    command: ["uvx", "fastmail-blade-mcp"]
    env:
      FASTMAIL_API_TOKEN: "${FASTMAIL_API_TOKEN}"
  google-workspace:
    command: ["uvx", "workspace-mcp", "--tools", "gmail"]
    env:
      GOOGLE_OAUTH_PORT: "8420"
```

Env-var fallback (when no config file exists) uses the generic triplet:

| Var | Purpose |
|-----|---------|
| `<DOMAIN>_ADAPTER_PROVIDER` | Provider name |
| `<DOMAIN>_ADAPTER_COMMAND` | Provider command (JSON array) |
| `<DOMAIN>_ADAPTER_ENV` | Provider env vars (JSON object) |
| `<DOMAIN>_WRITE_ENABLED` | Gate write ops (default `false`) |
| `<DOMAIN>_ADAPTER_LOG_LEVEL` | Logging level (default `INFO`) |

## Development

```bash
uv sync --group dev --group test
make test          # 30+ unit tests (conformance parametrised over 38 contracts)
make check         # ruff + mypy
make test-cov      # coverage report
```

Smoke fixtures live in `tests/manual/` (skipped in CI; run with `pytest -m manual`).

## Stability and versioning

SemVer-strict.

- **`v0.x`** — pre-stable; API may change between minors.
- **`v1.0.0`** — API frozen. Backwards-compatible minor bumps only.
- **Major bumps** require shim opt-in via the `adapter_framework: ">=X.Y,<X+1"` field in `stallari-plugin.yaml`.

## License

MIT
