---
phase: 02-scanners
verified: 2026-02-26T18:35:00Z
status: passed
score: 5/5
---

# Phase 2: Scanners — Verification

**Phase Goal:** Developers can scan text for PII, topics, toxicity, cyber threats, and jailbreak attempts with a single `guard.scan_input()` call
**Verified:** 2026-02-26T18:35:00Z
**Status:** passed
**Re-verification:** No — initial verification

## Must-Have Checks

### 1. `guard.scan_input()` sends real HTTP request and returns structured ScanResult
**Status:** Verified
**Evidence:**
`Guard.scan_input()` in `guard.py` (lines 89–154) constructs a `guardline_specs` dict from scanner instances, POSTs to `{base_url}/api/security/scan` via `self._session.post(url, json=payload, timeout=self._timeout)`, parses the response with `ScanResult.model_validate(resp.json())`, and returns a `ScanResult`. No stub patterns. The session is a real `requests.Session` built in `_http.py` with auth headers. `ScanResult` (models.py) has fields: `status`, `processed_text`, `placeholders`, `level`, `scanners` (dict of `ScannerResult` keyed by guardline ID), and `risk_categories`. `ScannerResult` has `triggered`, `score`, `detections`. This is a fully wired HTTP call, not a placeholder.
**File(s):**
- `/Users/shalev/Desktop/meshulash_guard/src/meshulash_guard/guard.py` (lines 89–154)
- `/Users/shalev/Desktop/meshulash_guard/src/meshulash_guard/_http.py`
- `/Users/shalev/Desktop/meshulash_guard/src/meshulash_guard/models.py`

---

### 2. All five scanner classes are importable with their scoped label enums
**Status:** Verified
**Evidence:**
Confirmed via live Python execution with `PYTHONPATH=src`:
```
from meshulash_guard import (
    PIIScanner, PIILabel, TopicScanner, TopicLabel,
    ToxicityScanner, ToxicityLabel, CyberScanner, CyberLabel,
    JailbreakScanner, JailbreakLabel
)
```
All 10 names resolve without error. The re-export chain is fully wired:
- `scanners/__init__.py` imports all 5 scanner classes and 5 label enums
- `meshulash_guard/__init__.py` imports from `.scanners` and lists all 10 names in `__all__`

Enum member counts (including ALL sentinel):
- `PIILabel`: 53 members (47 canonical + 5 bundle shortcuts + 1 ALL)
- `TopicLabel`: 31 members (30 topics + ALL)
- `ToxicityLabel`: 5 members (4 labels + ALL)
- `CyberLabel`: 12 members (11 labels + ALL)
- `JailbreakLabel`: 3 members (2 labels + ALL)

**File(s):**
- `/Users/shalev/Desktop/meshulash_guard/src/meshulash_guard/__init__.py`
- `/Users/shalev/Desktop/meshulash_guard/src/meshulash_guard/scanners/__init__.py`
- `/Users/shalev/Desktop/meshulash_guard/src/meshulash_guard/scanners/pii.py`
- `/Users/shalev/Desktop/meshulash_guard/src/meshulash_guard/scanners/topic.py`
- `/Users/shalev/Desktop/meshulash_guard/src/meshulash_guard/scanners/toxicity.py`
- `/Users/shalev/Desktop/meshulash_guard/src/meshulash_guard/scanners/cyber.py`
- `/Users/shalev/Desktop/meshulash_guard/src/meshulash_guard/scanners/jailbreak.py`

---

### 3. PIIScanner accepts risk bundle shortcuts as shorthand for predefined label groups
**Status:** Verified
**Evidence:**
`_expand_labels()` in `pii.py` (lines 144–174) expands bundle shortcuts SDK-side before building the guardline spec:
- `PIILabel.PII` → 23 canonical labels (identity, contact, financial)
- `PIILabel.SECRETS` → 20 credential labels
- `PIILabel.PHI` → 1 label (PERSON_NAME)
- `PIILabel.PCI` → 4 financial labels
- `PIILabel.TECH` → 2 technical labels
- `PIILabel.ALL` → 45 deduplicated canonical labels across all bundles

`_build_spec()` calls `_expand_labels()` and writes the result into `required.ner` and `required.regex` in the guardline spec. The `bundles` field is also populated for forward compatibility but is not the mechanism the server uses. Verified with live execution: `PIIScanner(labels=[PIILabel.PII]).to_guardline_spec()` produces a spec with 23 labels in `required.ner` and `required.regex`.

Per-label action overrides also work: `PIIScanner(labels=[PIILabel.EMAIL_ADDRESS, PIILabel.NATIONAL_ID], overrides={PIILabel.NATIONAL_ID: Action.BLOCK}).to_guardline_specs()` returns `{"main": ..., "override_0": ...}` with EMAIL_ADDRESS in the replace group and NATIONAL_ID in the block group.

**File(s):**
- `/Users/shalev/Desktop/meshulash_guard/src/meshulash_guard/scanners/pii.py` (lines 26–354)

---

### 4. `guard.scan_output()` raises `NotImplementedError` with "coming in v2" message
**Status:** Verified
**Evidence:**
`Guard.scan_output()` in `guard.py` (lines 156–162) is:
```python
def scan_output(self, prompt: str, output: str, scanners: list) -> ScanResult:
    raise NotImplementedError("scan_output() is coming in v2")
```
Verified with live execution: calling `Guard.scan_output(None, 'prompt', 'output', [])` raises `NotImplementedError` with message `"scan_output() is coming in v2"`. The message contains the required "coming in v2" text.

**File(s):**
- `/Users/shalev/Desktop/meshulash_guard/src/meshulash_guard/guard.py` (lines 156–162)

---

### 5. Each scanner's label enum maps to correct TC head/label registry — developers never see TC head names
**Status:** Verified
**Evidence:**
All four TC-based scanners use a `_TC_HEAD` class constant as an implementation detail:
- `TopicScanner._TC_HEAD = "topic"`
- `ToxicityScanner._TC_HEAD = "hate_speech"`
- `CyberScanner._TC_HEAD = "cysecbench"`
- `JailbreakScanner._TC_HEAD = "jailbreak"`

The `to_guardline_spec()` method builds `required.tc` as `[[_TC_HEAD, lbl.value], ...]` pairs. Developers interact only with the label enum (`TopicLabel.HEALTH`, `ToxicityLabel.HATE_SPEECH`, etc.) — the TC head string is never exposed in the public API and never appears in type signatures, `__all__`, or import surfaces.

Label values are exact server strings confirmed by the spec output: `TopicLabel.HEALTH.value == "Health"`, `ToxicityLabel.HATE_SPEECH.value == "hate speech"` (lowercase with space), `CyberLabel.MALWARE_ATTACKS.value == "Malware Attacks"` (mixed case). The `_StrValueMixin` ensures `str(label)` returns the exact value for JSON serialization.

PIIScanner uses a different mechanism (NER/regex labels, not TC heads) — canonical label strings like `"EMAIL_ADDRESS"` map directly to the server's label registry via `required.ner` and `required.regex`.

**File(s):**
- `/Users/shalev/Desktop/meshulash_guard/src/meshulash_guard/scanners/topic.py` (line 71)
- `/Users/shalev/Desktop/meshulash_guard/src/meshulash_guard/scanners/toxicity.py` (line 45)
- `/Users/shalev/Desktop/meshulash_guard/src/meshulash_guard/scanners/cyber.py` (line 52)
- `/Users/shalev/Desktop/meshulash_guard/src/meshulash_guard/scanners/jailbreak.py` (line 52)
- `/Users/shalev/Desktop/meshulash_guard/src/meshulash_guard/enums.py` (lines 16–23, `_StrValueMixin`)

---

## Summary

All 5 must-haves are verified. The phase goal is fully achieved.

`guard.scan_input()` is a complete, non-stub implementation: it builds a real `guardline_specs` payload from scanner instances (supporting both simple `to_guardline_spec()` and multi-spec `to_guardline_specs()` patterns), POSTs to the server, handles errors, and returns a fully typed `ScanResult` with all required fields.

All five scanner classes (`PIIScanner`, `TopicScanner`, `ToxicityScanner`, `CyberScanner`, `JailbreakScanner`) are importable from both `meshulash_guard` and `meshulash_guard.scanners` with their scoped label enums. PIIScanner bundle expansion (PII/PHI/PCI/SECRETS/TECH/ALL) is implemented SDK-side and produces correctly populated `required.ner`/`required.regex` arrays. TC scanners use `_TC_HEAD` constants that are fully hidden from developers. `scan_output()` raises `NotImplementedError("scan_output() is coming in v2")` as required.

One design note worth flagging (not a gap): `JailbreakScanner` is documented as "coming soon on the server" — the SDK-side implementation is complete and correct, but results may be empty until the server deploys the jailbreak TC head. This is intentional and documented in the class docstring.

## Gaps

None.

## Human Verification

None required for structural verification. The one item that would need a live server to confirm is whether the actual HTTP round-trip to `/api/security/scan` succeeds and returns parseable JSON matching `ScanResult`. That is a Phase 4 (integration test) concern, not a Phase 2 SDK structure concern.

---

*Verified: 2026-02-26T18:35:00Z*
*Verifier: Claude (gsd-verifier)*
