Files
cleveragents-core/docs/reference/template_security.md
T
HAL9000 18d00c04c4
CI / lint (pull_request) Failing after 1m15s
CI / quality (pull_request) Successful in 1m21s
CI / typecheck (pull_request) Successful in 1m34s
CI / security (pull_request) Successful in 1m37s
CI / coverage (pull_request) Has been skipped
CI / unit_tests (pull_request) Failing after 1m37s
CI / docker (pull_request) Has been skipped
CI / build (pull_request) Successful in 33s
CI / helm (pull_request) Successful in 26s
CI / push-validation (pull_request) Successful in 19s
CI / e2e_tests (pull_request) Successful in 3m20s
CI / integration_tests (pull_request) Successful in 4m32s
CI / status-check (pull_request) Failing after 3s
fix(skills): implement multi-scope agent skill discovery for global, project, and local tiers
Implements AgentSkillDiscovery class to support discovering Agent Skills from
multiple configured directories across three scopes (global, project, local).
Handles name collisions with precedence: local > project > global.

Adds comprehensive BDD test coverage for multi-scope discovery scenarios including:
- Global-only, project-only, and local-only discovery
- Combined discovery from all scopes
- Name collision resolution with proper precedence
- Non-existent and empty scope directory handling
- Multiple skills in same scope discovery

ISSUES CLOSED: #9369
2026-05-06 19:55:22 +00:00

4.3 KiB

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:

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:

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:

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):

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:

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