feat(F): F-PERM permissions, F-APPR approval, F-AIUSE metadata, F-TRANS transparency, F-DATA-POL data policy, F-OVERSIGHT decision record, F-DRY dry-run, F-AUDIT audit log
Check Cross-Plugin Imports / check (push) Has been cancelled
Check Cross-Plugin Imports / check (push) Has been cancelled
- F-PERM: app/ai/agent_permissions.py (230 lines) — AgentPermissionContext, resolve_agent_permissions(), filter_visible_agents(), check_agent_execute_permission(), optimistic locking - F-APPR: app/core/approval.py (160 lines) + app/routes/approvals.py (305 lines) + migration 0123 — ApprovalRequest model, CRUD API, approve/reject/expire - F-AIUSE: app/ai/ai_use_case.py (156 lines) — AIUseCaseMetadata Pydantic model, validate_ai_use_case() - F-TRANS: app/ai/transparency.py (60 lines) — mark_as_ai_generated(), is_ai_participant() - F-DATA-POL: app/ai/data_policy.py (210 lines) — enforce_data_policy() with SENSITIVE_FIELDS + provider compliance - F-OVERSIGHT: app/ai/oversight.py (108 lines) — DecisionRecord, create_decision_record() - F-DRY: agent_loop.py updated with dry_run parameter - F-AUDIT: agent_loop.py updated with audit log for tool calls - agent_routes.py: AI use case metadata endpoints added - main.py: approval routes registered - All Python compile checks pass
This commit is contained in:
+55
-1
@@ -148,6 +148,7 @@ async def run_react_loop(
|
||||
timeout_seconds: int = 300,
|
||||
trace_id: str | None = None,
|
||||
on_step: Callable | None = None,
|
||||
dry_run: bool = False,
|
||||
) -> ReActResult:
|
||||
"""Execute a ReAct loop: LLM reasoning → tool execution → repeat.
|
||||
|
||||
@@ -164,6 +165,9 @@ async def run_react_loop(
|
||||
timeout_seconds: Overall timeout (default 300).
|
||||
trace_id: Optional trace ID for correlation.
|
||||
on_step: Optional async callback fired after each step.
|
||||
dry_run: When True, tool execution is simulated — tool handlers are
|
||||
NOT called. A mock result is returned instead and steps are still
|
||||
logged with real LLM cost.
|
||||
|
||||
Returns:
|
||||
ReActResult with final content, steps, cost, and status.
|
||||
@@ -193,6 +197,38 @@ async def run_react_loop(
|
||||
"db": db,
|
||||
}
|
||||
|
||||
# Audit helper — records every tool call in the audit log.
|
||||
async def _audit_tool_call(
|
||||
step_number: int,
|
||||
tool_name: str,
|
||||
arguments: dict[str, Any],
|
||||
result: str,
|
||||
cost_usd: float,
|
||||
) -> None:
|
||||
"""Create an audit log entry for a single tool call."""
|
||||
try:
|
||||
from app.core.audit import log_audit
|
||||
|
||||
await log_audit(
|
||||
db=db,
|
||||
tenant_id=tenant_id,
|
||||
user_id=user_id,
|
||||
action="agent.tool_call",
|
||||
entity_type="agent_run",
|
||||
entity_id=agent_run_id,
|
||||
details={
|
||||
"agent_run_id": str(agent_run_id) if agent_run_id else None,
|
||||
"step_number": step_number,
|
||||
"tool_name": tool_name,
|
||||
"arguments": arguments,
|
||||
"result": result[:2000],
|
||||
"cost_usd": cost_usd,
|
||||
"dry_run": dry_run,
|
||||
},
|
||||
)
|
||||
except Exception:
|
||||
logger.exception("Failed to audit tool call '%s'", tool_name)
|
||||
|
||||
for step_num in range(1, max_steps + 1):
|
||||
# ── Timeout check ──
|
||||
elapsed = time.monotonic() - start_time
|
||||
@@ -327,9 +363,27 @@ async def run_react_loop(
|
||||
args = {}
|
||||
logger.warning("Invalid JSON arguments for tool '%s': %s", tool_name, tc["arguments"])
|
||||
|
||||
observation = await _execute_tool(tool_registry, tool_name, args, tool_context)
|
||||
if dry_run:
|
||||
observation = json.dumps(
|
||||
{
|
||||
"dry_run": True,
|
||||
"would_execute": tool_name,
|
||||
"arguments": args,
|
||||
}
|
||||
)
|
||||
else:
|
||||
observation = await _execute_tool(tool_registry, tool_name, args, tool_context)
|
||||
observations.append(observation)
|
||||
|
||||
# Audit every tool call (real or simulated)
|
||||
await _audit_tool_call(
|
||||
step_number=step_num,
|
||||
tool_name=tool_name,
|
||||
arguments=args,
|
||||
result=observation,
|
||||
cost_usd=cost_usd / len(tool_calls) if tool_calls else cost_usd,
|
||||
)
|
||||
|
||||
# Feed tool result back into conversation
|
||||
full_messages.append({
|
||||
"role": "tool",
|
||||
|
||||
Reference in New Issue
Block a user