---
phase: 03.1-demo-chatbot
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
  - demo/chatbot.py
autonomous: true
user_setup:
  - service: google-genai
    why: "Gemini API access for LLM chat"
    env_vars:
      - name: GEMINI_API_KEY
        source: "Google AI Studio -> Get API key (https://aistudio.google.com/apikey)"
  - service: meshulash-guard
    why: "Security scanning server"
    env_vars:
      - name: MESHULASH_API_KEY
        source: "Meshulash dashboard or deployment config"
      - name: MESHULASH_TENANT_ID
        source: "Meshulash dashboard — tenant identifier"
      - name: MESHULASH_SERVER_URL
        source: "Meshulash server URL (e.g., https://app.meshulash.ai)"

must_haves:
  truths:
    - "demo/chatbot.py is a runnable Python script that imports meshulash-guard and google-genai without error"
    - "Five named presets are defined exercising PII bundles, PII overrides, content moderation (toxicity+topic), cyber detection, and full guard (all scanners)"
    - "The chat loop scans input via guard.scan_input(), gates on blocked status before sending to Gemini, scans output via guard.scan_output(), and deanonymizes the final text"
    - "Scan results are printed in a developer-realistic format showing status, triggered scanners, scores, detections, and placeholder counts"
    - "Environment variables GEMINI_API_KEY, MESHULASH_API_KEY, MESHULASH_TENANT_ID, MESHULASH_SERVER_URL are validated at startup with clear error messages"
  artifacts:
    - path: "demo/chatbot.py"
      provides: "Complete Gemini chatbot demo exercising all SDK scanner types"
      min_lines: 150
  key_links:
    - from: "demo/chatbot.py"
      to: "meshulash_guard.Guard"
      via: "import and instantiation"
      pattern: "from meshulash_guard import.*Guard"
    - from: "demo/chatbot.py"
      to: "google.genai.Client"
      via: "import and instantiation"
      pattern: "from google import genai"
    - from: "chat_turn()"
      to: "guard.scan_input()"
      via: "scan before LLM call"
      pattern: "guard\\.scan_input\\("
    - from: "chat_turn()"
      to: "guard.deanonymize()"
      via: "restore originals after output scan"
      pattern: "guard\\.deanonymize\\("
---

<objective>
Create the demo chatbot script — a terminal REPL that connects to Gemini and routes all input/output through meshulash-guard scanners with five named presets.

Purpose: Proves the SDK works end-to-end on a real app and serves as a reference implementation showing how a developer would integrate meshulash-guard into an LLM application.
Output: `demo/chatbot.py` — a single runnable script exercising all SDK scanner types with the scan-input / gate / LLM / scan-output / deanonymize workflow.
</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/03.1-demo-chatbot/03.1-CONTEXT.md
@.planning/phases/03.1-demo-chatbot/03.1-RESEARCH.md
@src/meshulash_guard/__init__.py
@src/meshulash_guard/guard.py
@src/meshulash_guard/models.py
@src/meshulash_guard/enums.py
</context>

<tasks>

<task type="auto">
  <name>Task 1: Install google-genai and create demo/chatbot.py</name>
  <files>demo/chatbot.py</files>
  <action>
**Step 1: Install google-genai into the project virtualenv:**

```bash
/Users/shalev/Desktop/meshulash_guard/.venv/bin/pip install google-genai
```

**Step 2: Create `demo/chatbot.py`** with the following structure. This is a single Python script — not a package. The script is run directly: `.venv/bin/python demo/chatbot.py`.

The script must contain these sections, in order:

**1. Shebang + module docstring:**
```python
#!/usr/bin/env python3
"""meshulash-guard Demo Chatbot

Reference implementation showing how to integrate meshulash-guard
into a Gemini-powered LLM application.

Usage:
    export GEMINI_API_KEY="..."
    export MESHULASH_API_KEY="..."
    export MESHULASH_TENANT_ID="..."
    export MESHULASH_SERVER_URL="https://app.meshulash.ai"
    python demo/chatbot.py
"""
```

**2. Imports:** `os`, `sys`, `from google import genai`, `from google.genai import types`, and all needed meshulash-guard imports:
```python
from meshulash_guard import (
    Guard, Action, Condition,
    PIIScanner, PIILabel,
    ToxicityScanner, ToxicityLabel,
    TopicScanner, TopicLabel,
    CyberScanner, CyberLabel,
    JailbreakScanner, JailbreakLabel,
    ScanResult,
    AuthError, ConnectionError, ServerError,
)
```

**3. Environment validation:** A `_require_env(name)` helper that reads `os.environ.get(name)`, prints to stderr and `sys.exit(1)` if missing. Call it for all four env vars at module level:
- `GEMINI_API_KEY`
- `MESHULASH_API_KEY`
- `MESHULASH_TENANT_ID`
- `MESHULASH_SERVER_URL`

**4. PRESETS dict** — five presets keyed by string "1" through "5". Each preset has `name`, `description`, and `scanners` list. Use the exact preset definitions from the research (03.1-RESEARCH.md Pattern 1), with these five presets:

1. **PII Protection** — `PIIScanner(labels=[PIILabel.PII], action=Action.REPLACE)` — exercises bundle expansion with REPLACE action
2. **PII + Overrides** — `PIIScanner(labels=[PIILabel.PII, PIILabel.SECRETS], action=Action.REPLACE, overrides={PIILabel.SECRETS: Action.LOG})` — exercises per-label action overrides via to_guardline_specs()
3. **Content Moderation** — `ToxicityScanner(labels=[ToxicityLabel.TOXICITY, ToxicityLabel.HATE_SPEECH], action=Action.BLOCK)` + `TopicScanner(labels=[TopicLabel.SEXUAL_EXPLICIT, TopicLabel.IDENTITY_ATTACK], action=Action.BLOCK)` — exercises TC scanners with BLOCK
4. **Cyber Threat Detection** — `CyberScanner(labels=[CyberLabel.ALL], action=Action.BLOCK)` — exercises ALL label expansion with BLOCK
5. **Full Guard** — `PIIScanner(labels=[PIILabel.ALL], action=Action.REPLACE)` + `ToxicityScanner(labels=[ToxicityLabel.ALL], action=Action.BLOCK)` + `CyberScanner(labels=[CyberLabel.ALL], action=Action.BLOCK)` + a comment noting JailbreakScanner is forward-compatible (include `JailbreakScanner(labels=[JailbreakLabel.ALL], action=Action.BLOCK)` with a comment: `# Forward-compatible: activates when server deploys jailbreak TC head`)

**5. `_print_scan_result(direction, result)` helper** — prints scan results in developer-realistic format. Follow the exact pattern from research Pattern 3:
- Print `-- {direction} SCAN [{status.upper()}] --`
- If `result.risk_categories` is non-empty, print them
- For each scanner in `result.scanners`: if `sr.triggered`, print scanner ID, score, and detection summary (`label(action)` for each detection)
- If `result.placeholders` is non-empty, print placeholder count
- If nothing triggered (clean scan), the status line alone is sufficient output

**6. `chat_turn(guard, chat_session, scanners, user_input)` function** — the core loop body. Follow research Pattern 2 exactly:
1. `input_result = guard.scan_input(user_input, scanners=scanners)` — scan input
2. `_print_scan_result("INPUT", input_result)` — print result
3. If `input_result.status == "blocked"`: print block message, return early
4. `text_to_send = input_result.processed_text` — use processed (possibly redacted) text
5. `gemini_response = chat_session.send_message(text_to_send)` — send to Gemini
6. `raw_llm_text = gemini_response.text` — extract response text
7. `output_result = guard.scan_output(raw_llm_text, scanners=scanners)` — scan output
8. `_print_scan_result("OUTPUT", output_result)` — print result
9. If `output_result.status == "blocked"`: print block message, return early
10. `final_text = guard.deanonymize(output_result.processed_text)` — restore originals
11. Print `Assistant: {final_text}` with a blank line before and after

**7. `main()` function:**
1. Print banner: "meshulash-guard Demo Chatbot"
2. Print preset menu: for each preset, print number, name, and description
3. Read preset choice with `input("Preset number: ")`, validate, exit on invalid
4. Print selected preset name
5. Initialize Guard: `Guard(api_key=MESHULASH_API_KEY, tenant_id=MESHULASH_TENANT_ID, server_url=MESHULASH_SERVER_URL)`
6. Initialize Gemini: `genai.Client(api_key=GEMINI_API_KEY)`, then `client.chats.create(model="gemini-2.5-flash", config=types.GenerateContentConfig(system_instruction="You are a helpful assistant."))` — use NON-streaming, synchronous API
7. Print "Chat started. Type your message. Press Ctrl+C or Ctrl+D to exit."
8. REPL loop: `while True`, read `input("You: ").strip()`, catch `KeyboardInterrupt` and `EOFError` for clean exit, skip empty input, call `chat_turn()` wrapped in try/except for `(AuthError, ConnectionError, ServerError)` printing `[ERROR] SDK error: {e}`

**8. `if __name__ == "__main__": main()`** guard.

IMPORTANT notes:
- Do NOT use streaming (`send_message_stream`). Use synchronous `send_message()`.
- Do NOT use async API. Use synchronous `genai.Client`, not `client.aio`.
- Do NOT call `guard.clear_cache()` inside the turn loop. Vault must persist across turns for deanonymize to work.
- Use `input_result.processed_text` as text sent to Gemini, NOT the raw `user_input`.
- The deanonymize chain is: `gemini_response.text` -> `guard.scan_output()` -> `output_result.processed_text` -> `guard.deanonymize()` -> `final_text`.
- TopicLabel values must match the SDK's actual enum — verify from `src/meshulash_guard/scanners/topic.py` before writing.
  </action>
  <verify>
Run these checks:

```bash
cd /Users/shalev/Desktop/meshulash_guard

# 1. google-genai is installed
.venv/bin/python -c "from google import genai; print(f'google-genai installed: {genai.__version__}')"

# 2. Script exists and has correct structure
test -f demo/chatbot.py && echo "demo/chatbot.py exists"

# 3. All imports resolve without error (no live server needed)
.venv/bin/python -c "
import sys
sys.path.insert(0, 'src')

# Verify all meshulash-guard imports from the script work
from meshulash_guard import (
    Guard, Action, Condition,
    PIIScanner, PIILabel,
    ToxicityScanner, ToxicityLabel,
    TopicScanner, TopicLabel,
    CyberScanner, CyberLabel,
    JailbreakScanner, JailbreakLabel,
    ScanResult,
    AuthError, ConnectionError, ServerError,
)

# Verify google imports work
from google import genai
from google.genai import types

print('All imports resolve successfully')
"

# 4. Script parses without syntax errors
.venv/bin/python -c "
import ast
with open('demo/chatbot.py') as f:
    ast.parse(f.read())
print('Script parses without syntax errors')
"

# 5. Verify presets structure
.venv/bin/python -c "
import sys, importlib.util
spec = importlib.util.spec_from_file_location('chatbot', 'demo/chatbot.py')
# We can't fully import (needs env vars), but we can verify the file is valid Python
import ast
with open('demo/chatbot.py') as f:
    tree = ast.parse(f.read())

# Check that PRESETS is defined
names = [node.targets[0].id for node in ast.walk(tree) if isinstance(node, ast.Assign) and isinstance(node.targets[0], ast.Name)]
assert 'PRESETS' in names, 'PRESETS not defined'

# Check key functions exist
func_names = [node.name for node in ast.walk(tree) if isinstance(node, ast.FunctionDef)]
assert '_require_env' in func_names, '_require_env not defined'
assert '_print_scan_result' in func_names, '_print_scan_result not defined'
assert 'chat_turn' in func_names, 'chat_turn not defined'
assert 'main' in func_names, 'main not defined'

print(f'Structure verified: PRESETS defined, functions: {[f for f in func_names if not f.startswith(\"__\")]}')
"

# 6. Verify line count (should be 150+ for a complete implementation)
wc -l demo/chatbot.py
```
  </verify>
  <done>demo/chatbot.py exists with all imports resolving, five presets defined, all four key functions present (_require_env, _print_scan_result, chat_turn, main), and no syntax errors. google-genai is installed in .venv.</done>
</task>

<task type="auto">
  <name>Task 2: Verify scanner constructor calls match actual SDK signatures</name>
  <files>demo/chatbot.py</files>
  <action>
Read the actual scanner source files to verify the preset scanner constructor calls in demo/chatbot.py use the correct argument names, label enum values, and types. This catches mismatches between the research examples and the real SDK.

Check each scanner used in presets:

1. **PIIScanner**: Read `src/meshulash_guard/scanners/pii.py` — verify `__init__(self, labels, action, condition, threshold, allowlist, overrides)` signature and that `overrides` takes `dict[PIILabel, Action]`.

2. **ToxicityScanner**: Read `src/meshulash_guard/scanners/toxicity.py` — verify `__init__` signature and that `ToxicityLabel.TOXICITY` and `ToxicityLabel.HATE_SPEECH` exist.

3. **TopicScanner**: Read `src/meshulash_guard/scanners/topic.py` — verify the TopicLabel enum has `SEXUAL_EXPLICIT` and `IDENTITY_ATTACK` members (the research used these; confirm the actual member names match).

4. **CyberScanner**: Read `src/meshulash_guard/scanners/cyber.py` — verify `CyberLabel.ALL` exists.

5. **JailbreakScanner**: Read `src/meshulash_guard/scanners/jailbreak.py` — verify `JailbreakLabel.ALL` exists.

If any label names in the presets don't match the actual enum member names, fix them in demo/chatbot.py. The presets must use the real enum values — do NOT guess or rely on the research alone.

After verifying, run a comprehensive import test that actually constructs each preset's scanner list to confirm no runtime errors:

```python
from meshulash_guard import (
    Action, PIIScanner, PIILabel,
    ToxicityScanner, ToxicityLabel,
    TopicScanner, TopicLabel,
    CyberScanner, CyberLabel,
    JailbreakScanner, JailbreakLabel,
)

# Construct every preset's scanners to verify constructors work
preset1 = [PIIScanner(labels=[PIILabel.PII], action=Action.REPLACE)]
preset2 = [PIIScanner(labels=[PIILabel.PII, PIILabel.SECRETS], action=Action.REPLACE, overrides={PIILabel.SECRETS: Action.LOG})]
preset3 = [
    ToxicityScanner(labels=[ToxicityLabel.TOXICITY, ToxicityLabel.HATE_SPEECH], action=Action.BLOCK),
    TopicScanner(labels=[TopicLabel.????, TopicLabel.????], action=Action.BLOCK),  # Verify actual names
]
preset4 = [CyberScanner(labels=[CyberLabel.ALL], action=Action.BLOCK)]
preset5 = [
    PIIScanner(labels=[PIILabel.ALL], action=Action.REPLACE),
    ToxicityScanner(labels=[ToxicityLabel.ALL], action=Action.BLOCK),
    CyberScanner(labels=[CyberLabel.ALL], action=Action.BLOCK),
    JailbreakScanner(labels=[JailbreakLabel.ALL], action=Action.BLOCK),
]
```

Fix any mismatches found. The demo must construct all scanners without error.
  </action>
  <verify>
Run this comprehensive constructor verification:

```bash
cd /Users/shalev/Desktop/meshulash_guard
.venv/bin/python -c "
from meshulash_guard import (
    Action, PIIScanner, PIILabel,
    ToxicityScanner, ToxicityLabel,
    TopicScanner, TopicLabel,
    CyberScanner, CyberLabel,
    JailbreakScanner, JailbreakLabel,
)

# Preset 1: PII Protection
p1 = [PIIScanner(labels=[PIILabel.PII], action=Action.REPLACE)]
assert len(p1) == 1
print('Preset 1 (PII Protection): OK')

# Preset 2: PII + Overrides
p2 = [PIIScanner(labels=[PIILabel.PII, PIILabel.SECRETS], action=Action.REPLACE, overrides={PIILabel.SECRETS: Action.LOG})]
assert len(p2) == 1
# Verify override produces multiple specs
specs = p2[0].to_guardline_specs()
assert len(specs) >= 2, f'Expected 2+ specs from overrides, got {len(specs)}'
print(f'Preset 2 (PII + Overrides): OK — {len(specs)} guardline specs')

# Preset 3: Content Moderation (TopicLabel names must be verified from source)
p3_scanners = []
p3_scanners.append(ToxicityScanner(labels=[ToxicityLabel.TOXICITY, ToxicityLabel.HATE_SPEECH], action=Action.BLOCK))
# TopicScanner labels — use whatever names were verified from topic.py
import ast
with open('demo/chatbot.py') as f:
    tree = ast.parse(f.read())
print('Preset 3 (Content Moderation): OK')

# Preset 4: Cyber
p4 = [CyberScanner(labels=[CyberLabel.ALL], action=Action.BLOCK)]
print('Preset 4 (Cyber Threat Detection): OK')

# Preset 5: Full Guard
p5 = [
    PIIScanner(labels=[PIILabel.ALL], action=Action.REPLACE),
    ToxicityScanner(labels=[ToxicityLabel.ALL], action=Action.BLOCK),
    CyberScanner(labels=[CyberLabel.ALL], action=Action.BLOCK),
    JailbreakScanner(labels=[JailbreakLabel.ALL], action=Action.BLOCK),
]
print('Preset 5 (Full Guard): OK')

print()
print('ALL PRESET CONSTRUCTORS VERIFIED — no runtime errors')
"
```
  </verify>
  <done>All five presets construct their scanner lists without runtime error, TopicLabel and other enum member names match the actual SDK source, and PIIScanner overrides produce multiple guardline specs as expected.</done>
</task>

</tasks>

<verification>
Phase 3.1 is complete when:
1. `demo/chatbot.py` exists and parses without syntax errors
2. All imports (meshulash-guard + google-genai) resolve successfully
3. All five preset scanner constructors execute without runtime error
4. The script contains the complete scan-input / gate / LLM / scan-output / deanonymize workflow in chat_turn()
5. Environment variables are validated at startup with clear error messages
6. The _print_scan_result helper shows status, triggered scanners, scores, and placeholder counts
</verification>

<success_criteria>
- `python -c "import ast; ast.parse(open('demo/chatbot.py').read())"` passes (no syntax errors)
- All meshulash-guard and google-genai imports resolve without error
- All five preset scanner lists construct without error
- Script has 150+ lines (complete implementation, not a stub)
- Functions _require_env, _print_scan_result, chat_turn, main all defined
- chat_turn contains guard.scan_input, guard.scan_output, and guard.deanonymize calls
</success_criteria>

<output>
After completion, create `.planning/phases/03.1-demo-chatbot/03.1-01-SUMMARY.md`
</output>
