## 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

# Writer Tools

Recipe scripts call the platform API through `ctx.api_client`, which is the same `PretorianClient` the rest of pretorin uses. This page covers the common write paths and how audit metadata gets stamped automatically when your recipe runs inside an execution context.

## How Audit Metadata Gets Stamped

The calling agent opens an execution context with `start_recipe(recipe_id, recipe_version, params)` and gets back a `context_id`. Subsequent platform writes from that MCP session pick up the context automatically and stamp:

    {
      "producer_kind": "recipe",
      "producer_id": "<recipe_id>",
      "producer_version": "<recipe_version>",
      "recipe_context_id": "<context_id>"
    }

When the calling agent makes an MCP-routed write — i.e. the write goes through `create_evidence`, `create_evidence_batch`, or `update_narrative` — it must pass `recipe_context_id`. The MCP handler reads that context and builds the metadata. Direct agent writes without a recipe context return a structured `recipe_required` error.

When your script makes a write directly through `ctx.api_client` (skipping MCP), you have to pass `audit_metadata` yourself. The helper to build it lives at `pretorin.evidence.audit_metadata`:

    from pretorin.evidence.audit_metadata import build_recipe_metadata

    audit_metadata = build_recipe_metadata(
        body=source_excerpt,
        source_uri="file://config/rbac.yaml",
        source_type="repo_file",
        recipe_id=ctx.recipe_id,
        recipe_version=ctx.recipe_version,
        source_label="RBAC configuration",
        source_locator="lines 12-30",
        source_excerpt=source_excerpt,
        capture_method="repository_file_read",
        redaction_summary=...,
    )

    evidence = EvidenceCreate(
        control_id="ac-2",
        framework_id="nist-800-53-r5",
        name="...",
        evidence_type="configuration",
        description="RBAC role mapping is enforced in the IdP configuration.",
        artifact_content=composed_markdown,
        audit_metadata=audit_metadata,
    )
    await ctx.api_client.create_evidence(ctx.system_id, evidence)

Most recipes don’t need the direct path — write through MCP and let the handler stamp.

## Read-Side Helpers

You’ll often need to read platform state before writing. The most useful methods on `ctx.api_client`:

Method | Returns | Use
--- | --- | ---
`list_systems()` | `list[dict]` | Find a system id when one wasn’t passed in `ctx.system_id`.
`get_test_manifest(system_id, stig_id=None)` | `dict` | The applicable rules for a system, optionally narrowed to one STIG.
`get_control(framework_id, control_id)` | `ControlDetail` | Full control text + family + status.
`get_controls_batch(framework_id, control_ids)` | dict | Batch fetch — cheaper than N calls.
`get_control_implementation(...)` | dict | Current narrative + status for one control.
`get_stig_applicability(system_id)` | dict | Which STIGs apply to a system.
`get_compliance_status(system_id, framework_id)` | dict | High-level coverage rollup.
`get_source_manifest(system_id)` | dict | Verified-sources state for the system.

### Manifest helpers for scanner recipes

If you’re building a scanner recipe, three helpers in `pretorin.scanners.manifest` cover the common shape:

    from pretorin.scanners.manifest import (
        fetch_test_manifest,
        rules_for_stig,
        summarize_results,
    )

    manifest = await fetch_test_manifest(ctx.api_client, ctx.system_id, stig_id="RHEL_9_STIG")
    rules = rules_for_stig(manifest, "RHEL_9_STIG")
    results = await my_scanner.execute(rules, config={"target": "local"})
    summary = summarize_results(results)

Every built-in scanner recipe uses exactly this pattern. If you’re wrapping a new scanner, copy `inspec-baseline/scripts/run_scan.py` as a starting point.

## Write-Side: Evidence

    from pretorin.client.models import EvidenceCreate

    evidence = EvidenceCreate(
        control_id="ac-2",
        framework_id="nist-800-53-r5",
        name="RBAC configuration excerpt",
        evidence_type="configuration",
        description="RBAC role mapping is enforced in the IdP configuration.",
        artifact_content=composed_markdown,
        audit_metadata=audit_metadata,
    )
    result = await ctx.api_client.create_evidence(ctx.system_id, evidence)

For batched writes (much faster when you have ≥ 10):

    from pretorin.client.models import EvidenceBatchItemCreate

    items = [
        EvidenceBatchItemCreate(
            control_id="ac-2",
            name="...",
            description="Short summary",
            artifact_content=composed_markdown,
            evidence_type="...",
            audit_metadata=audit_metadata,
        )
        for _ in batch
    ]
    response = await ctx.api_client.create_evidence_batch(
        ctx.system_id,
        framework_id="nist-800-53-r5",
        items=items,
    )

For binary/unstructured artifacts (a screenshot, a PDF, an exported report), upload the file:

    result = await ctx.api_client.upload_evidence(
        system_id=ctx.system_id,
        file_path="/tmp/my-screenshot.png",
        name="Console screenshot — IAM users page",
        evidence_type="screenshot",
        control_id="ac-2",
    )

## Write-Side: Narratives

    await ctx.api_client.update_narrative(
        system_id=ctx.system_id,
        framework_id="nist-800-53-r5",
        control_id="ac-2",
        narrative=composed_text,
        recipe_context_id=ctx.recipe_context_id,
        evidence_ids=["ev-123"],
    )

Narrative-producing recipes must cite evidence ids. Open the context with `start_recipe(..., evidence_ids=[...])`, compose only from those evidence items, then pass the same ids to `update_narrative`.

## Write-Side: Test Results

For scanner recipes:

    await ctx.api_client.submit_test_results(
        system_id=ctx.system_id,
        benchmark_id="RHEL_9_STIG",
        results=[...],   # list of TestResult dicts
    )

## Redaction and Markdown Composition

Two helpers from `pretorin.evidence`:

    from pretorin.evidence.redact import redact_secrets
    from pretorin.evidence.markdown import compose

    redacted_text, redaction = redact_secrets(raw_text)
    markdown = compose(
        prose="The IAM trust policy is configured to require MFA for assume-role.",
        snippet=redacted_text,
        snippet_lang="json",
        file_path="iam/trust-policy.json",
    )

`redact_secrets` returns a `RedactionResult` with counts per secret type (AWS access keys, GitHub tokens, JWTs, etc.). `compose` turns prose + snippet into the audit-grade markdown body the platform expects on evidence records.

These two are the building blocks of the `code-evidence-capture` built-in recipe — read its source under `src/pretorin/recipes/_recipes/code-evidence-capture/` for a real-world usage pattern.

## What Recipe Scripts Should Not Do

- Don’t bypass `ctx.api_client` to talk to the platform via raw HTTP. You’ll skip the auth, retry, and error-handling logic the client provides.
- Don’t construct `audit_metadata` from scratch. Use `build_recipe_metadata` / `build_recipe_metadata_from_context` so the shape stays consistent.
- Don’t call `start_recipe` from inside a script. The recipe context is already open — that’s how the script got invoked. Nesting is forbidden.
- Don’t assume `ctx.system_id` is set. If your recipe requires a system, raise early with a clear message rather than passing `None` through to the API.