prompt_renderer

Jinja2-based system prompt renderer with SSTI hardening.

Loads a .j2 template file once at startup and renders it on each call with room-specific and tool-specific context variables. Uses a SandboxedEnvironment and recursively sanitises user-controllable values to prevent server-side template injection.

class prompt_renderer.LoggingSandboxedEnvironment(*args, **kwargs)[source]

Bases: SandboxedEnvironment

Sandboxed Jinja2 environment that audits its creation and blocked attribute access.

Thin subclass of jinja2.sandbox.SandboxedEnvironment that adds observability to prompt compilation: it announces every environment it constructs (tagged with the template name) and turns each silent sandbox refusal into a logged warning, so that a server-side template-injection probe leaves an auditable trail instead of failing quietly. The security policy itself is unchanged – only the logging is added on top.

Instantiated by PromptRenderer.__init__ while loading the system-prompt template, by kg_agentic_extraction.py for the KG extraction prompt, and by knowledge_anchoring/template_loader.py for its shared sandbox; the SSTI test suite (tests/test_jinja_sandbox_ssti) also exercises it directly.

__init__(*args, **kwargs)[source]

Build a logging sandboxed Jinja2 environment and announce it.

Pops the bespoke template_name keyword (used only for log attribution) out of kwargs before delegating the rest of the positional and keyword arguments to jinja2.sandbox.SandboxedEnvironment.__init__(), so the parent never sees an unexpected argument. After the base environment is constructed it emits an info log line through the module logger recording which template this environment was created for, giving operators a breadcrumb whenever a new prompt-compilation sandbox is spun up.

This constructor is invoked indirectly by PromptRenderer.__init__, which instantiates a LoggingSandboxedEnvironment while loading the system-prompt template; no other internal caller constructs it directly.

Parameters:
  • *args – Positional arguments forwarded verbatim to the SandboxedEnvironment base class.

  • **kwargs – Keyword arguments forwarded to the base class, except for the consumed template_name entry which defaults to "unknown" and is used purely for the log message.

Return type:

None

is_safe_attribute(obj, attr, value)[source]

Decide whether template code may read an attribute, logging denials.

Delegates the actual security judgement to jinja2.sandbox.SandboxedEnvironment.is_safe_attribute(), which applies Jinja2’s sandbox policy (blocking dunder access and other attributes flagged unsafe). When the base class refuses access, this override emits a warning through the module logger naming the attribute and the object type involved, turning otherwise-silent sandbox refusals into an auditable signal of a possible server-side template-injection attempt. The boolean verdict is then returned unchanged so the sandbox still enforces the same restriction.

Jinja2’s sandbox machinery calls this automatically during template rendering for every attribute access; there is no direct internal caller, though the sibling sandbox in knowledge_anchoring/template_loader.py mirrors this pattern.

Parameters:
  • obj – The object whose attribute is being accessed inside a template.

  • attr – The attribute name the template is attempting to read.

  • value – The resolved attribute value supplied by Jinja2.

Returns:

True if the attribute access is permitted, False if the sandbox blocks it.

Return type:

bool

prompt_renderer.sanitize_context(value)[source]

Recursively strip Jinja2 metacharacters from user-controllable strings.

Replaces {{, }}, {%, %}, {#, #} with Unicode full-width lookalikes so they cannot be interpreted as template syntax if an | tojson filter is ever omitted.

Non-string leaves (ints, floats, bools, None) pass through unchanged. Dicts and lists are walked recursively.

Return type:

Any

Parameters:

value (Any)

class prompt_renderer.PromptRenderer(template_path, default_extras=None)[source]

Bases: object

Render a Jinja2 system-prompt template with per-request context.

Uses SandboxedEnvironment to prevent template injection even if a caller accidentally passes unsanitised user data.

Parameters:
  • template_path (str | Path) – Path to the .j2 template file (e.g. "system_prompt.j2").

  • default_extras (dict[str, Any] | None) – Optional dict of variables injected into every render call (e.g. the list of registered tools). Per-call context takes precedence over these defaults.

__init__(template_path, default_extras=None)[source]

Load and compile the system-prompt template into a sandboxed renderer.

Resolves and validates the template path, then builds a LoggingSandboxedEnvironment (with trailing-newline preservation and block trimming/stripping enabled) backed by a FileSystemLoader rooted at the template’s parent directory, compiles the named template once, and stashes the default_extras injected into every later render. The compilation happens here so per-request renders stay cheap. Reads the filesystem to confirm the template exists and emits an info log line through the module logger once it is loaded.

Constructed by callers across the codebase – the inference worker (inference_main.py), message-context injection (message_processor/context_injections.py), subagent tooling (tools/subagent_tools.py), the SWORD overlay test suites, and the SSTI tests – typically once at startup with system_prompt.j2.

Parameters:
  • template_path (str | Path) – Path to the .j2 template file (e.g. "system_prompt.j2"); accepts a string or Path.

  • default_extras (dict[str, Any] | None) – Optional variables injected into every render() call (e.g. the registered-tool list); per-call context overrides these. Defaults to an empty dict.

Raises:

FileNotFoundError – If no file exists at template_path.

Return type:

None

render(context=None)[source]

Render the template with the supplied context.

All values in context are recursively sanitised to strip Jinja2 metacharacters before rendering.

The following keys are automatically injected if not already present:

  • current_date – today’s date in YYYY-MM-DD format (UTC).

Keys from default_extras (set at init or later) are included but can be overridden by context.

Return type:

str

Parameters:

context (dict[str, Any] | None)