Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ dependencies = [
"psycopg[binary]>=3.2",
"cuga-oak-health; python_version>='3.12'",
"aiosmtpd",
"toolguard>=0.2.17",
]

[project.optional-dependencies]
Expand Down
6 changes: 6 additions & 0 deletions src/cuga/backend/cuga_graph/policy/filesystem_sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,12 @@ def _policy_to_markdown(self, policy: Policy) -> str:
if policy.target_apps:
frontmatter['target_apps'] = policy.target_apps
frontmatter['prepend'] = policy.prepend
if policy.tool_guards:
# Convert ToolGuard objects to dict for YAML serialization
frontmatter['tool_guards'] = {
tool_name: guard.model_dump()
for tool_name, guard in policy.tool_guards.items()
}
content = policy.guide_content or ""
elif isinstance(policy, IntentGuard):
if policy.response:
Expand Down
11 changes: 11 additions & 0 deletions src/cuga/backend/cuga_graph/policy/folder_loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,16 @@ def create_tool_guide_from_markdown(
if not triggers:
triggers = [AlwaysTrigger()]

# Parse tool_guards if present
tool_guards = None
tool_guards_data = frontmatter.get('tool_guards')
if tool_guards_data and isinstance(tool_guards_data, dict):
from cuga.backend.cuga_graph.policy.models import ToolGuard
tool_guards = {
tool_name: ToolGuard(**guard_data)
for tool_name, guard_data in tool_guards_data.items()
}
Comment on lines +215 to +221

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Fail fast on malformed tool_guards frontmatter.

At Line 216, a non-dict tool_guards value is silently ignored, which can accidentally disable intended guard enforcement. Reject invalid shapes explicitly instead of falling back to None.

Proposed fix
-    tool_guards = None
-    tool_guards_data = frontmatter.get('tool_guards')
-    if tool_guards_data and isinstance(tool_guards_data, dict):
-        from cuga.backend.cuga_graph.policy.models import ToolGuard
-        tool_guards = {
-            tool_name: ToolGuard(**guard_data)
-            for tool_name, guard_data in tool_guards_data.items()
-        }
+    tool_guards = None
+    if 'tool_guards' in frontmatter:
+        tool_guards_data = frontmatter.get('tool_guards')
+        if not isinstance(tool_guards_data, dict):
+            raise ValueError(f"ToolGuide {name} has invalid 'tool_guards' (must be a dictionary)")
+
+        from cuga.backend.cuga_graph.policy.models import ToolGuard
+        tool_guards = {}
+        for tool_name, guard_data in tool_guards_data.items():
+            if not isinstance(guard_data, dict):
+                raise ValueError(
+                    f"ToolGuide {name} has invalid guard config for '{tool_name}' (must be a dictionary)"
+                )
+            tool_guards[tool_name] = ToolGuard(**guard_data)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/cuga/backend/cuga_graph/policy/folder_loader.py` around lines 215 - 221,
The code silently ignores non-dict frontmatter for tool_guards; update the block
that reads frontmatter.get('tool_guards') to validate the shape and fail fast:
if tool_guards_data is None do nothing, if it's a dict continue to construct
tool_guards via ToolGuard(**guard_data) as now, but if tool_guards_data exists
and is not a dict raise a clear exception (e.g., ValueError) with a message
referencing 'tool_guards' and the offending type so malformed frontmatter is
rejected rather than silently ignored.


return ToolGuide(
id=frontmatter.get('id', f"tool_guide_{Path(file_path).stem}"),
name=name,
Expand All @@ -219,6 +229,7 @@ def create_tool_guide_from_markdown(
target_apps=frontmatter.get('target_apps'),
guide_content=content,
prepend=frontmatter.get('prepend', False),
tool_guards=tool_guards,
priority=frontmatter.get('priority', 50),
enabled=frontmatter.get('enabled', True),
)
Expand Down
23 changes: 23 additions & 0 deletions src/cuga/backend/cuga_graph/policy/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,26 @@ def validate_trigger_targets(self):
return self


class ToolGuard(BaseModel):
"""Guard configuration for a specific tool with compliance rules."""

violating_examples: List[str] = Field(
default_factory=list, description="Examples of violating usage patterns"
)
compliance_examples: List[str] = Field(
default_factory=list, description="Examples of compliant usage patterns"
)
policy_code: str = Field(
default="",
description=(
"Python code that validates tool usage compliance. "
"This code is executed in a sandboxed environment using the toolguard library. "
"Only trusted administrators with manage access should be allowed to modify policy code. "
"While sandboxed, policy code should still be reviewed for correctness and performance."
)
)


class ToolGuide(BaseModel):
"""Policy that enriches tool descriptions with additional markdown content."""

Expand All @@ -215,6 +235,9 @@ class ToolGuide(BaseModel):
)
guide_content: str = Field(..., description="Markdown content to append to tool descriptions")
prepend: bool = Field(False, description="Whether to prepend content instead of appending")
tool_guards: Optional[Dict[str, ToolGuard]] = Field(
default=None, description="Optional guard configurations per tool (key: tool_name, value: ToolGuard)"
)
metadata: Dict[str, Any] = Field(default_factory=dict, description="Additional metadata")
priority: int = Field(0, description="Priority when multiple guides match (higher = more important)")
enabled: bool = Field(True, description="Whether this guide is active")
Expand Down
8 changes: 8 additions & 0 deletions src/cuga/backend/cuga_graph/policy/storage.py
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,14 @@ async def _generate_policy_embedding(self, policy: Policy) -> List[float]:
text_parts.append(policy.guide_content[:300])
if policy.target_tools and "*" not in policy.target_tools:
text_parts.append(f"Tools: {', '.join(policy.target_tools[:10])}")
if policy.tool_guards:
# Add tool guard information to search text
for tool_name, guard in policy.tool_guards.items():
text_parts.append(f"Guard for {tool_name}")
if guard.violating_examples:
text_parts.append(f"Violations: {' '.join(guard.violating_examples[:3])}")
if guard.compliance_examples:
text_parts.append(f"Compliance: {' '.join(guard.compliance_examples[:3])}")
Comment on lines +185 to +187

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Cap tool-guard example text before embedding.

Lines 185-187 concatenate raw examples with no length bound. Large examples can blow embedding input limits and fail policy writes.

Proposed fix
                 if policy.tool_guards:
                     # Add tool guard information to search text
+                    max_example_chars = 200
                     for tool_name, guard in policy.tool_guards.items():
                         text_parts.append(f"Guard for {tool_name}")
                         if guard.violating_examples:
-                            text_parts.append(f"Violations: {' '.join(guard.violating_examples[:3])}")
+                            violations = " ".join(
+                                ex[:max_example_chars] for ex in guard.violating_examples[:3]
+                            )
+                            text_parts.append(f"Violations: {violations}")
                         if guard.compliance_examples:
-                            text_parts.append(f"Compliance: {' '.join(guard.compliance_examples[:3])}")
+                            compliance = " ".join(
+                                ex[:max_example_chars] for ex in guard.compliance_examples[:3]
+                            )
+                            text_parts.append(f"Compliance: {compliance}")
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/cuga/backend/cuga_graph/policy/storage.py` around lines 185 - 187, The
current code appends raw guard.violating_examples and guard.compliance_examples
into text_parts with no length limits, which can exceed embedding input size;
before joining each example list (used where text_parts is built), truncate each
example string to a safe max length (e.g., 1000 chars) and/or limit the number
of characters per joined block, and then append the truncated/joined strings
instead of the raw ones; implement this by adding a small helper (e.g.,
truncate(text, max_chars)) or inline slicing when building the strings that
reference text_parts, guard.violating_examples, and guard.compliance_examples so
large examples are capped prior to embedding/storage.


elif isinstance(policy, OutputFormatter):
# OutputFormatter-specific content
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
"""Tests for tool guard policies."""


Loading