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

- 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:
Agent Zero
2026-08-17 16:57:50 +02:00
parent dbeadd8ab1
commit 638e3f3e1e
11 changed files with 1455 additions and 1 deletions
@@ -11,6 +11,7 @@ from datetime import UTC
from typing import Any
from fastapi import APIRouter, Depends, HTTPException, Query
from pydantic import BaseModel
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.db import get_db
@@ -258,6 +259,93 @@ async def update_agent(
return _agent_to_response(agent)
# ─── AI Use-Case Metadata ───
class AIUseCaseMetadataUpdate(BaseModel):
"""Update AI use-case metadata for an agent."""
intended_purpose: str | None = None
owner: str | None = None
data_categories: list[str] | None = None
allowed_providers: list[str] | None = None
allowed_models: list[str] | None = None
allowed_actions: list[str] | None = None
oversight_policy: str | None = None
risk_class: str | None = None
human_review_required: bool | None = None
@router.get(
"/{agent_id}/ai-use-case",
dependencies=[Depends(require_permission("agents:read"))],
)
async def get_ai_use_case(
agent_id: str,
current_user: dict[str, Any] = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
"""Get AI use-case metadata for an agent."""
tenant_id = uuid.UUID(current_user["tenant_id"])
try:
aid = uuid.UUID(agent_id)
except (ValueError, TypeError):
raise HTTPException(status_code=400, detail="Invalid agent ID") from None
agent = await AgentService.get_by_id(db, tenant_id, aid)
if agent is None:
raise HTTPException(status_code=404, detail="Agent not found")
return {"agent_id": agent_id, "ai_use_case_metadata": agent.ai_use_case_metadata or {}}
@router.patch(
"/{agent_id}/ai-use-case",
dependencies=[Depends(require_permission("agents:write"))],
)
async def update_ai_use_case(
agent_id: str,
data: AIUseCaseMetadataUpdate,
current_user: dict[str, Any] = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
"""Update AI use-case metadata for an agent."""
tenant_id = uuid.UUID(current_user["tenant_id"])
user_id = uuid.UUID(current_user["user_id"])
try:
aid = uuid.UUID(agent_id)
except (ValueError, TypeError):
raise HTTPException(status_code=400, detail="Invalid agent ID") from None
agent = await AgentService.get_by_id(db, tenant_id, aid)
if agent is None:
raise HTTPException(status_code=404, detail="Agent not found")
# Merge with existing metadata (partial update).
current = dict(agent.ai_use_case_metadata or {})
updates = data.model_dump(exclude_none=True)
current.update(updates)
# Validate the merged metadata against the agent config.
from app.ai.ai_use_case import AIUseCaseMetadata, validate_ai_use_case
try:
metadata = AIUseCaseMetadata(**current)
except Exception as e:
raise HTTPException(status_code=400, detail=f"Invalid AI use-case metadata: {e}") from None
warnings = validate_ai_use_case(metadata, agent)
updated = await AgentService.update(
db, tenant_id, aid, {"ai_use_case_metadata": current}, user_id=user_id
)
if updated is None:
raise HTTPException(status_code=404, detail="Agent not found")
return {
"agent_id": agent_id,
"ai_use_case_metadata": current,
"warnings": warnings,
}
@router.delete(
"/{agent_id}",
dependencies=[Depends(require_permission("agents:delete"))],