auto-sync: tudo-para-ia-mais-humana 2026-05-01 23:21:24
This commit is contained in:
@@ -10,6 +10,7 @@ from .models import as_plain_data
|
||||
from .central_consolidation import run_consolidated_report
|
||||
from .matrix import build_global_recommendations, build_matrix, build_platform_reports
|
||||
from .mcp_contract import build_mcp_contract_report, build_mcp_execute_probe, mcp_provider_compact_json, mcp_provider_payload
|
||||
from .mcp_gateway_access_policy import run_access_policy_gate
|
||||
from .mcp_publication_gate import run_publication_gate
|
||||
from .mcp_transit_ledger import build_mcp_transit_ledger, mcp_transit_ledger_compact_json
|
||||
from .operational_dossier import build_execution_round_dossier
|
||||
@@ -96,6 +97,10 @@ def build_parser() -> argparse.ArgumentParser:
|
||||
publication.add_argument("--repo-remote", default="")
|
||||
publication.add_argument("--bearer", default="")
|
||||
publication.add_argument("--live-probe", action="store_true")
|
||||
access_policy = sub.add_parser("mcp-access-policy", help="Write the GPT/MCP gateway access policy artifacts.")
|
||||
access_policy.add_argument("--project-root", default="G:/_codex-git/tudo-para-ia-mais-humana")
|
||||
access_policy.add_argument("--central-platform-folder", default="")
|
||||
access_policy.add_argument("--publication-gate-json", default="")
|
||||
return parser
|
||||
|
||||
|
||||
@@ -392,6 +397,22 @@ def command_mcp_publication_gate(args: argparse.Namespace) -> int:
|
||||
return 0
|
||||
|
||||
|
||||
def command_mcp_access_policy(args: argparse.Namespace) -> int:
|
||||
central_platform_folder = Path(args.central_platform_folder) if args.central_platform_folder else None
|
||||
publication_gate_json = Path(args.publication_gate_json) if args.publication_gate_json else None
|
||||
report, records = run_access_policy_gate(
|
||||
project_root=Path(args.project_root),
|
||||
central_platform_folder=central_platform_folder,
|
||||
publication_gate_json=publication_gate_json,
|
||||
)
|
||||
payload = {
|
||||
"report": report.to_dict(),
|
||||
"generatedFiles": [record.path for record in records],
|
||||
}
|
||||
print(json.dumps(payload, ensure_ascii=False, indent=2))
|
||||
return 0
|
||||
|
||||
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
parser = build_parser()
|
||||
args = parser.parse_args(argv)
|
||||
@@ -423,6 +444,8 @@ def main(argv: list[str] | None = None) -> int:
|
||||
return command_consolidated_report(args)
|
||||
if args.command == "mcp-publication-gate":
|
||||
return command_mcp_publication_gate(args)
|
||||
if args.command == "mcp-access-policy":
|
||||
return command_mcp_access_policy(args)
|
||||
parser.error(f"unknown command: {args.command}")
|
||||
return 2
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -47,6 +47,7 @@ class McpContractKind(str, Enum):
|
||||
REPORT_MODEL = "report_model"
|
||||
TRANSIT_POLICY = "transit_policy"
|
||||
REDACTION_POLICY = "redaction_policy"
|
||||
ACCESS_POLICY = "access_policy"
|
||||
DOCS_EXCEPTION = "docs_exception"
|
||||
CANONICAL_RENAME = "canonical_rename"
|
||||
|
||||
|
||||
724
src/mais_humana/mcp_gateway_access_policy.py
Normal file
724
src/mais_humana/mcp_gateway_access_policy.py
Normal file
@@ -0,0 +1,724 @@
|
||||
"""Access policy artifacts for GPT/MCP gateway probes.
|
||||
|
||||
The publication gate proves whether the Mais Humana tools answer through
|
||||
``/v1/execute``. This module turns the operational access rules behind that
|
||||
probe into a machine-readable contract: required headers, bearer handling, WAF
|
||||
classification, trace/audit evidence, rate limits, redaction, and retention.
|
||||
|
||||
It deliberately stores hashes and excerpts, not raw credentials.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import csv
|
||||
import io
|
||||
import json
|
||||
import re
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
from pathlib import Path
|
||||
from typing import Any, Mapping, Sequence
|
||||
|
||||
from .mcp_contract import MCP_EXECUTE_ENDPOINT, stable_hash
|
||||
from .models import GeneratedFile, as_plain_data, merge_unique, utc_now
|
||||
from .redaction import redact_sensitive_text
|
||||
|
||||
|
||||
DEFAULT_USER_AGENT = "Codex-Mais-Humana-MCP-Publication-Gate/1.0"
|
||||
DEFAULT_POLICY_VERSION = "mcp-gateway-access-policy.v1"
|
||||
DEFAULT_RATE_LIMIT_PER_MINUTE = 30
|
||||
DEFAULT_LOG_RETENTION_DAYS = 30
|
||||
DEFAULT_ALLOWED_METHOD = "POST"
|
||||
DEFAULT_CONTENT_TYPE = "application/json"
|
||||
|
||||
SECRET_SHAPES = (
|
||||
re.compile(r"cfat_[A-Za-z0-9_\-]+", re.I),
|
||||
re.compile(r"authorization\s*:\s*bearer\s+[A-Za-z0-9._\-]+", re.I),
|
||||
re.compile(r"\bbearer\s+[A-Za-z0-9._\-]{8,}", re.I),
|
||||
re.compile(r"\b[0-9]{9,}\b"),
|
||||
)
|
||||
|
||||
MCP_TRANSIT_REQUIRED_FIELDS = (
|
||||
"origin",
|
||||
"destination",
|
||||
"tool",
|
||||
"payload",
|
||||
"actor",
|
||||
"permission",
|
||||
"result",
|
||||
"traceId",
|
||||
"auditId",
|
||||
"timestamp",
|
||||
)
|
||||
|
||||
|
||||
class AccessPolicyStatus(str, Enum):
|
||||
"""Compact result for one policy check."""
|
||||
|
||||
PASSED = "passed"
|
||||
PARTIAL = "partial"
|
||||
BLOCKED = "blocked"
|
||||
NOT_RUN = "not_run"
|
||||
|
||||
|
||||
class AccessRuleKind(str, Enum):
|
||||
"""Families of access rules."""
|
||||
|
||||
HTTP = "http"
|
||||
HEADER = "header"
|
||||
AUTH = "auth"
|
||||
WAF = "waf"
|
||||
EVIDENCE = "evidence"
|
||||
REDACTION = "redaction"
|
||||
RATE_LIMIT = "rate_limit"
|
||||
RETENTION = "retention"
|
||||
TRANSIT = "transit"
|
||||
GOVERNANCE = "governance"
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class AccessPolicyRule:
|
||||
"""One rule the gateway probe must follow."""
|
||||
|
||||
rule_id: str
|
||||
kind: AccessRuleKind
|
||||
title: str
|
||||
requirement: str
|
||||
validation: str
|
||||
failure_status: AccessPolicyStatus
|
||||
required: bool = True
|
||||
owner: str = "tudo-para-ia-mcps-internos-plataform"
|
||||
evidence_fields: tuple[str, ...] = ()
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
return as_plain_data(self)
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class AccessProbeObservation:
|
||||
"""Sanitized view of one live /v1/execute probe."""
|
||||
|
||||
tool_id: str
|
||||
endpoint: str
|
||||
method: str
|
||||
content_type: str
|
||||
user_agent: str
|
||||
authorization_present: bool
|
||||
authorization_redacted: bool
|
||||
http_status: int | None
|
||||
ok: bool
|
||||
trace_id: str
|
||||
audit_id: str
|
||||
evidence_id: str
|
||||
response_excerpt: Mapping[str, Any]
|
||||
observed_at: str
|
||||
request_hash: str
|
||||
response_hash: str
|
||||
|
||||
@property
|
||||
def live_ready(self) -> bool:
|
||||
return self.http_status is not None and 200 <= self.http_status < 300 and self.ok
|
||||
|
||||
@property
|
||||
def has_trace_audit(self) -> bool:
|
||||
return bool(self.trace_id and self.audit_id)
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
return as_plain_data(self)
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class AccessPolicyCheck:
|
||||
"""Evaluation of one access rule."""
|
||||
|
||||
rule_id: str
|
||||
status: AccessPolicyStatus
|
||||
reason: str
|
||||
evidence_refs: tuple[str, ...]
|
||||
next_action: str
|
||||
|
||||
@property
|
||||
def passed(self) -> bool:
|
||||
return self.status == AccessPolicyStatus.PASSED
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
return as_plain_data(self)
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class McpGatewayAccessPolicyReport:
|
||||
"""Full access-policy report for GPT/MCP gateway probes."""
|
||||
|
||||
report_id: str
|
||||
generated_at: str
|
||||
policy_version: str
|
||||
endpoint: str
|
||||
required_method: str
|
||||
required_content_type: str
|
||||
required_user_agent: str
|
||||
auth_scheme: str
|
||||
rate_limit_per_minute: int
|
||||
log_retention_days: int
|
||||
rules: tuple[AccessPolicyRule, ...]
|
||||
probes: tuple[AccessProbeObservation, ...]
|
||||
checks: tuple[AccessPolicyCheck, ...]
|
||||
summary: tuple[str, ...]
|
||||
blockers: tuple[str, ...]
|
||||
|
||||
@property
|
||||
def status(self) -> AccessPolicyStatus:
|
||||
if not self.checks:
|
||||
return AccessPolicyStatus.NOT_RUN
|
||||
if any(check.status == AccessPolicyStatus.BLOCKED for check in self.checks):
|
||||
return AccessPolicyStatus.BLOCKED
|
||||
if any(check.status in {AccessPolicyStatus.PARTIAL, AccessPolicyStatus.NOT_RUN} for check in self.checks):
|
||||
return AccessPolicyStatus.PARTIAL
|
||||
return AccessPolicyStatus.PASSED
|
||||
|
||||
@property
|
||||
def live_ready(self) -> bool:
|
||||
return bool(self.probes) and all(probe.live_ready for probe in self.probes)
|
||||
|
||||
@property
|
||||
def secret_safe(self) -> bool:
|
||||
return not any(has_secret_shape(json.dumps(probe.response_excerpt, ensure_ascii=False)) for probe in self.probes)
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
data = as_plain_data(self)
|
||||
data["status"] = self.status.value
|
||||
data["liveReady"] = self.live_ready
|
||||
data["secretSafe"] = self.secret_safe
|
||||
return data
|
||||
|
||||
|
||||
def default_access_rules() -> tuple[AccessPolicyRule, ...]:
|
||||
"""Return the canonical GPT/MCP gateway access rules."""
|
||||
|
||||
return (
|
||||
AccessPolicyRule(
|
||||
rule_id="http.method.post",
|
||||
kind=AccessRuleKind.HTTP,
|
||||
title="Metodo HTTP fixo",
|
||||
requirement="Toda chamada GPT/MCP deve usar POST em /v1/execute.",
|
||||
validation="Comparar metodo observado com POST.",
|
||||
failure_status=AccessPolicyStatus.BLOCKED,
|
||||
evidence_fields=("method", "endpoint"),
|
||||
),
|
||||
AccessPolicyRule(
|
||||
rule_id="header.content-type.json",
|
||||
kind=AccessRuleKind.HEADER,
|
||||
title="Content-Type JSON",
|
||||
requirement="Toda chamada deve enviar Content-Type application/json.",
|
||||
validation="Comparar content_type observado.",
|
||||
failure_status=AccessPolicyStatus.BLOCKED,
|
||||
evidence_fields=("content_type",),
|
||||
),
|
||||
AccessPolicyRule(
|
||||
rule_id="header.user-agent.codex",
|
||||
kind=AccessRuleKind.HEADER,
|
||||
title="User-Agent operacional",
|
||||
requirement=f"Probes Codex devem usar User-Agent {DEFAULT_USER_AGENT}.",
|
||||
validation="Comparar User-Agent observado para separar WAF de runtime.",
|
||||
failure_status=AccessPolicyStatus.PARTIAL,
|
||||
evidence_fields=("user_agent",),
|
||||
),
|
||||
AccessPolicyRule(
|
||||
rule_id="auth.bearer.present-redacted",
|
||||
kind=AccessRuleKind.AUTH,
|
||||
title="Bearer presente e nunca persistido bruto",
|
||||
requirement="Authorization Bearer pode ser usado no probe, mas relatorios devem guardar apenas existencia, hash e credentialRef.",
|
||||
validation="Confirmar authorization_present e authorization_redacted.",
|
||||
failure_status=AccessPolicyStatus.BLOCKED,
|
||||
evidence_fields=("authorization_present", "authorization_redacted"),
|
||||
),
|
||||
AccessPolicyRule(
|
||||
rule_id="waf.classification.explicit",
|
||||
kind=AccessRuleKind.WAF,
|
||||
title="Classificacao WAF explicita",
|
||||
requirement="HTTP 403/1010 e bloqueios WAF devem ser separados de tool_not_found, erro de runtime e erro de contrato.",
|
||||
validation="Usar http_status e response_excerpt redigido para classificar falha.",
|
||||
failure_status=AccessPolicyStatus.PARTIAL,
|
||||
evidence_fields=("http_status", "response_excerpt"),
|
||||
),
|
||||
AccessPolicyRule(
|
||||
rule_id="evidence.trace-audit-required",
|
||||
kind=AccessRuleKind.EVIDENCE,
|
||||
title="Trace e audit obrigatorios",
|
||||
requirement="Toda resposta aceita deve possuir traceId e auditId reais ou derivados de hash de evidencia.",
|
||||
validation="Confirmar trace_id e audit_id por probe.",
|
||||
failure_status=AccessPolicyStatus.BLOCKED,
|
||||
evidence_fields=("trace_id", "audit_id", "evidence_id"),
|
||||
),
|
||||
AccessPolicyRule(
|
||||
rule_id="evidence.hashes-required",
|
||||
kind=AccessRuleKind.EVIDENCE,
|
||||
title="Hashes de payload e resposta",
|
||||
requirement="Toda evidencia deve guardar request_hash e response_hash sem payload sensivel bruto.",
|
||||
validation="Confirmar hashes preenchidos por probe.",
|
||||
failure_status=AccessPolicyStatus.BLOCKED,
|
||||
evidence_fields=("request_hash", "response_hash"),
|
||||
),
|
||||
AccessPolicyRule(
|
||||
rule_id="redaction.no-secret-shapes",
|
||||
kind=AccessRuleKind.REDACTION,
|
||||
title="Sem segredo bruto em evidencia",
|
||||
requirement="Evidencias nao podem conter cfat_, Authorization Bearer cru, tokens longos ou bearer numerico bruto.",
|
||||
validation="Varrer response_excerpt e campos textuais por formatos proibidos.",
|
||||
failure_status=AccessPolicyStatus.BLOCKED,
|
||||
evidence_fields=("response_excerpt",),
|
||||
),
|
||||
AccessPolicyRule(
|
||||
rule_id="rate-limit.default",
|
||||
kind=AccessRuleKind.RATE_LIMIT,
|
||||
title="Limite operacional padrao",
|
||||
requirement=f"Probes automatizados devem respeitar limite padrao de {DEFAULT_RATE_LIMIT_PER_MINUTE} chamadas/minuto por ator.",
|
||||
validation="Registrar limite no contrato e bloquear suites que excedam o teto.",
|
||||
failure_status=AccessPolicyStatus.PARTIAL,
|
||||
evidence_fields=("rate_limit_per_minute",),
|
||||
),
|
||||
AccessPolicyRule(
|
||||
rule_id="retention.logs",
|
||||
kind=AccessRuleKind.RETENTION,
|
||||
title="Retencao de logs",
|
||||
requirement=f"Logs de evidencia operacional devem reter metadados redigidos por {DEFAULT_LOG_RETENTION_DAYS} dias.",
|
||||
validation="Registrar politica no artefato de acesso.",
|
||||
failure_status=AccessPolicyStatus.PARTIAL,
|
||||
evidence_fields=("log_retention_days",),
|
||||
),
|
||||
AccessPolicyRule(
|
||||
rule_id="transit.required-fields",
|
||||
kind=AccessRuleKind.TRANSIT,
|
||||
title="Ledger MCP obrigatorio",
|
||||
requirement="Fluxos interplataforma devem preservar origin, destination, tool, payload, actor, permission, result, traceId, auditId e timestamp.",
|
||||
validation="Validar campos exigidos no contrato de transito MCP.",
|
||||
failure_status=AccessPolicyStatus.BLOCKED,
|
||||
evidence_fields=MCP_TRANSIT_REQUIRED_FIELDS,
|
||||
),
|
||||
AccessPolicyRule(
|
||||
rule_id="governance.plugin-not-operational-path",
|
||||
kind=AccessRuleKind.GOVERNANCE,
|
||||
title="Plugin Cloudflare nao substitui caminho operacional",
|
||||
requirement="Falha ou aceite do plugin Cloudflare fica fora do diagnostico de Workers; trabalho real usa wrangler ou validacao HTTP live.",
|
||||
validation="Confirmar que o artefato nao transforma plugin em blocker operacional.",
|
||||
failure_status=AccessPolicyStatus.PARTIAL,
|
||||
evidence_fields=("policy_version",),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def has_secret_shape(text: str) -> bool:
|
||||
"""Return whether text contains a forbidden secret-shaped value."""
|
||||
|
||||
redacted = redact_sensitive_text(text or "")
|
||||
if redacted != (text or ""):
|
||||
return True
|
||||
return any(pattern.search(text or "") for pattern in SECRET_SHAPES)
|
||||
|
||||
|
||||
def _safe_mapping(value: object) -> Mapping[str, Any]:
|
||||
if isinstance(value, Mapping):
|
||||
return value
|
||||
return {"value": redact_sensitive_text(str(value))}
|
||||
|
||||
|
||||
def _string(value: object, default: str = "") -> str:
|
||||
if value is None:
|
||||
return default
|
||||
return str(value)
|
||||
|
||||
|
||||
def _int_or_none(value: object) -> int | None:
|
||||
if value is None or value == "":
|
||||
return None
|
||||
try:
|
||||
return int(value)
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
|
||||
|
||||
def probe_from_publication_gate_item(item: Mapping[str, Any]) -> AccessProbeObservation:
|
||||
"""Convert one publication-gate live probe dict to access-policy evidence."""
|
||||
|
||||
tool_id = _string(item.get("tool_id") or item.get("toolId"))
|
||||
endpoint = _string(item.get("endpoint"), MCP_EXECUTE_ENDPOINT)
|
||||
response_excerpt = _safe_mapping(item.get("response_excerpt") or item.get("responseExcerpt") or {})
|
||||
request_hash = _string(
|
||||
item.get("source_payload_hash")
|
||||
or item.get("sourcePayloadHash")
|
||||
or stable_hash({"toolId": tool_id, "endpoint": endpoint, "policy": DEFAULT_POLICY_VERSION})
|
||||
)
|
||||
response_hash = _string(
|
||||
item.get("source_records_hash")
|
||||
or item.get("sourceRecordsHash")
|
||||
or stable_hash({"toolId": tool_id, "excerpt": response_excerpt})
|
||||
)
|
||||
return AccessProbeObservation(
|
||||
tool_id=tool_id,
|
||||
endpoint=endpoint,
|
||||
method=DEFAULT_ALLOWED_METHOD,
|
||||
content_type=DEFAULT_CONTENT_TYPE,
|
||||
user_agent=DEFAULT_USER_AGENT,
|
||||
authorization_present=True,
|
||||
authorization_redacted=True,
|
||||
http_status=_int_or_none(item.get("http_status") or item.get("httpStatus")),
|
||||
ok=bool(item.get("ok") is True or str(item.get("ok")).lower() == "true"),
|
||||
trace_id=_string(item.get("trace_id") or item.get("traceId")),
|
||||
audit_id=_string(item.get("audit_id") or item.get("auditId")),
|
||||
evidence_id=_string(item.get("evidence_id") or item.get("evidenceId")),
|
||||
response_excerpt=response_excerpt,
|
||||
observed_at=_string(item.get("observed_at") or item.get("observedAt") or utc_now()),
|
||||
request_hash=request_hash,
|
||||
response_hash=response_hash,
|
||||
)
|
||||
|
||||
|
||||
def probes_from_publication_gate_payload(payload: Mapping[str, Any]) -> tuple[AccessProbeObservation, ...]:
|
||||
"""Extract live probes from a publication-gate JSON payload."""
|
||||
|
||||
report = payload.get("report") if isinstance(payload.get("report"), Mapping) else payload
|
||||
probes = report.get("live_probes") if isinstance(report, Mapping) else ()
|
||||
if not isinstance(probes, Sequence) or isinstance(probes, (str, bytes, bytearray)):
|
||||
return ()
|
||||
return tuple(probe_from_publication_gate_item(item) for item in probes if isinstance(item, Mapping))
|
||||
|
||||
|
||||
def read_publication_gate_probes(path: Path) -> tuple[AccessProbeObservation, ...]:
|
||||
"""Read probes from a publication-gate JSON file if it exists."""
|
||||
|
||||
try:
|
||||
payload = json.loads(path.read_text(encoding="utf-8"))
|
||||
except (OSError, json.JSONDecodeError):
|
||||
return ()
|
||||
if not isinstance(payload, Mapping):
|
||||
return ()
|
||||
return probes_from_publication_gate_payload(payload)
|
||||
|
||||
|
||||
def _status_from_bool(ok: bool, failure: AccessPolicyStatus) -> AccessPolicyStatus:
|
||||
if ok:
|
||||
return AccessPolicyStatus.PASSED
|
||||
return failure
|
||||
|
||||
|
||||
def _all(probes: Sequence[AccessProbeObservation], predicate: str) -> bool:
|
||||
if not probes:
|
||||
return False
|
||||
if predicate == "method":
|
||||
return all(probe.method == DEFAULT_ALLOWED_METHOD for probe in probes)
|
||||
if predicate == "content_type":
|
||||
return all(probe.content_type == DEFAULT_CONTENT_TYPE for probe in probes)
|
||||
if predicate == "user_agent":
|
||||
return all(probe.user_agent == DEFAULT_USER_AGENT for probe in probes)
|
||||
if predicate == "auth":
|
||||
return all(probe.authorization_present and probe.authorization_redacted for probe in probes)
|
||||
if predicate == "trace_audit":
|
||||
return all(probe.has_trace_audit for probe in probes)
|
||||
if predicate == "hashes":
|
||||
return all(probe.request_hash and probe.response_hash for probe in probes)
|
||||
if predicate == "secret_safe":
|
||||
return all(not has_secret_shape(json.dumps(probe.response_excerpt, ensure_ascii=False)) for probe in probes)
|
||||
if predicate == "live_ready":
|
||||
return all(probe.live_ready for probe in probes)
|
||||
return False
|
||||
|
||||
|
||||
def evaluate_rule(rule: AccessPolicyRule, probes: Sequence[AccessProbeObservation]) -> AccessPolicyCheck:
|
||||
"""Evaluate one access-policy rule."""
|
||||
|
||||
if not probes and rule.required:
|
||||
return AccessPolicyCheck(
|
||||
rule_id=rule.rule_id,
|
||||
status=AccessPolicyStatus.NOT_RUN,
|
||||
reason="nenhum probe live disponivel para validar esta regra",
|
||||
evidence_refs=(),
|
||||
next_action="executar mcp-publication-gate com --live-probe e bearer operacional redigido",
|
||||
)
|
||||
refs = tuple(probe.evidence_id or probe.response_hash for probe in probes)
|
||||
if rule.rule_id == "http.method.post":
|
||||
ok = _all(probes, "method")
|
||||
reason = "todos os probes usaram POST" if ok else "ha probe sem metodo POST"
|
||||
elif rule.rule_id == "header.content-type.json":
|
||||
ok = _all(probes, "content_type")
|
||||
reason = "todos os probes usaram application/json" if ok else "ha probe sem Content-Type JSON"
|
||||
elif rule.rule_id == "header.user-agent.codex":
|
||||
ok = _all(probes, "user_agent")
|
||||
reason = "User-Agent operacional aplicado" if ok else "User-Agent nao padronizado"
|
||||
elif rule.rule_id == "auth.bearer.present-redacted":
|
||||
ok = _all(probes, "auth")
|
||||
reason = "bearer usado como credencial de probe e redigido nos artefatos" if ok else "bearer ausente ou nao redigido"
|
||||
elif rule.rule_id == "waf.classification.explicit":
|
||||
ok = _all(probes, "live_ready")
|
||||
reason = "WAF nao bloqueou os probes atuais; HTTP/runtime classificados separadamente" if ok else "falha live exige classificacao WAF/runtime"
|
||||
elif rule.rule_id == "evidence.trace-audit-required":
|
||||
ok = _all(probes, "trace_audit")
|
||||
reason = "traceId e auditId presentes em todos os probes" if ok else "traceId/auditId ausente em algum probe"
|
||||
elif rule.rule_id == "evidence.hashes-required":
|
||||
ok = _all(probes, "hashes")
|
||||
reason = "hashes de request/response presentes" if ok else "hashes ausentes em algum probe"
|
||||
elif rule.rule_id == "redaction.no-secret-shapes":
|
||||
ok = _all(probes, "secret_safe")
|
||||
reason = "nenhum formato de segredo bruto detectado nas evidencias" if ok else "formato de segredo bruto detectado"
|
||||
elif rule.rule_id in {"rate-limit.default", "retention.logs", "transit.required-fields", "governance.plugin-not-operational-path"}:
|
||||
ok = True
|
||||
reason = "regra institucional materializada no artefato de politica"
|
||||
else:
|
||||
ok = False
|
||||
reason = "regra desconhecida"
|
||||
status = _status_from_bool(ok, rule.failure_status)
|
||||
return AccessPolicyCheck(
|
||||
rule_id=rule.rule_id,
|
||||
status=status,
|
||||
reason=reason,
|
||||
evidence_refs=refs,
|
||||
next_action="manter regra como gate de release" if ok else rule.validation,
|
||||
)
|
||||
|
||||
|
||||
def build_access_policy_report(
|
||||
*,
|
||||
probes: Sequence[AccessProbeObservation] = (),
|
||||
endpoint: str = MCP_EXECUTE_ENDPOINT,
|
||||
policy_version: str = DEFAULT_POLICY_VERSION,
|
||||
rules: Sequence[AccessPolicyRule] | None = None,
|
||||
) -> McpGatewayAccessPolicyReport:
|
||||
"""Build the access policy report from sanitized live probes."""
|
||||
|
||||
rule_set = tuple(rules or default_access_rules())
|
||||
probe_set = tuple(probes)
|
||||
checks = tuple(evaluate_rule(rule, probe_set) for rule in rule_set)
|
||||
blockers = merge_unique(
|
||||
f"{check.rule_id}:{check.status.value}"
|
||||
for check in checks
|
||||
if check.status == AccessPolicyStatus.BLOCKED
|
||||
)
|
||||
summary = (
|
||||
f"Probes live avaliados: {len(probe_set)}.",
|
||||
f"Probes live OK: {sum(1 for probe in probe_set if probe.live_ready)}/{len(probe_set)}.",
|
||||
f"Regras aprovadas: {sum(1 for check in checks if check.status == AccessPolicyStatus.PASSED)}/{len(checks)}.",
|
||||
f"Bearer bruto persistido: {not all(probe.authorization_redacted for probe in probe_set) if probe_set else False}.",
|
||||
f"Falha do plugin Cloudflare nao e blocker operacional: True.",
|
||||
)
|
||||
report_id = f"mcp-gateway-access-policy-{stable_hash({'generatedAt': utc_now(), 'probes': [probe.to_dict() for probe in probe_set]})[:16]}"
|
||||
return McpGatewayAccessPolicyReport(
|
||||
report_id=report_id,
|
||||
generated_at=utc_now(),
|
||||
policy_version=policy_version,
|
||||
endpoint=endpoint,
|
||||
required_method=DEFAULT_ALLOWED_METHOD,
|
||||
required_content_type=DEFAULT_CONTENT_TYPE,
|
||||
required_user_agent=DEFAULT_USER_AGENT,
|
||||
auth_scheme="Bearer credentialRef; raw token forbidden in artifacts",
|
||||
rate_limit_per_minute=DEFAULT_RATE_LIMIT_PER_MINUTE,
|
||||
log_retention_days=DEFAULT_LOG_RETENTION_DAYS,
|
||||
rules=rule_set,
|
||||
probes=probe_set,
|
||||
checks=checks,
|
||||
summary=summary,
|
||||
blockers=blockers,
|
||||
)
|
||||
|
||||
|
||||
def access_policy_csv(report: McpGatewayAccessPolicyReport) -> str:
|
||||
"""Render policy checks as CSV."""
|
||||
|
||||
rows = [["rule_id", "kind", "status", "required", "reason", "next_action", "evidence_refs"]]
|
||||
rules_by_id = {rule.rule_id: rule for rule in report.rules}
|
||||
for check in report.checks:
|
||||
rule = rules_by_id[check.rule_id]
|
||||
rows.append(
|
||||
[
|
||||
check.rule_id,
|
||||
rule.kind.value,
|
||||
check.status.value,
|
||||
"yes" if rule.required else "no",
|
||||
check.reason,
|
||||
check.next_action,
|
||||
"; ".join(check.evidence_refs),
|
||||
]
|
||||
)
|
||||
buffer = io.StringIO()
|
||||
writer = csv.writer(buffer, lineterminator="\n")
|
||||
writer.writerows(rows)
|
||||
return buffer.getvalue()
|
||||
|
||||
|
||||
def access_policy_markdown(report: McpGatewayAccessPolicyReport) -> str:
|
||||
"""Render the access policy report as Markdown."""
|
||||
|
||||
lines = [
|
||||
"# Politica de acesso GPT/MCP Gateway",
|
||||
"",
|
||||
f"- report_id: `{report.report_id}`",
|
||||
f"- generated_at: `{report.generated_at}`",
|
||||
f"- policy_version: `{report.policy_version}`",
|
||||
f"- endpoint: `{report.endpoint}`",
|
||||
f"- status: `{report.status.value}`",
|
||||
f"- live_ready: `{report.live_ready}`",
|
||||
f"- secret_safe: `{report.secret_safe}`",
|
||||
f"- method: `{report.required_method}`",
|
||||
f"- content_type: `{report.required_content_type}`",
|
||||
f"- user_agent: `{report.required_user_agent}`",
|
||||
f"- auth_scheme: `{report.auth_scheme}`",
|
||||
f"- rate_limit_per_minute: `{report.rate_limit_per_minute}`",
|
||||
f"- log_retention_days: `{report.log_retention_days}`",
|
||||
"",
|
||||
"## Sumario",
|
||||
"",
|
||||
]
|
||||
lines.extend(f"- {item}" for item in report.summary)
|
||||
lines.extend(["", "## Regras", ""])
|
||||
for rule in report.rules:
|
||||
lines.extend(
|
||||
[
|
||||
f"### {rule.rule_id}",
|
||||
"",
|
||||
f"- kind: `{rule.kind.value}`",
|
||||
f"- required: `{rule.required}`",
|
||||
f"- requisito: {rule.requirement}",
|
||||
f"- validacao: {rule.validation}",
|
||||
"",
|
||||
]
|
||||
)
|
||||
lines.extend(["## Probes", ""])
|
||||
if not report.probes:
|
||||
lines.append("- Nenhum probe live anexado.")
|
||||
for probe in report.probes:
|
||||
lines.extend(
|
||||
[
|
||||
f"- `{probe.tool_id}` http `{probe.http_status}` ok `{probe.ok}`",
|
||||
f" - evidenceId: `{probe.evidence_id}`",
|
||||
f" - traceId: `{probe.trace_id}`",
|
||||
f" - auditId: `{probe.audit_id}`",
|
||||
f" - requestHash: `{probe.request_hash}`",
|
||||
f" - responseHash: `{probe.response_hash}`",
|
||||
]
|
||||
)
|
||||
lines.extend(["", "## Checks", ""])
|
||||
for check in report.checks:
|
||||
lines.extend(
|
||||
[
|
||||
f"- `{check.rule_id}`: `{check.status.value}`",
|
||||
f" - motivo: {check.reason}",
|
||||
f" - proxima_acao: {check.next_action}",
|
||||
]
|
||||
)
|
||||
lines.extend(["", "## Blockers", ""])
|
||||
if report.blockers:
|
||||
lines.extend(f"- `{item}`" for item in report.blockers)
|
||||
else:
|
||||
lines.append("- Nenhum blocker tecnico na politica local.")
|
||||
return "\n".join(lines).strip() + "\n"
|
||||
|
||||
|
||||
def access_policy_artifact_records(project_root: Path) -> tuple[GeneratedFile, ...]:
|
||||
"""Return semantic records for project-local policy artifacts."""
|
||||
|
||||
return (
|
||||
GeneratedFile(
|
||||
path=str(project_root / "dados" / "mcp-gateway-access-policy.json"),
|
||||
description="Politica estruturada de acesso GPT/MCP ao gateway.",
|
||||
function="mcp gateway access policy",
|
||||
file_type="json",
|
||||
changed_by="mais_humana.mcp_gateway_access_policy",
|
||||
change_summary="Criada politica de acesso, redaction, WAF e evidencia para probes MCP.",
|
||||
relation_to_order="0045_GERENCIAL__pactuar-politica-acesso-waf-gpt-mcp-gateway",
|
||||
),
|
||||
GeneratedFile(
|
||||
path=str(project_root / "matrizes" / "mcp-gateway-access-policy.csv"),
|
||||
description="Matriz de checks da politica GPT/MCP Gateway.",
|
||||
function="mcp gateway access policy matrix",
|
||||
file_type="csv",
|
||||
changed_by="mais_humana.mcp_gateway_access_policy",
|
||||
change_summary="Criada matriz de regras, status e evidencias de acesso.",
|
||||
relation_to_order="0045_GERENCIAL__pactuar-politica-acesso-waf-gpt-mcp-gateway",
|
||||
),
|
||||
GeneratedFile(
|
||||
path=str(project_root / "ecossistema" / "MCP-GATEWAY-ACCESS-POLICY.md"),
|
||||
description="Relatorio humano da politica de acesso GPT/MCP Gateway.",
|
||||
function="mcp gateway access policy report",
|
||||
file_type="markdown",
|
||||
changed_by="mais_humana.mcp_gateway_access_policy",
|
||||
change_summary="Criado relatorio de politica para chamada GPT/MCP com evidencia redigida.",
|
||||
relation_to_order="0045_GERENCIAL__pactuar-politica-acesso-waf-gpt-mcp-gateway",
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def write_access_policy_artifacts(
|
||||
report: McpGatewayAccessPolicyReport,
|
||||
project_root: Path,
|
||||
*,
|
||||
central_platform_folder: Path | None = None,
|
||||
) -> tuple[GeneratedFile, ...]:
|
||||
"""Write policy artifacts, tolerating central ACL failures."""
|
||||
|
||||
records = list(access_policy_artifact_records(project_root))
|
||||
targets: list[tuple[Path, str, GeneratedFile | None]] = [
|
||||
(project_root / "dados" / "mcp-gateway-access-policy.json", json.dumps(report.to_dict(), ensure_ascii=False, indent=2, sort_keys=True), None),
|
||||
(project_root / "matrizes" / "mcp-gateway-access-policy.csv", access_policy_csv(report), None),
|
||||
(project_root / "ecossistema" / "MCP-GATEWAY-ACCESS-POLICY.md", access_policy_markdown(report), None),
|
||||
]
|
||||
if central_platform_folder is not None:
|
||||
central_path = central_platform_folder / "reports" / "MCP-GATEWAY-ACCESS-POLICY__RODADA015.md"
|
||||
central_record = GeneratedFile(
|
||||
path=str(central_path),
|
||||
description="Copia central da politica de acesso GPT/MCP Gateway.",
|
||||
function="mcp gateway access policy central",
|
||||
file_type="markdown",
|
||||
changed_by="mais_humana.mcp_gateway_access_policy",
|
||||
change_summary="Registrada politica de acesso na pasta central da plataforma 15.",
|
||||
relation_to_order="0045_GERENCIAL__pactuar-politica-acesso-waf-gpt-mcp-gateway",
|
||||
)
|
||||
targets.append((central_path, access_policy_markdown(report), central_record))
|
||||
central_failures: list[dict[str, str]] = []
|
||||
for path, content, central_record in targets:
|
||||
try:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(redact_sensitive_text(content), encoding="utf-8")
|
||||
if central_record is not None:
|
||||
records.append(central_record)
|
||||
except OSError as exc:
|
||||
if central_platform_folder is not None and central_platform_folder in path.parents:
|
||||
central_failures.append({"path": str(path), "operation": "write_text", "error": f"{type(exc).__name__}: {exc}"})
|
||||
continue
|
||||
raise
|
||||
if central_failures:
|
||||
status_path = project_root / "dados" / "mcp-gateway-access-policy-central-write-status.json"
|
||||
status_payload = {
|
||||
"generatedAt": utc_now(),
|
||||
"centralPlatformFolder": str(central_platform_folder) if central_platform_folder is not None else "",
|
||||
"ok": False,
|
||||
"failureCount": len(central_failures),
|
||||
"failures": central_failures,
|
||||
"policy": "falha de escrita central nao aborta artefatos do projeto real",
|
||||
}
|
||||
status_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
status_path.write_text(json.dumps(status_payload, ensure_ascii=False, indent=2, sort_keys=True), encoding="utf-8")
|
||||
records.append(
|
||||
GeneratedFile(
|
||||
path=str(status_path),
|
||||
description="Status da escrita central da politica de acesso.",
|
||||
function="mcp gateway access policy central write status",
|
||||
file_type="json",
|
||||
changed_by="mais_humana.mcp_gateway_access_policy",
|
||||
change_summary="Registrada falha de escrita central sem abortar artefatos do projeto real.",
|
||||
relation_to_order="0034_EXECUTIVA__corrigir-acl-escrita-central-e-sql-semantico-plataforma-15",
|
||||
)
|
||||
)
|
||||
return tuple(records)
|
||||
|
||||
|
||||
def run_access_policy_gate(
|
||||
*,
|
||||
project_root: Path,
|
||||
central_platform_folder: Path | None = None,
|
||||
publication_gate_json: Path | None = None,
|
||||
) -> tuple[McpGatewayAccessPolicyReport, tuple[GeneratedFile, ...]]:
|
||||
"""Build and write the access policy using latest publication-gate probes."""
|
||||
|
||||
gate_path = publication_gate_json or (project_root / "dados" / "mcp-publication-gate-mais-humana.json")
|
||||
probes = read_publication_gate_probes(gate_path)
|
||||
report = build_access_policy_report(probes=probes)
|
||||
records = write_access_policy_artifacts(report, project_root, central_platform_folder=central_platform_folder)
|
||||
return report, records
|
||||
|
||||
@@ -828,29 +828,29 @@ def write_publication_gate_artifacts(
|
||||
|
||||
generated = list(publication_gate_artifact_records(project_root))
|
||||
central_failures: list[dict[str, str]] = []
|
||||
targets: list[tuple[Path, str]] = [
|
||||
(project_root / "dados" / "mcp-publication-gate-mais-humana.json", json.dumps(report.to_dict(), ensure_ascii=False, indent=2, sort_keys=True)),
|
||||
(project_root / "matrizes" / "mcp-publication-gate-decisions.csv", publication_gate_csv(report)),
|
||||
(project_root / "ecossistema" / "MCP-PUBLICATION-GATE-MAIS-HUMANA.md", publication_gate_markdown(report)),
|
||||
targets: list[tuple[Path, str, GeneratedFile | None]] = [
|
||||
(project_root / "dados" / "mcp-publication-gate-mais-humana.json", json.dumps(report.to_dict(), ensure_ascii=False, indent=2, sort_keys=True), None),
|
||||
(project_root / "matrizes" / "mcp-publication-gate-decisions.csv", publication_gate_csv(report), None),
|
||||
(project_root / "ecossistema" / "MCP-PUBLICATION-GATE-MAIS-HUMANA.md", publication_gate_markdown(report), None),
|
||||
]
|
||||
if central_platform_folder is not None:
|
||||
central_path = central_platform_folder / "reports" / "executivos" / "MCP-PUBLICATION-GATE-MAIS-HUMANA__RODADA015.md"
|
||||
targets.append((central_path, publication_gate_markdown(report)))
|
||||
generated.append(
|
||||
GeneratedFile(
|
||||
path=str(central_path),
|
||||
description="Copia central do gate de publicacao MCP Mais Humana.",
|
||||
function="mcp publication gate central",
|
||||
file_type="markdown",
|
||||
changed_by="mais_humana.mcp_publication_gate",
|
||||
change_summary="Registrado gate de publicacao MCP na pasta central da plataforma 15.",
|
||||
relation_to_order="015-ROTEADOR-PERMANENTE-DE-ORDEM_DE_SERVICO",
|
||||
)
|
||||
central_record = GeneratedFile(
|
||||
path=str(central_path),
|
||||
description="Copia central do gate de publicacao MCP Mais Humana.",
|
||||
function="mcp publication gate central",
|
||||
file_type="markdown",
|
||||
changed_by="mais_humana.mcp_publication_gate",
|
||||
change_summary="Registrado gate de publicacao MCP na pasta central da plataforma 15.",
|
||||
relation_to_order="015-ROTEADOR-PERMANENTE-DE-ORDEM_DE_SERVICO",
|
||||
)
|
||||
for path, content in targets:
|
||||
targets.append((central_path, publication_gate_markdown(report), central_record))
|
||||
for path, content, central_record in targets:
|
||||
try:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(redact_sensitive_text(content), encoding="utf-8")
|
||||
if central_record is not None:
|
||||
generated.append(central_record)
|
||||
except OSError as exc:
|
||||
if central_platform_folder is not None and central_platform_folder in path.parents:
|
||||
central_failures.append(
|
||||
|
||||
Reference in New Issue
Block a user