## Keyboard shortcuts

Press `←` or `→` to navigate between chapters

Press `S` or `/` to search in the book

Press `?` to show this help

Press `Esc` to hide this help

# Worked Example: a Community Recipe

This walkthrough builds a community recipe end-to-end, the same way you would. The recipe captures the most recent N entries from a structured audit log file as evidence for an access-control review.

The recipe doesn’t ship with pretorin-cli — it’s a teaching artifact. Drop it under `~/.pretorin/recipes/audit-log-capture/` to actually run it; the files are reproduced below for reference.

## What This Recipe Does

The audit team needs evidence that an admin’s recent actions are being logged. They have a structured log file (one JSON event per line). The recipe:

1. Reads the last N entries from the log.
2. Filters to events matching a username.
3. Composes a markdown evidence body with the events as a code block.
4. Returns the composed text to the calling agent so the agent can hand it to `pretorin_create_evidence`.

The recipe doesn’t write evidence itself — it returns structured data the agent submits through the MCP write boundary. That’s where audit metadata gets stamped automatically.

## Directory Layout

    ~/.pretorin/recipes/audit-log-capture/
    ├── recipe.md
    ├── README.md
    └── scripts/
        └── capture.py

## `recipe.md`

    ---
    id: audit-log-capture
    version: 0.1.0
    name: "Audit Log Capture"
    description: "Capture the most recent admin events from a JSONL audit log and return a formatted markdown body for evidence submission."
    use_when: "The auditor needs evidence that admin actions are logged. You have a JSONL audit log file path and a username to filter on."
    produces: evidence
    author: "Example Team"
    license: Apache-2.0
    attests:
      - { control: AU-2, framework: nist-800-53-r5 }
      - { control: AU-3, framework: nist-800-53-r5 }
    params:
      log_path:
        type: string
        description: "Absolute path to the JSONL audit log file"
        required: true
      username:
        type: string
        description: "Admin username to filter events for"
        required: true
      limit:
        type: integer
        description: "Maximum number of events to include"
        default: 20
    scripts:
      capture:
        path: scripts/capture.py
        description: "Read the audit log, filter by username, return composed markdown."
        params:
          log_path:
            type: string
            description: "Absolute path to JSONL log"
            required: true
          username:
            type: string
            description: "Admin username"
            required: true
          limit:
            type: integer
            description: "Max events"
            default: 20
    ---

    # Audit Log Capture

    Reads the tail of a JSONL audit log, filters to events for one admin
    user, and returns a composed markdown body the calling agent can submit
    as a configuration evidence record.

    The agent should attach the result to the relevant `AU-2` / `AU-3`
    implementation narrative for the system.

## `scripts/capture.py`

    """Capture recent audit events for a specific admin user."""

    from __future__ import annotations

    import json
    from pathlib import Path
    from typing import Any

    from pretorin.evidence.markdown import compose

    async def run(
        ctx: Any,
        *,
        log_path: str,
        username: str,
        limit: int = 20,
    ) -> dict[str, Any]:
        """Read tail of JSONL log, filter to one user, compose evidence body."""
        path = Path(log_path)
        if not path.is_file():
            raise FileNotFoundError(f"audit log not found: {log_path}")

        matched: list[dict[str, Any]] = []
        for raw in path.read_text(encoding="utf-8").splitlines():
            if not raw.strip():
                continue
            try:
                event = json.loads(raw)
            except json.JSONDecodeError:
                continue
            if event.get("user") == username:
                matched.append(event)
            if len(matched) >= limit:
                break

        snippet = "\n".join(json.dumps(e, sort_keys=True) for e in matched)
        body = compose(
            prose=(
                f"The {len(matched)} most recent audit events for user "
                f"{username!r} from {path.name}. Each line is one structured "
                "event (timestamp, action, target, source IP)."
            ),
            snippet=snippet,
            snippet_lang="json",
            file_path=str(path),
        )
        return {
            "username": username,
            "event_count": len(matched),
            "evidence_body": body,
        }

## `README.md`

    # audit-log-capture

    A community recipe that captures admin audit events as markdown evidence.

    ## Try it

    1. Drop this directory under ~/.pretorin/recipes/audit-log-capture/
    2. Run `pretorin recipe validate audit-log-capture`
    3. From your AI agent (Claude Code, Codex CLI), ask:
       > "Capture the last 20 audit events for admin alice from /var/log/audit.jsonl
       > as evidence for AU-2."

## Walking Through It

### Why split into manifest + script

Everything inside the frontmatter is what the calling agent reads to decide whether to use the recipe. Everything in `scripts/capture.py` is how the work happens. Keep the description sharp — the agent picks based on it.

### Why use `compose` from `pretorin.evidence.markdown`

`compose` produces an audit-grade markdown body: prose explaining the context, a fenced code block with the snippet, an italic provenance footer with the file path and timestamp. Doing this by hand drifts; using the helper keeps every evidence record consistent.

### Why no `create_evidence` call

The recipe returns the composed body and lets the calling agent submit it via `pretorin_create_evidence`. This is the right shape for two reasons:

1. Audit metadata gets stamped at the MCP write boundary. The recipe context is open in the calling agent’s session; routing through MCP means the handler reads the context and stamps `producer_kind="recipe"` automatically.
2. The agent stays in the loop. The agent can review the composed body, ask the user to confirm before submitting, or attach extra metadata it pulled from elsewhere.

A recipe that takes raw input and produces structured output the agent hands to a writer tool is the most useful shape. Recipes that perform their own writes are sometimes appropriate (the scanner recipes do, via `submit_test_results`) but the default should be: return data, let the agent submit.

### How the agent invokes it

The MCP tool name is `pretorin_recipe_audit_log_capture__capture` (the hyphens in the recipe id become underscores; the script name is the suffix after `__`).

The agent’s call sequence:

    pretorin_start_recipe(id="audit-log-capture", version="0.1.0",
                          params={"log_path": "/var/log/audit.jsonl",
                                  "username": "alice", "limit": 20})
      → returns context_id

    pretorin_recipe_audit_log_capture__capture(
        log_path="/var/log/audit.jsonl",
        username="alice",
        limit=20,
    )
      → returns {"username": "alice", "event_count": 20, "evidence_body": "..."}

    pretorin_create_evidence(
        system_id="...",
        control_id="au-2",
        framework_id="nist-800-53-r5",
        name="Recent admin audit events for alice",
        evidence_type="configuration",
        description=<the evidence_body from the previous call>,
        recipe_context_id=<context_id>,
    )
      → platform stamps producer_kind="recipe", producer_id="audit-log-capture",
                        producer_version="0.1.0"

    pretorin_end_recipe(context_id=<context_id>, status="pass")

### What the agent should not do

The recipe doesn’t include a `submit` script that calls `create_evidence` directly. Why: every evidence record submitted from inside the recipe context picks up audit metadata at the MCP boundary; moving the write into the script would require the script to build metadata itself, which is one more place for the audit trail to drift.

When in doubt, return data, let the agent write.