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
+108
View File
@@ -0,0 +1,108 @@
"""Human oversight and decision records for AI agents.
Stores a durable audit trail of AI recommendations and the human decisions
made on them. A ``DecisionRecord`` captures the recommendation, the evidence
that supported it, and the reviewer's decision (approved / rejected) with an
explanation when the decision deviates from the recommendation.
Used by:
- ``app/ai/agent_loop.py`` — recording recommendations that need review
- ``app/plugins/builtins/automation`` — agent run oversight
"""
from __future__ import annotations
import uuid
from dataclasses import dataclass, field
from datetime import UTC, datetime
from typing import Any
from sqlalchemy import String, Text
from sqlalchemy.dialects.postgresql import JSONB
from sqlalchemy.dialects.postgresql import UUID as PGUUID
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import Mapped, mapped_column
from app.core.db import Base, TenantMixin
from app.models.owned_mixin import OwnedMixin
@dataclass
class DecisionRecord:
"""A recommendation and its human decision, for the audit trail.
Attributes:
agent_run_id: The agent run this decision belongs to.
recommendation: The AI's recommendation text.
evidence: Supporting data for the recommendation.
reviewer_id: The human reviewer (None while pending).
decision: ``approved``, ``rejected``, or ``None`` (pending).
decision_timestamp: ISO timestamp of the decision (None while pending).
deviation_note: Explanation when the decision differs from the
recommendation.
"""
agent_run_id: uuid.UUID
recommendation: str
evidence: dict[str, Any] = field(default_factory=dict)
reviewer_id: uuid.UUID | None = None
decision: str | None = None # "approved", "rejected", None (pending)
decision_timestamp: str | None = None
deviation_note: str | None = None
class DecisionRecordDB(Base, TenantMixin, OwnedMixin):
"""Persistent storage for AI decision records (audit trail)."""
__tablename__ = "ai_decision_records"
id: Mapped[uuid.UUID] = mapped_column(
PGUUID(as_uuid=True), primary_key=True, default=uuid.uuid4
)
agent_run_id: Mapped[uuid.UUID] = mapped_column(
PGUUID(as_uuid=True), nullable=False, index=True
)
recommendation: Mapped[str] = mapped_column(Text, nullable=False)
evidence: Mapped[dict[str, Any]] = mapped_column(
JSONB, nullable=False, default=dict
)
reviewer_id: Mapped[uuid.UUID | None] = mapped_column(
PGUUID(as_uuid=True), nullable=True
)
decision: Mapped[str | None] = mapped_column(String(20), nullable=True)
decision_timestamp: Mapped[str | None] = mapped_column(
String(40), nullable=True
)
deviation_note: Mapped[str | None] = mapped_column(Text, nullable=True)
async def create_decision_record(
db: AsyncSession,
tenant_id: uuid.UUID,
record: DecisionRecord,
) -> uuid.UUID:
"""Store a decision record for the audit trail.
Args:
db: Async DB session.
tenant_id: Tenant ID.
record: The decision record to persist.
Returns:
The UUID of the created record.
"""
entry = DecisionRecordDB(
tenant_id=tenant_id,
agent_run_id=record.agent_run_id,
recommendation=record.recommendation,
evidence=record.evidence or {},
reviewer_id=record.reviewer_id,
decision=record.decision,
decision_timestamp=record.decision_timestamp
or (datetime.now(UTC).isoformat() if record.decision else None),
deviation_note=record.deviation_note,
owner_id=record.reviewer_id,
)
db.add(entry)
await db.flush()
return entry.id