Skip to content

Commit 4ae60e8

Browse files
evanbijoy251claude
andcommitted
fix: synthesize messages for BEFORE_MODEL when messages is empty
Production WASM rules (e.g. block_ssn_in_prompts) check input.messages[].content (plain string) rather than the flat model_input field. The callback-handler path never passes messages, so context.messages is always [] and rules silently miss violations. Fix: context_to_input now builds a synthetic [{"role": "user", "content": model_input}] entry when messages is empty for BEFORE_MODEL. Verified against the production bundle — the SSN rule now fires: fired_deny=["Message contains a value matching the SSN pattern (###-##-####)"], deny=true. Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 5145646 commit 4ae60e8

2 files changed

Lines changed: 50 additions & 1 deletion

File tree

src/uipath/runtime/governance/rego/evaluator.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,14 @@ def context_to_input(
5151
agent_input = context.agent_input
5252
if context.hook == LifecycleHook.BEFORE_MODEL and not agent_input:
5353
agent_input = context.model_input
54+
# WASM rules check input.messages[].content (plain string) rather than
55+
# the flat model_input field. When messages is empty for a BEFORE_MODEL
56+
# hook, synthesize a single user message from model_input so that rules
57+
# like block_ssn_in_prompts can match. The callback-handler path never
58+
# passes messages, so this is the only way to populate the field.
59+
messages = context.messages
60+
if context.hook == LifecycleHook.BEFORE_MODEL and not messages and context.model_input:
61+
messages = [{"role": "user", "content": context.model_input}]
5462
return {
5563
"hook": context.hook.value,
5664
"agent_input": agent_input,
@@ -67,7 +75,7 @@ def context_to_input(
6775
"llm_calls": session.get("llm_calls", 0),
6876
},
6977
"ring": context.ring,
70-
"messages": context.messages,
78+
"messages": messages,
7179
"features": features,
7280
}
7381

tests/test_rego_evaluator.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,47 @@ def test_context_to_input_other_hooks_no_normalization() -> None:
154154
assert result["agent_input"] == ""
155155

156156

157+
def test_context_to_input_before_model_synthesizes_messages() -> None:
158+
# When messages is empty for BEFORE_MODEL, a synthetic user message is
159+
# built from model_input so WASM rules checking input.messages fire.
160+
ctx = CheckContext(
161+
hook=LifecycleHook.BEFORE_MODEL,
162+
agent_name="a",
163+
runtime_id="r",
164+
model_input="identify 999-99-9999",
165+
messages=[],
166+
)
167+
result = context_to_input(ctx)
168+
assert result["messages"] == [{"role": "user", "content": "identify 999-99-9999"}]
169+
170+
171+
def test_context_to_input_before_model_keeps_explicit_messages() -> None:
172+
# When messages is already populated, it is NOT overwritten.
173+
existing = [{"role": "user", "content": "original"}]
174+
ctx = CheckContext(
175+
hook=LifecycleHook.BEFORE_MODEL,
176+
agent_name="a",
177+
runtime_id="r",
178+
model_input="other text",
179+
messages=existing,
180+
)
181+
result = context_to_input(ctx)
182+
assert result["messages"] == existing
183+
184+
185+
def test_context_to_input_other_hooks_no_messages_synthesis() -> None:
186+
# Messages synthesis must not apply to other hooks.
187+
ctx = CheckContext(
188+
hook=LifecycleHook.BEFORE_AGENT,
189+
agent_name="a",
190+
runtime_id="r",
191+
model_input="should not synthesize",
192+
messages=[],
193+
)
194+
result = context_to_input(ctx)
195+
assert result["messages"] == []
196+
197+
157198
# ---------------------------------------------------------------------------
158199
# _extract_wasm_from_bundle / _extract_data_json_from_bundle
159200
# ---------------------------------------------------------------------------

0 commit comments

Comments
 (0)