# Story 4.20: Migrate fapilog-tamper to Standard Plugin Architecture

**Status:** Complete  
**Priority:** High  
**Depends on:** 4.19 (Simple Plugin Configuration API), 4.14-4.17 (fapilog-tamper implementation)  
**Effort:** 3-5 days

---

## Problem Statement

The `fapilog-tamper` enterprise plugin currently uses a bespoke `IntegrityPlugin` protocol with special hooks (`get_enricher()`, `wrap_sink()`). This creates:

1. **Inconsistent discovery**: Uses `fapilog.integrity` entry point group, separate from standard plugin groups
2. **Special-case code paths**: `_build_pipeline()` has dedicated integrity plugin handling
3. **Configuration fragmentation**: `core.integrity_plugin` and `core.integrity_config` are separate from the unified `sink_config`/`enricher_config` pattern
4. **Documentation burden**: Two different plugin systems to explain

Story 4.19 established a clean plugin architecture. This story migrates `fapilog-tamper` to use it.

---

## Goals

1. **Expose IntegrityEnricher as a standard enricher** via `fapilog.enrichers` entry point
2. **Expose SealedSink as a standard sink** via `fapilog.sinks` entry point (wraps inner sink internally)
3. **Unified configuration** via `enricher_config.integrity` and `sink_config.sealed`
4. **Deprecate IntegrityPlugin protocol** and special handling in core
5. **Maintain backward compatibility** during transition period

---

## Design

### Current Architecture (Before)

```
┌─────────────────────────────────────────────────────────┐
│  fapilog core                                           │
│  ┌─────────────────────────────────────────────────┐   │
│  │ _build_pipeline()                                │   │
│  │   if integrity_plugin:                           │   │
│  │     plugin = load_integrity_plugin(name)         │   │
│  │     enricher = plugin.get_enricher(config)       │   │
│  │     for sink in sinks:                           │   │
│  │       sink = plugin.wrap_sink(sink, config)      │   │
│  └─────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────┘
                          │
                          ▼
┌─────────────────────────────────────────────────────────┐
│  fapilog-tamper (IntegrityPlugin protocol)              │
│  - _TamperSealedPlugin.get_enricher() → IntegrityEnricher
│  - _TamperSealedPlugin.wrap_sink() → SealedSink         │
└─────────────────────────────────────────────────────────┘
```

### Target Architecture (After)

```
┌─────────────────────────────────────────────────────────┐
│  fapilog core                                           │
│  ┌─────────────────────────────────────────────────┐   │
│  │ _build_pipeline()                                │   │
│  │   enrichers = _load_plugins("fapilog.enrichers") │   │
│  │   sinks = _load_plugins("fapilog.sinks")         │   │
│  └─────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────┘
                          │
                          ▼
┌─────────────────────────────────────────────────────────┐
│  fapilog-tamper                                         │
│  Entry points:                                          │
│    fapilog.enrichers = integrity = IntegrityEnricher    │
│    fapilog.sinks = sealed = SealedSink                  │
│                                                         │
│  Configuration via standard Settings:                   │
│    enricher_config.integrity.algorithm = "sha256"       │
│    sink_config.sealed.inner_sink = "rotating_file"      │
│    sink_config.sealed.manifest_path = "/var/log/..."    │
└─────────────────────────────────────────────────────────┘
```

---

## Implementation Plan

### Phase 1: Refactor fapilog-tamper Entry Points

**File: `packages/fapilog-tamper/pyproject.toml`**

```toml
[project.entry-points."fapilog.enrichers"]
integrity = "fapilog_tamper.enricher:IntegrityEnricher"

[project.entry-points."fapilog.sinks"]
sealed = "fapilog_tamper.sink:SealedSink"

# Keep legacy entry point during deprecation period
[project.entry-points."fapilog.integrity"]
tamper-sealed = "fapilog_tamper.plugin:_TamperSealedPlugin"
```

### Phase 2: Update IntegrityEnricher

Make `IntegrityEnricher` instantiable via standard `load_plugin()`:

```python
# packages/fapilog-tamper/src/fapilog_tamper/enricher.py

class IntegrityEnricher(BaseEnricher):
    """Adds hash chain fields to log entries for tamper detection."""

    def __init__(
        self,
        *,
        algorithm: str = "sha256",
        key_id: str | None = None,
        key_provider: str = "env",  # env | vault | kms
        chain_state_path: str | None = None,
    ) -> None:
        self._algorithm = algorithm
        self._key_id = key_id
        self._key_provider = key_provider
        self._chain_state = ChainState(path=chain_state_path)

    def enrich(self, entry: dict[str, Any]) -> dict[str, Any]:
        # Add _integrity fields
        ...
```

### Phase 3: Create SealedSink as Composable Sink

Instead of wrapping arbitrary sinks, `SealedSink` becomes a sink that:

1. Delegates to a configured inner sink
2. Generates manifests on rotation

```python
# packages/fapilog-tamper/src/fapilog_tamper/sink.py

class SealedSink(BaseSink):
    """Sink wrapper that generates tamper-evident manifests on rotation."""

    def __init__(
        self,
        *,
        inner_sink: str = "rotating_file",
        inner_config: dict[str, Any] | None = None,
        manifest_path: str | None = None,
        sign_manifests: bool = False,
        key_id: str | None = None,
    ) -> None:
        from fapilog.plugins import load_plugin

        self._inner = load_plugin("fapilog.sinks", inner_sink, inner_config or {})
        self._manifest_path = manifest_path
        self._sign = sign_manifests
        self._key_id = key_id

    async def write(self, entry: dict[str, Any]) -> None:
        await self._inner.write(entry)

    async def rotate(self) -> None:
        await self._inner.rotate()
        await self._generate_manifest()
```

### Phase 4: Add Configuration Models

**File: `packages/fapilog-tamper/src/fapilog_tamper/config.py`**

```python
class IntegrityEnricherConfig(BaseModel):
    """Configuration for integrity enricher."""

    algorithm: Literal["sha256", "sha3_256"] = Field(
        default="sha256", description="Hash algorithm for chain"
    )
    key_id: str | None = Field(
        default=None, description="Key identifier for HMAC"
    )
    key_provider: Literal["env", "vault", "kms"] = Field(
        default="env", description="Key provider backend"
    )
    chain_state_path: str | None = Field(
        default=None, description="Path to persist chain state"
    )


class SealedSinkConfig(BaseModel):
    """Configuration for sealed sink."""

    inner_sink: str = Field(
        default="rotating_file", description="Name of inner sink to wrap"
    )
    inner_config: dict[str, Any] = Field(
        default_factory=dict, description="Configuration for inner sink"
    )
    manifest_path: str | None = Field(
        default=None, description="Directory for manifest files"
    )
    sign_manifests: bool = Field(
        default=False, description="Sign manifests with Ed25519"
    )
    key_id: str | None = Field(
        default=None, description="Key ID for signing"
    )
```

### Phase 5: Deprecate IntegrityPlugin in Core

**File: `src/fapilog/plugins/integrity.py`**

```python
import warnings

def load_integrity_plugin(name: str) -> IntegrityPlugin:
    """Load an integrity plugin by name.

    .. deprecated:: 0.4.0
        Use standard plugin loading instead:
        - Enricher: settings.core.enrichers = ["integrity"]
        - Sink: settings.core.sinks = ["sealed"]
    """
    warnings.warn(
        "load_integrity_plugin is deprecated. Use standard plugin loading: "
        "core.enrichers=['integrity'], core.sinks=['sealed']",
        DeprecationWarning,
        stacklevel=2,
    )
    # ... existing implementation for backward compat
```

**File: `src/fapilog/__init__.py`**

Remove special-case integrity handling from `_build_pipeline()` after deprecation period.

### Phase 6: Update Documentation

1. Update `docs/addons/tamper-evident-logging.md` with new configuration
2. Add migration guide for existing `integrity_plugin` users
3. Update `docs/plugins/configuration.md` with examples

---

## Configuration Examples

### Before (Legacy)

```yaml
# settings.yaml
core:
  integrity_plugin: "tamper-sealed"
  integrity_config:
    algorithm: sha256
    sign_manifests: true
```

### After (Standard)

```yaml
# settings.yaml
core:
  enrichers:
    - runtime_info
    - context_vars
    - integrity # from fapilog-tamper
  sinks:
    - sealed # from fapilog-tamper

enricher_config:
  integrity:
    algorithm: sha256
    key_provider: vault
    chain_state_path: /var/lib/fapilog/chain.state

sink_config:
  sealed:
    inner_sink: rotating_file
    inner_config:
      directory: /var/log/myapp
      max_bytes: 10485760
    manifest_path: /var/log/myapp/manifests
    sign_manifests: true
```

---

## Acceptance Criteria

### Must Have

- [ ] `IntegrityEnricher` discoverable via `fapilog.enrichers` entry point
- [ ] `SealedSink` discoverable via `fapilog.sinks` entry point
- [ ] Both plugins configurable via standard `enricher_config`/`sink_config`
- [ ] `load_integrity_plugin()` emits `DeprecationWarning`
- [ ] Legacy `core.integrity_plugin` still works during deprecation
- [ ] All existing fapilog-tamper tests pass
- [ ] New tests for standard plugin loading path
- [ ] 90%+ coverage maintained

### Should Have

- [ ] Migration guide in documentation
- [ ] Example configurations in docs
- [ ] CLI: `fapilog plugins list` shows integrity/sealed

### Won't Have (This Story)

- [ ] Full removal of `IntegrityPlugin` protocol (future story)
- [ ] Automatic migration of existing configs

---

## Test Plan

1. **Unit Tests**

   - `test_integrity_enricher_via_load_plugin()`
   - `test_sealed_sink_via_load_plugin()`
   - `test_deprecation_warning_on_legacy_load()`
   - `test_legacy_integrity_plugin_still_works()`

2. **Integration Tests**

   - Full pipeline with `enrichers=["integrity"]`, `sinks=["sealed"]`
   - Verify hash chain integrity across log entries
   - Verify manifest generation on rotation

3. **Backward Compatibility**
   - Existing `core.integrity_plugin` configuration works
   - Both old and new paths produce equivalent output

---

## Migration Path

### For Users

1. **Immediate**: No action required; legacy config continues to work
2. **Recommended**: Update to new configuration style
3. **Future (0.5.0+)**: Legacy `integrity_plugin` setting removed

### Example Migration

```diff
 core:
-  integrity_plugin: "tamper-sealed"
-  integrity_config:
-    algorithm: sha256
+  enrichers:
+    - runtime_info
+    - integrity
+  sinks:
+    - sealed

+enricher_config:
+  integrity:
+    algorithm: sha256

+sink_config:
+  sealed:
+    inner_sink: rotating_file
```

---

## Risks & Mitigations

| Risk                              | Impact | Mitigation                                  |
| --------------------------------- | ------ | ------------------------------------------- |
| Breaking existing deployments     | High   | Maintain backward compat during deprecation |
| SealedSink composition complexity | Medium | Clear docs, sensible defaults               |
| Two code paths during transition  | Medium | Good test coverage for both                 |

---

## Estimated Effort

| Task                            | Effort      |
| ------------------------------- | ----------- |
| Refactor entry points           | 0.5 day     |
| Update IntegrityEnricher        | 0.5 day     |
| Create SealedSink as composable | 1 day       |
| Add config models               | 0.5 day     |
| Deprecation warnings            | 0.25 day    |
| Tests                           | 1 day       |
| Documentation                   | 0.5 day     |
| **Total**                       | **~4 days** |

---

## Open Questions

1. **Should `SealedSink` support multiple inner sinks?** (e.g., multiplex to file + remote)

   - Recommendation: No, keep simple. Use standard multiplexing at pipeline level.

2. **Key management integration**: Should key provider config be shared between enricher and sink?
   - Recommendation: Yes, add a shared `TamperConfig` at top level.

---

## Related Stories

- **4.19**: Simple Plugin Configuration API (prerequisite)
- **4.14-4.17**: Original fapilog-tamper implementation
- **Future**: Remove deprecated IntegrityPlugin protocol
