M0 e M1 revisados e fixes implementados

This commit is contained in:
Marcos Lohmann
2026-09-14 08:18:53 -03:00
parent 439f60598e
commit f3efcc3414
72 changed files with 4831 additions and 128 deletions

View File

View File

@@ -0,0 +1,52 @@
"""Approval request contract (EP-14-T01, README.md §26).
Out-of-band, bound to the *exact* operation it was requested for (not just
the tool name), with an expiry and single-use semantics -- so an approval
granted for one call can never be replayed against a different one (same
tool, different arguments) or reused twice (README.md §46, "approval
replay").
"""
from __future__ import annotations
import hashlib
import json
from datetime import UTC, datetime
from typing import Any, Literal
from uuid import uuid4
from pydantic import BaseModel, Field
def compute_operation_hash(*, client_profile: str, tool: str, arguments: dict[str, Any]) -> str:
"""Binds an approval to one exact (client_profile, tool, arguments) triple.
Canonical JSON (sorted keys) so the same logical call always hashes the
same way regardless of dict insertion order.
"""
canonical = json.dumps(
{"client_profile": client_profile, "tool": tool, "arguments": arguments},
sort_keys=True,
default=str,
)
return hashlib.sha256(canonical.encode("utf-8")).hexdigest()
class ApprovalRequest(BaseModel):
approval_id: str = Field(default_factory=lambda: str(uuid4()))
operation_hash: str
client_profile: str
tool: str
arguments_summary: dict[str, Any]
"""A copy of the relevant arguments for an approver to review -- README.md
§26 ("com resumo dos argumentos relevantes"). Not redacted in this RI;
a production deployment would apply the same PII-minimization rules as
audit logging (README.md §27)."""
reason_code: str
requested_at: datetime = Field(default_factory=lambda: datetime.now(UTC))
expires_at: datetime
status: Literal["pending", "granted", "denied", "consumed"] = "pending"
def is_expired(self, *, now: datetime | None = None) -> bool:
return (now or datetime.now(UTC)) >= self.expires_at

View File

@@ -0,0 +1,113 @@
"""Approval service (EP-14-T02, README.md §26).
Gates a `REQUIRE_APPROVAL` PDP decision (EP-03-T04): the Gateway calls
`check_and_consume` for every attempt of an operation that needs approval; a
human approver (CLI, API, or a future Orchestrator human-in-the-loop node,
EP-11-T05) calls `request`/`grant`/`deny` out-of-band. Approval is never
implicit -- a REQUIRE_APPROVAL attempt with no matching grant is denied, not
queued-and-allowed.
"""
from __future__ import annotations
from datetime import UTC, datetime, timedelta
from typing import Any
from emcp_bus.approval.models import ApprovalRequest, compute_operation_hash
from emcp_bus.audit.events import approval_granted
from emcp_bus.audit.sink import AuditSink
class ApprovalNotFoundError(Exception):
pass
class ApprovalService:
def __init__(self, audit_sink: AuditSink | None = None) -> None:
self._requests: dict[str, ApprovalRequest] = {}
self._audit_sink = audit_sink
def request(
self,
*,
client_profile: str,
tool: str,
arguments: dict[str, Any],
reason_code: str,
ttl_seconds: int = 300,
) -> ApprovalRequest:
"""Create (or return the existing pending one for) an approval request
for this exact operation -- repeated attempts of the same call while
one is pending do not spawn duplicate requests for an approver to
wade through."""
operation_hash = compute_operation_hash(
client_profile=client_profile, tool=tool, arguments=arguments
)
existing = self._pending_for_operation(operation_hash)
if existing is not None:
return existing
approval = ApprovalRequest(
operation_hash=operation_hash,
client_profile=client_profile,
tool=tool,
arguments_summary=arguments,
reason_code=reason_code,
expires_at=datetime.now(UTC) + timedelta(seconds=ttl_seconds),
)
self._requests[approval.approval_id] = approval
return approval
def grant(self, approval_id: str) -> None:
approval = self._get(approval_id)
approval.status = "granted"
if self._audit_sink is not None:
self._audit_sink.emit(
approval_granted(
approval_id=approval.approval_id,
client_profile=approval.client_profile,
tool=approval.tool,
)
)
def deny(self, approval_id: str) -> None:
approval = self._get(approval_id)
approval.status = "denied"
def check_and_consume(
self, *, client_profile: str, tool: str, arguments: dict[str, Any]
) -> bool:
"""True exactly once for a matching, granted, unexpired approval.
Single-use (README.md §46, "approval replay"): a granted approval
that passes this check is immediately marked `consumed` and can
never satisfy a second attempt, even of the identical operation.
"""
operation_hash = compute_operation_hash(
client_profile=client_profile, tool=tool, arguments=arguments
)
for approval in self._requests.values():
if (
approval.operation_hash == operation_hash
and approval.status == "granted"
and not approval.is_expired()
):
approval.status = "consumed"
return True
return False
def _pending_for_operation(self, operation_hash: str) -> ApprovalRequest | None:
for approval in self._requests.values():
if (
approval.operation_hash == operation_hash
and approval.status == "pending"
and not approval.is_expired()
):
return approval
return None
def _get(self, approval_id: str) -> ApprovalRequest:
approval = self._requests.get(approval_id)
if approval is None:
raise ApprovalNotFoundError(approval_id)
return approval