---
phase: 01-foundation
plan: 02
type: execute
wave: 1
depends_on: []
files_modified:
  - /Users/shalev/Desktop/meshulash_guard/pyproject.toml
  - /Users/shalev/Desktop/meshulash_guard/src/meshulash_guard/__init__.py
  - /Users/shalev/Desktop/meshulash_guard/src/meshulash_guard/enums.py
  - /Users/shalev/Desktop/meshulash_guard/src/meshulash_guard/exceptions.py
  - /Users/shalev/Desktop/meshulash_guard/src/meshulash_guard/models.py
  - /Users/shalev/Desktop/meshulash_guard/src/meshulash_guard/_http.py
  - /Users/shalev/Desktop/meshulash_guard/src/meshulash_guard/guard.py
  - /Users/shalev/Desktop/meshulash_guard/src/meshulash_guard/scanners/__init__.py
  - /Users/shalev/Desktop/meshulash_guard/src/meshulash_guard/scanners/base.py
autonomous: true

must_haves:
  truths:
    - "pip install -e . succeeds from the repo root"
    - "from meshulash_guard import Guard, Action, Condition, ScanResult imports without error"
    - "Guard(api_key='x', tenant_id='y', server_url='http://localhost') constructs (may fail on auth but must not fail on import/type errors)"
    - "Action.REPLACE, Action.BLOCK, Action.LOG have string values 'replace', 'block', 'log'"
    - "Condition.ANY, Condition.ALL, Condition.K_OF, Condition.CONTEXTUAL have correct string values"
    - "ScanResult, ScannerResult, Detection are Pydantic BaseModel subclasses with frozen=True"
    - "BaseScanner is abstract and cannot be instantiated directly"
    - "BaseNormalizer is abstract and cannot be instantiated directly"
  artifacts:
    - path: "/Users/shalev/Desktop/meshulash_guard/pyproject.toml"
      provides: "PEP 517/518 package config with hatchling build backend"
      contains: "meshulash-guard"
    - path: "/Users/shalev/Desktop/meshulash_guard/src/meshulash_guard/__init__.py"
      provides: "Public API surface"
      exports: ["Guard", "Action", "Condition", "ScanResult", "ScannerResult", "Detection"]
    - path: "/Users/shalev/Desktop/meshulash_guard/src/meshulash_guard/enums.py"
      provides: "Action and Condition enums"
      contains: "class Action"
    - path: "/Users/shalev/Desktop/meshulash_guard/src/meshulash_guard/exceptions.py"
      provides: "Exception hierarchy"
      contains: "class MeshulashError"
    - path: "/Users/shalev/Desktop/meshulash_guard/src/meshulash_guard/models.py"
      provides: "Pydantic result types"
      contains: "class ScanResult"
    - path: "/Users/shalev/Desktop/meshulash_guard/src/meshulash_guard/_http.py"
      provides: "Session builder with retry"
      contains: "_build_session"
    - path: "/Users/shalev/Desktop/meshulash_guard/src/meshulash_guard/guard.py"
      provides: "Guard class with scan_input stub and scan_output NotImplementedError"
      contains: "class Guard"
    - path: "/Users/shalev/Desktop/meshulash_guard/src/meshulash_guard/scanners/base.py"
      provides: "BaseScanner and BaseNormalizer abstract classes"
      contains: "class BaseScanner"
  key_links:
    - from: "guard.py Guard.__init__"
      to: "_http._build_session"
      via: "import and call"
      pattern: "_build_session"
    - from: "guard.py Guard._validate_credentials"
      to: "/api/security/health/auth"
      via: "HTTP GET on init"
      pattern: "health/auth"
    - from: "__init__.py"
      to: "guard.py, models.py, enums.py, exceptions.py"
      via: "re-exports"
      pattern: "from .guard import Guard"
    - from: "guard.py Guard.scan_input"
      to: "/api/security/scan"
      via: "session.post with json payload"
      pattern: "api/security/scan"
---

<objective>
Create the meshulash-guard Python package with all SDK scaffold code: package config, enums, exceptions, Pydantic result types, HTTP client, Guard class, and abstract base classes.

Purpose: Developers can `pip install meshulash-guard` and import all core types. The Guard class is wired to make HTTP calls (scan_input will be completed in Phase 2 when scanners exist). This establishes every type, contract, and pattern that Phase 2 scanners will build on.

Output: A pip-installable Python package at `/Users/shalev/Desktop/meshulash_guard/` with the `src/meshulash_guard/` layout.
</objective>

<execution_context>
@/Users/shalev/.claude/get-shit-done/workflows/execute-plan.md
@/Users/shalev/.claude/get-shit-done/templates/summary.md
</execution_context>

<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/01-foundation/01-CONTEXT.md
@.planning/phases/01-foundation/01-RESEARCH.md
@SDK_DESIGN.md
</context>

<tasks>

<task type="auto">
  <name>Task 1: Create package structure with pyproject.toml, enums, and exceptions</name>
  <files>
    /Users/shalev/Desktop/meshulash_guard/pyproject.toml
    /Users/shalev/Desktop/meshulash_guard/src/meshulash_guard/__init__.py
    /Users/shalev/Desktop/meshulash_guard/src/meshulash_guard/enums.py
    /Users/shalev/Desktop/meshulash_guard/src/meshulash_guard/exceptions.py
  </files>
  <action>
Create the directory structure and foundational files. All files go in `/Users/shalev/Desktop/meshulash_guard/`.

**1. pyproject.toml** (at repo root)

Use hatchling build backend. Follow the exact pattern from RESEARCH.md:
```toml
[build-system]
requires = ["hatchling>=1.26"]
build-backend = "hatchling.build"

[project]
name = "meshulash-guard"
version = "0.1.0"
description = "Python SDK for the Meshulash AI security engine"
readme = "README.md"
requires-python = ">=3.9"
license = "Proprietary"
authors = [
    { name = "Meshulash", email = "dev@meshulash.ai" },
]
classifiers = [
    "Programming Language :: Python :: 3",
    "Operating System :: OS Independent",
    "Intended Audience :: Developers",
    "Topic :: Security",
]
dependencies = [
    "pydantic>=2.0,<3",
    "requests>=2.28,<3",
]

[project.optional-dependencies]
dev = [
    "pytest>=7.0",
    "pytest-mock>=3.0",
]

[tool.hatch.build.targets.wheel]
packages = ["src/meshulash_guard"]
```

**2. src/meshulash_guard/enums.py**

Two enums using `(str, Enum)` mixin for Python 3.9+ compatibility:

```python
from enum import Enum

class Action(str, Enum):
    """Maps to server PolicyAction values."""
    REPLACE = "replace"
    BLOCK = "block"
    LOG = "log"

class Condition(str, Enum):
    """Maps to server gating conditions."""
    ANY = "any"
    ALL = "all"
    K_OF = "k_of"
    CONTEXTUAL = "contextual_analysis"
```

**3. src/meshulash_guard/exceptions.py**

Module docstring should note that `ConnectionError` intentionally shadows the builtin (same pattern as `requests.exceptions.ConnectionError`).

```python
class MeshulashError(Exception):
    """Base exception for all meshulash-guard errors."""

class AuthError(MeshulashError):
    """API key invalid or missing."""

class ServerError(MeshulashError):
    """Server returned 5xx response."""
    def __init__(self, message: str, status_code: int):
        super().__init__(message)
        self.status_code = status_code

class ConnectionError(MeshulashError):
    """Could not reach the security server."""

class ValidationError(MeshulashError):
    """Request payload failed server-side validation."""
```

**4. src/meshulash_guard/__init__.py** (placeholder -- will be completed in Task 2 after all modules exist)

For now, create with just:
```python
"""Meshulash Guard - Python SDK for the Meshulash AI security engine."""
__version__ = "0.1.0"
```

Also create `src/meshulash_guard/scanners/__init__.py` as an empty file (scanner subpackage, populated in Phase 2).

Create all necessary directories: `src/meshulash_guard/` and `src/meshulash_guard/scanners/`.
  </action>
  <verify>
```bash
cd /Users/shalev/Desktop/meshulash_guard
python -c "
from src.meshulash_guard.enums import Action, Condition
assert Action.REPLACE.value == 'replace'
assert Action.BLOCK.value == 'block'
assert Action.LOG.value == 'log'
assert Condition.ANY.value == 'any'
assert Condition.CONTEXTUAL.value == 'contextual_analysis'
# str mixin test
assert Action.REPLACE == 'replace'
print('Enums OK')

from src.meshulash_guard.exceptions import MeshulashError, AuthError, ServerError, ConnectionError, ValidationError
assert issubclass(AuthError, MeshulashError)
assert issubclass(ServerError, MeshulashError)
assert issubclass(ConnectionError, MeshulashError)
assert issubclass(ValidationError, MeshulashError)
try:
    raise ServerError('test', 500)
except ServerError as e:
    assert e.status_code == 500
print('Exceptions OK')
"
```
  </verify>
  <done>
- pyproject.toml exists with correct metadata, hatchling backend, pydantic+requests deps
- Action enum has REPLACE/BLOCK/LOG with string values, usable as strings
- Condition enum has ANY/ALL/K_OF/CONTEXTUAL with correct server-matching string values
- Exception hierarchy: MeshulashError base with AuthError, ServerError (with status_code), ConnectionError, ValidationError
- Directory structure: src/meshulash_guard/ and src/meshulash_guard/scanners/ exist
  </done>
</task>

<task type="auto">
  <name>Task 2: Create Pydantic models, HTTP client, Guard class, base classes, and finalize __init__.py</name>
  <files>
    /Users/shalev/Desktop/meshulash_guard/src/meshulash_guard/models.py
    /Users/shalev/Desktop/meshulash_guard/src/meshulash_guard/_http.py
    /Users/shalev/Desktop/meshulash_guard/src/meshulash_guard/guard.py
    /Users/shalev/Desktop/meshulash_guard/src/meshulash_guard/scanners/base.py
    /Users/shalev/Desktop/meshulash_guard/src/meshulash_guard/scanners/__init__.py
    /Users/shalev/Desktop/meshulash_guard/src/meshulash_guard/__init__.py
  </files>
  <action>
**1. src/meshulash_guard/models.py**

Pydantic v2 result types. Use `ConfigDict(frozen=True)` for immutability. Use v2 API only (model_validate, model_dump -- NOT .dict() or .parse_obj()).

```python
from pydantic import BaseModel, ConfigDict
from typing import Optional
from .enums import Action

class Detection(BaseModel):
    model_config = ConfigDict(frozen=True)
    label: str
    text: str
    start: int
    end: int
    confidence: float
    action: Action

class ScannerResult(BaseModel):
    model_config = ConfigDict(frozen=True)
    triggered: bool
    score: float
    detections: list[Detection]

class ScanResult(BaseModel):
    model_config = ConfigDict(frozen=True)
    status: str                           # "clean" | "secured" | "blocked"
    processed_text: str
    placeholders: dict[str, str]
    level: int
    scanners: dict[str, ScannerResult]    # keyed by scanner/guardline ID
    risk_categories: list[str]
```

**2. src/meshulash_guard/_http.py** (private module, underscore prefix)

Build a `requests.Session` with:
- `X-Api-Key` and `X-Tenant-Id` as session-level headers
- `Accept: application/json` as session-level header
- Do NOT set `Content-Type` as session-level header (breaks multipart later -- see Pitfall 3 in RESEARCH.md)
- Retry adapter: `Retry(total=3, backoff_factor=0.5, status_forcelist=[502, 503, 504], allowed_methods={"POST"})` -- MUST include `allowed_methods={"POST"}` (Pitfall 1 in RESEARCH.md)
- Mount adapter on both `https://` and `http://`

```python
import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry

def _build_session(api_key: str, tenant_id: str) -> requests.Session:
    session = requests.Session()
    session.headers.update({
        "X-Api-Key": api_key,
        "X-Tenant-Id": tenant_id,
        "Accept": "application/json",
    })
    retry = Retry(
        total=3,
        backoff_factor=0.5,
        status_forcelist=[502, 503, 504],
        allowed_methods={"POST"},
    )
    adapter = HTTPAdapter(max_retries=retry)
    session.mount("https://", adapter)
    session.mount("http://", adapter)
    return session
```

**3. src/meshulash_guard/scanners/base.py**

Abstract base classes using `abc.ABC` and `@abstractmethod`:

```python
from abc import ABC, abstractmethod

class BaseScanner(ABC):
    """Base class for all scanners. Each scanner translates its config into a guardline spec."""

    @abstractmethod
    def to_guardline_spec(self) -> dict:
        """Translate scanner config into a guardline_spec dict for the server."""

class BaseNormalizer(ABC):
    """Base class for normalizers. Contract defined in v1; server support in v2."""

    @abstractmethod
    def to_normalizer_spec(self) -> dict:
        """Translate normalizer config into a server-side normalizer spec."""
```

**4. src/meshulash_guard/scanners/__init__.py**

Empty for now -- scanners will be re-exported here in Phase 2:
```python
"""Scanner implementations. Import individual scanners from their modules."""
```

**5. src/meshulash_guard/guard.py**

The Guard class. Key design points:

- `__init__` takes `api_key: str`, `tenant_id: str`, optional `server_url: str | None = None`, optional `timeout: int = 60`
- Default server URL: `https://app.meshulash.ai` (from CONTEXT.md: cloud default)
- Builds session via `_build_session(api_key, tenant_id)`
- Calls `_validate_credentials()` to fail fast on invalid key
- Has `_vault: dict[str, str] = {}` for placeholder cache (Phase 3)

Methods:
- `_validate_credentials()`: GET to `{base_url}/api/security/health/auth` with timeout. Catches `requests.exceptions.ConnectionError` and re-raises as SDK ConnectionError. Checks status 401 -> AuthError, 5xx -> ServerError.
- `scan_input(text: str, scanners: list) -> ScanResult`: For Phase 1, this is a working method that:
  1. Builds `guardline_specs` dict from scanners: `{f"sdk_{scanner.__class__.__name__.lower()}_{i}": scanner.to_guardline_spec() for i, scanner in enumerate(scanners)}`
  2. POSTs to `{base_url}/api/security/scan` with `json={"text": text, "guardline_specs": guardline_specs}`, Content-Type application/json (set per-request, not session-level)
  3. Handles errors: ConnectionError, Timeout -> SDK exceptions, 401 -> AuthError, 422 -> ValidationError, 5xx -> ServerError
  4. Parses response with `ScanResult.model_validate(response.json())`
  5. Updates `self._vault` with result.placeholders
  6. Returns the ScanResult
- `scan_output(prompt: str, output: str, scanners: list) -> ScanResult`: Raises `NotImplementedError("scan_output() is coming in v2")`
- `deanonymize(text: str) -> str`: Replaces placeholders in text using `self._vault`
- `clear_cache() -> None`: Clears `self._vault`

Use `from __future__ import annotations` for Python 3.9 compat with `str | None` syntax.

Import `requests` the library carefully -- this is used for exception types (`requests.exceptions.ConnectionError`, `requests.exceptions.Timeout`). Import as `import requests as _requests` to avoid confusion in the module.

**6. Update src/meshulash_guard/__init__.py**

Finalize with all public exports:
```python
"""Meshulash Guard - Python SDK for the Meshulash AI security engine."""

from .guard import Guard
from .models import ScanResult, ScannerResult, Detection
from .enums import Action, Condition
from .exceptions import (
    MeshulashError,
    AuthError,
    ServerError,
    ConnectionError,
    ValidationError,
)

__version__ = "0.1.0"
__all__ = [
    "Guard",
    "ScanResult",
    "ScannerResult",
    "Detection",
    "Action",
    "Condition",
    "MeshulashError",
    "AuthError",
    "ServerError",
    "ConnectionError",
    "ValidationError",
]
```

Scanners are NOT exported at the top level -- they live in `meshulash_guard.scanners` (Phase 2).
  </action>
  <verify>
```bash
cd /Users/shalev/Desktop/meshulash_guard && pip install -e . && python -c "
# Test imports
from meshulash_guard import Guard, Action, Condition, ScanResult, ScannerResult, Detection
from meshulash_guard import MeshulashError, AuthError, ServerError, ConnectionError, ValidationError
from meshulash_guard.scanners.base import BaseScanner, BaseNormalizer
print('All imports OK')

# Test enums are usable as strings
assert Action.REPLACE == 'replace'
assert f'{Condition.ANY}' == 'any'
print('Enums OK')

# Test Pydantic models
d = Detection(label='EMAIL', text='a@b.com', start=0, end=7, confidence=0.9, action=Action.REPLACE)
assert d.label == 'EMAIL'
sr = ScannerResult(triggered=True, score=0.9, detections=[d])
assert sr.triggered is True
result = ScanResult(
    status='secured', processed_text='[EMAIL-1234]', placeholders={'[EMAIL-1234]': 'a@b.com'},
    level=2, scanners={'pii': sr}, risk_categories=['PII']
)
assert result.status == 'secured'
dumped = result.model_dump()
assert isinstance(dumped, dict)
print('Pydantic models OK')

# Test abstract classes
try:
    BaseScanner()
    assert False, 'Should not instantiate'
except TypeError:
    pass
try:
    BaseNormalizer()
    assert False, 'Should not instantiate'
except TypeError:
    pass
print('Abstract classes OK')

# Test exception hierarchy
try:
    raise AuthError('bad key')
except MeshulashError:
    pass
print('Exception hierarchy OK')

# Test Guard construction (will fail on auth but should not fail on type/import errors)
try:
    g = Guard(api_key='test', tenant_id='test', server_url='http://localhost:9999')
except (ConnectionError, Exception):
    pass  # Expected - no server running
print('Guard construction attempted OK')

print('ALL CHECKS PASSED')
"
```
  </verify>
  <done>
- `pip install -e .` succeeds from repo root
- `from meshulash_guard import Guard, Action, Condition, ScanResult` works
- All Pydantic models validate and serialize correctly with v2 API
- Guard class has scan_input (working), scan_output (NotImplementedError), deanonymize, clear_cache
- Guard.__init__ builds session with correct headers and retry, calls health/auth for key validation
- BaseScanner and BaseNormalizer are abstract and enforce to_guardline_spec()/to_normalizer_spec()
- _http._build_session creates session with retry on POST, no Content-Type header (set per-request)
- __init__.py exports Guard, ScanResult, ScannerResult, Detection, Action, Condition, all exceptions
- __version__ = "0.1.0"
  </done>
</task>

</tasks>

<verification>
1. Package installs: `cd /Users/shalev/Desktop/meshulash_guard && pip install -e .`
2. Core imports work: `python -c "from meshulash_guard import Guard, Action, Condition, ScanResult"`
3. Version accessible: `python -c "import meshulash_guard; print(meshulash_guard.__version__)"`  prints "0.1.0"
4. Pydantic v2 API used: `grep -r "model_dump\|model_validate" src/meshulash_guard/` finds hits, `grep -r "\.dict()\|parse_obj" src/meshulash_guard/` finds none
5. No Content-Type in session headers: `grep -r "Content-Type" src/meshulash_guard/_http.py` finds nothing
6. POST in retry allowed_methods: `grep "allowed_methods" src/meshulash_guard/_http.py` shows POST
</verification>

<success_criteria>
- Package is pip-installable with `pip install -e .`
- Guard, Action, Condition, ScanResult, ScannerResult, Detection importable from top-level
- All exceptions importable from top-level
- BaseScanner and BaseNormalizer importable from meshulash_guard.scanners.base
- Guard.scan_input builds guardline_specs from scanners and POSTs to /api/security/scan
- Guard.scan_output raises NotImplementedError
- Guard.deanonymize and Guard.clear_cache work for placeholder vault
- Pydantic models use v2 API exclusively (model_validate, model_dump, ConfigDict)
- HTTP session has retry on POST, no session-level Content-Type
</success_criteria>

<output>
After completion, create `.planning/phases/01-foundation/01-02-SUMMARY.md`
</output>
