Skip to content
Merged
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
80 changes: 80 additions & 0 deletions packages/agent-os/src/agent_os/integrations/crewai_adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,22 @@ def _wrap_tool(self, tool, agent_name: str):
crew_name = self._crew_name

def governed_run(*args, **kwargs):
"""Governed wrapper around a CrewAI tool's run method.

Intercepts the tool call, runs pre-execution policy checks,
records the invocation in the audit log, and delegates
to the original _run implementation.

Args:
*args: Positional arguments forwarded to the original tool.
**kwargs: Keyword arguments forwarded to the original tool.

Returns:
The result from the original tool's run method.

Raises:
PolicyViolationError: If the tool call violates the active policy.
"""
request = ToolCallRequest(
tool_name=tool_name,
arguments=kwargs if kwargs else {"args": args},
Expand Down Expand Up @@ -194,6 +210,22 @@ def _wrap_agent(self, agent):
crew_name = self._crew_name

def governed_execute(task, *args, **kwargs):
"""Governed wrapper around a CrewAI agent's task execution.

Intercepts each task execution call, applies pre-execution
policy checks, and delegates to the original execute method.

Args:
task: The CrewAI Task object to execute.
*args: Additional positional arguments.
**kwargs: Additional keyword arguments.

Returns:
The task execution result from the underlying agent.

Raises:
PolicyViolationError: If the execution violates the active policy.
"""
task_id = getattr(task, 'id', None) or str(id(task))
logger.info("Agent task execution started: crew_name=%s, task_id=%s", crew_name, task_id)
if self._kernel.policy.require_human_approval:
Expand Down Expand Up @@ -253,6 +285,22 @@ def _intercept_task_steps(

@functools.wraps(original_step)
def governed_step(*args: Any, _orig=original_step, _attr=step_attr, **kwargs: Any) -> Any:
"""Governed wrapper around a CrewAI task step.

Intercepts individual step calls within a task, validates
inputs against the active policy, and records each step
in the audit trail before delegating to the original method.

Args:
*args: Positional arguments forwarded to the original step.
**kwargs: Keyword arguments forwarded to the original step.

Returns:
The result from the original step method.

Raises:
PolicyViolationError: If the step input violates the active policy.
"""
step_record = {
"crew": crew_name,
"agent": agent_name,
Expand Down Expand Up @@ -306,6 +354,22 @@ def _intercept_crew_memory(

@functools.wraps(save_fn)
def governed_save(*args: Any, _orig=save_fn, _mname=save_method_name, **kwargs: Any) -> Any:
"""Governed wrapper around CrewAI memory save operations.

Validates content before it is written to crew memory,
checking for PII patterns and policy-blocked content.
Records every save attempt in the memory audit log.

Args:
*args: Positional arguments forwarded to the original save.
**kwargs: Keyword arguments forwarded to the original save.

Returns:
The result from the original memory save method.

Raises:
PolicyViolationError: If the content contains PII or blocked patterns.
"""
combined = str(args) + str(kwargs)

# PII / secrets check
Expand Down Expand Up @@ -357,6 +421,22 @@ def _detect_crew_delegation(

@functools.wraps(delegate_fn)
def governed_delegate(*args: Any, **kwargs: Any) -> Any:
"""Governed wrapper around CrewAI agent delegation.

Intercepts delegation calls between agents, tracks delegation
depth, and enforces the maximum delegation limit defined in
the active policy.

Args:
*args: Positional arguments forwarded to the original delegate.
**kwargs: Keyword arguments forwarded to the original delegate.

Returns:
The result from the delegated agent.

Raises:
PolicyViolationError: If the delegation depth exceeds the policy limit.
"""
depth = len(kernel._delegation_log) + 1
if depth > max_depth:
raise PolicyViolationError(
Expand Down
57 changes: 57 additions & 0 deletions packages/agent-os/src/agent_os/integrations/google_adk_adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -233,9 +233,26 @@ def unwrap(self, governed_agent: Any) -> Any:
# ------------------------------------------------------------------

def _default_violation_handler(self, error: PolicyViolationError) -> None:
"""Default handler called when a policy violation occurs.

Logs the violation at ERROR level. Override by passing a custom
on_violation callable to the kernel constructor.

Args:
error: The PolicyViolationError that was raised.
"""
logger.error(f"Policy violation: {error}")

def _record(self, event_type: str, agent_name: str, details: dict[str, Any]) -> None:
"""Append an audit event to the internal audit log.

Records the event only when log_all_calls is enabled.

Args:
event_type: Short string label for the event.
agent_name: Name of the ADK agent generating the event.
details: Arbitrary dict of additional context.
"""
if self._adk_config.log_all_calls:
self._audit_log.append(
AuditEvent(
Expand All @@ -247,26 +264,55 @@ def _record(self, event_type: str, agent_name: str, details: dict[str, Any]) ->
)

def _check_tool_allowed(self, tool_name: str) -> tuple[bool, str]:
"""Check whether a tool is permitted by the active ADK policy.

Args:
tool_name: Name of the ADK tool to check.

Returns:
Tuple of (allowed: bool, reason: str).
"""
if tool_name in self._adk_config.blocked_tools:
return False, f"Tool '{tool_name}' is blocked by policy"
if self._adk_config.allowed_tools and tool_name not in self._adk_config.allowed_tools:
return False, f"Tool '{tool_name}' not in allowed list"
return True, ""

def _check_content(self, content: str) -> tuple[bool, str]:
"""Scan a string for policy-blocked patterns.

Args:
content: The text to scan.

Returns:
Tuple of (allowed: bool, reason: str).
"""
content_lower = content.lower()
for pattern in self._adk_config.blocked_patterns:
if pattern.lower() in content_lower:
return False, f"Content matches blocked pattern: '{pattern}'"
return True, ""

def _check_timeout(self) -> tuple[bool, str]:
"""Check whether the kernel has exceeded its configured timeout.

Returns:
Tuple of (within_limit: bool, reason: str).
"""
elapsed = time.time() - self._start_time
if elapsed > self._adk_config.timeout_seconds:
return False, f"Execution timeout ({elapsed:.0f}s > {self._adk_config.timeout_seconds}s)"
return True, ""

def _check_budget(self, cost: float = 1.0) -> tuple[bool, str]:
"""Check whether a tool call would exceed the configured cost budget.

Args:
cost: Cost units to add for this call (default 1.0).

Returns:
Tuple of (within_budget: bool, reason: str).
"""
if self._adk_config.max_budget is not None:
if self._budget_spent + cost > self._adk_config.max_budget:
return False, (
Expand All @@ -286,6 +332,17 @@ def _needs_approval(self, tool_name: str) -> bool:
return True

def _raise_violation(self, policy_name: str, description: str) -> PolicyViolationError:
"""Create, record, and surface a PolicyViolationError.

Appends the error to the violations list and calls on_violation.

Args:
policy_name: Short identifier for the violated policy rule.
description: Human-readable description of the violation.

Returns:
The constructed PolicyViolationError (caller may raise it).
"""
error = PolicyViolationError(policy_name, description)
self._violations.append(error)
self.on_violation(error)
Expand Down
66 changes: 66 additions & 0 deletions packages/agent-os/src/agent_os/integrations/guardrails_adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,12 @@ class ValidationOutcome:
metadata: dict[str, Any] = field(default_factory=dict)

def to_dict(self) -> dict[str, Any]:
"""Serialise this outcome to a plain dictionary.

Returns:
A dict with validator, passed, and optionally error
and fixed_value keys.
"""
d: dict[str, Any] = {
"validator": self.validator_name,
"passed": self.passed,
Expand All @@ -101,9 +107,19 @@ class ValidationResult:

@property
def failed_validators(self) -> list[str]:
"""Return the names of all validators that did not pass.

Returns:
List of validator name strings where passed is False.
"""
return [o.validator_name for o in self.outcomes if not o.passed]

def to_dict(self) -> dict[str, Any]:
"""Serialise this aggregated result to a plain dictionary.

Returns:
A dict with passed, action, outcomes, and failed_validators keys.
"""
return {
"passed": self.passed,
"action": self.action_taken.value,
Expand All @@ -128,9 +144,23 @@ def __init__(self, patterns: list[str], validator_name: str = "regex"):

@property
def name(self) -> str:
"""Return the human-readable name of this regex validator.

Returns:
The validator name string used in audit logs and outcomes.
"""
return self._name

def validate(self, value: str, metadata: dict[str, Any] | None = None) -> ValidationOutcome:
"""Validate a string by checking it against blocked regex patterns.

Args:
value: The text to scan.
metadata: Optional dict of additional context (unused).

Returns:
ValidationOutcome indicating pass or fail.
"""

for pattern in self._patterns:
match = pattern.search(value)
Expand All @@ -152,9 +182,23 @@ def __init__(self, max_length: int = 10000, validator_name: str = "length"):

@property
def name(self) -> str:
"""Return the human-readable name of this length validator.

Returns:
The validator name string used in audit logs and outcomes.
"""
return self._name

def validate(self, value: str, metadata: dict[str, Any] | None = None) -> ValidationOutcome:
"""Validate that a string does not exceed the configured max length.

Args:
value: The text to check.
metadata: Optional dict of additional context (unused).

Returns:
ValidationOutcome with a fixed_value truncated to max_length on fail.
"""
if len(value) > self._max_length:
return ValidationOutcome(
validator_name=self._name,
Expand All @@ -174,9 +218,23 @@ def __init__(self, blocked_keywords: list[str], validator_name: str = "keywords"

@property
def name(self) -> str:
"""Return the human-readable name of this keyword validator.

Returns:
The validator name string used in audit logs and outcomes.
"""
return self._name

def validate(self, value: str, metadata: dict[str, Any] | None = None) -> ValidationOutcome:
"""Validate that a string contains none of the blocked keywords.

Args:
value: The text to scan (case-insensitive).
metadata: Optional dict of additional context (unused).

Returns:
ValidationOutcome indicating pass or fail.
"""
value_lower = value.lower()
for kw in self._keywords:
if kw in value_lower:
Expand Down Expand Up @@ -213,6 +271,14 @@ def __init__(
self._history: list[ValidationResult] = []

def _default_violation_handler(self, result: ValidationResult) -> None:
"""Default handler called when one or more validators fail.

Logs a warning for each failed validator name. Override by
passing a custom on_violation callable to GuardrailsKernel.

Args:
result: The aggregated ValidationResult.
"""
for name in result.failed_validators:
logger.warning(f"Guardrail violation: {name}")

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -237,10 +237,12 @@ def run_sync(self_inner, prompt: str, **kwargs) -> Any:

@property
def original(self_inner) -> Any:
"""Return the original unwrapped agent before governance wrapping."""
return self_inner._original

@property
def context(self_inner) -> ExecutionContext:
"""Return the ExecutionContext tracking call counts and session state."""
return self_inner._ctx

def __getattr__(self_inner, name: str) -> Any:
Expand Down Expand Up @@ -364,6 +366,7 @@ def _wrap_single_tool(

@wraps(original_fn)
def governed_fn(*args: Any, **kwargs: Any) -> Any:
"""Governed wrapper that validates and delegates PydanticAI tool calls."""
# Build arguments dict for policy check
call_args: dict[str, Any] = kwargs.copy()
if args:
Expand Down
Loading
Loading