# Template Security

## Overview

The `cleveragents.templates.secure_renderer` module provides a sandboxed
template renderer that prevents template injection attacks while
supporting simple variable substitution.

## Threat Model

Template injection occurs when user-controlled input is embedded in
template strings and evaluated. Python's `str.format()` is particularly
dangerous because it allows:

- **Attribute access**: `{obj.__class__.__init__.__globals__}` can leak
  sensitive module-level data.
- **Index access**: `{items[0]}` can read arbitrary collection entries.
- **Format specs**: `{val:>10}` can cause unexpected formatting or DoS.
- **Conversion flags**: `{val!r}` can change output representation.

The secure renderer rejects all of these, only allowing `{name}` style
substitution with simple identifiers.

## Allowed Syntax

Only the following template syntax is permitted:

```
{variable_name}
```

Where `variable_name` matches `\w+` (letters, digits, underscore).

Everything else is rejected:

| Pattern          | Risk                          | Result   |
|------------------|-------------------------------|----------|
| `{x.y}`          | Attribute access              | Rejected |
| `{x[0]}`         | Index access                  | Rejected |
| `{x:>10}`        | Format spec                   | Rejected |
| `{x!r}`          | Conversion flag               | Rejected |
| `{fn()}`         | Function call                 | Rejected |
| `{{ x }}`        | Jinja2 expression             | Rejected |
| `{% block %}`    | Jinja2 block                  | Rejected |

## Configuration

Use `TemplateConfig` to configure the renderer:

```python
from cleveragents.templates.secure_renderer import (
    SecureTemplateRenderer,
    TemplateConfig,
)

config = TemplateConfig(
    allowed_keys=frozenset({"name", "description", "role"}),
    max_template_length=10_000,   # default
    max_output_length=50_000,     # default
    reject_unknown_keys=True,     # default
    log_rejected=True,            # default
)
renderer = SecureTemplateRenderer(config=config)
```

### Parameters

- **`allowed_keys`**: Set of permitted placeholder names. Empty means
  any simple identifier is allowed (open mode).
- **`max_template_length`**: Maximum raw template string length
  (default: 10,000). Prevents DoS via oversized templates.
- **`max_output_length`**: Maximum rendered output length
  (default: 50,000). Prevents DoS via value expansion.
- **`reject_unknown_keys`**: Whether to raise on keys not in the
  allowlist (default: `True`). Ignored when `allowed_keys` is empty.
- **`log_rejected`**: Log rejected keys at WARNING level (default:
  `True`).

## Pre-validation

Use `validate_template()` at action/plan creation time to catch
issues early:

```python
from cleveragents.templates.secure_renderer import validate_template

errors = validate_template(
    template_text,
    allowed_keys=frozenset({"name", "description"}),
)
if errors:
    raise ValueError(f"Invalid template: {errors}")
```

## Convenience Function

For one-off renders without creating a renderer instance:

```python
from cleveragents.templates.secure_renderer import render_template_secure

result = render_template_secure(
    "Hello {name}",
    {"name": "World"},
    allowed_keys=frozenset({"name"}),
)
```

## Safe Substitution Behaviour

If a placeholder key is not found in the context, it is left as-is
in the output (safe-substitute behaviour):

```python
renderer.render("Hello {name}, {role}", {"name": "Alice"})
# => "Hello Alice, {role}"
```

## Legacy Compatibility

The existing `TemplateRenderer` in `cleveragents.templates.renderer`
now wraps `SecureTemplateRenderer` internally. It returns the raw
template on any rendering failure to preserve backward compatibility,
and logs a WARNING when doing so (enable DEBUG for the full template).

An optional `config` parameter allows callers to pass a
`TemplateConfig` through to the underlying secure renderer:

```python
from cleveragents.templates.renderer import TemplateRenderer
from cleveragents.templates.secure_renderer import TemplateConfig

renderer = TemplateRenderer(
    config=TemplateConfig(allowed_keys=frozenset({"name"})),
)
```

## Exception Hierarchy

```
TemplateError (base)
├── TemplateSecurityError   — unsafe constructs
├── TemplateSizeError       — length limits exceeded
└── TemplateValidationError — allowlist violations
```
