auto-sync: tudo-para-ia-mais-humana 2026-05-01 23:21:24
This commit is contained in:
14
dados/mcp-gateway-access-policy-central-write-status.json
Normal file
14
dados/mcp-gateway-access-policy-central-write-status.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"centralPlatformFolder": "G:\\_codex-git\\nucleo-gestao-operacional\\central-de-ordem-de-servico\\projects\\15_repo_tudo-para-ia-mais-humana-platform",
|
||||
"failureCount": 1,
|
||||
"failures": [
|
||||
{
|
||||
"error": "PermissionError: [Errno 13] Permission denied: 'G:\\\\_codex-git\\\\nucleo-gestao-operacional\\\\central-de-ordem-de-servico\\\\projects\\\\15_repo_tudo-para-ia-mais-humana-platform\\\\reports\\\\MCP-GATEWAY-ACCESS-POLICY__RODADA015.md'",
|
||||
"operation": "write_text",
|
||||
"path": "G:\\_codex-git\\nucleo-gestao-operacional\\central-de-ordem-de-servico\\projects\\15_repo_tudo-para-ia-mais-humana-platform\\reports\\MCP-GATEWAY-ACCESS-POLICY__RODADA015.md"
|
||||
}
|
||||
],
|
||||
"generatedAt": "2026-05-02T02:17:13+00:00",
|
||||
"ok": false,
|
||||
"policy": "falha de escrita central nao aborta artefatos do projeto real"
|
||||
}
|
||||
440
dados/mcp-gateway-access-policy.json
Normal file
440
dados/mcp-gateway-access-policy.json
Normal file
@@ -0,0 +1,440 @@
|
||||
{
|
||||
"auth_scheme": "Bearer credentialRef; raw token forbidden in artifacts",
|
||||
"blockers": [],
|
||||
"checks": [
|
||||
{
|
||||
"evidence_refs": [
|
||||
"evidence-a75a27e0669c49da1db8b615",
|
||||
"evidence-af37a8d489b0038a7a6b5575",
|
||||
"evidence-3f0e3b9f829c7ff912b335d0"
|
||||
],
|
||||
"next_action": "manter regra como gate de release",
|
||||
"reason": "todos os probes usaram POST",
|
||||
"rule_id": "http.method.post",
|
||||
"status": "passed"
|
||||
},
|
||||
{
|
||||
"evidence_refs": [
|
||||
"evidence-a75a27e0669c49da1db8b615",
|
||||
"evidence-af37a8d489b0038a7a6b5575",
|
||||
"evidence-3f0e3b9f829c7ff912b335d0"
|
||||
],
|
||||
"next_action": "manter regra como gate de release",
|
||||
"reason": "todos os probes usaram application/json",
|
||||
"rule_id": "header.content-type.json",
|
||||
"status": "passed"
|
||||
},
|
||||
{
|
||||
"evidence_refs": [
|
||||
"evidence-a75a27e0669c49da1db8b615",
|
||||
"evidence-af37a8d489b0038a7a6b5575",
|
||||
"evidence-3f0e3b9f829c7ff912b335d0"
|
||||
],
|
||||
"next_action": "manter regra como gate de release",
|
||||
"reason": "User-Agent operacional aplicado",
|
||||
"rule_id": "header.user-agent.codex",
|
||||
"status": "passed"
|
||||
},
|
||||
{
|
||||
"evidence_refs": [
|
||||
"evidence-a75a27e0669c49da1db8b615",
|
||||
"evidence-af37a8d489b0038a7a6b5575",
|
||||
"evidence-3f0e3b9f829c7ff912b335d0"
|
||||
],
|
||||
"next_action": "manter regra como gate de release",
|
||||
"reason": "bearer usado como credencial de probe e redigido nos artefatos",
|
||||
"rule_id": "auth.bearer.present-redacted",
|
||||
"status": "passed"
|
||||
},
|
||||
{
|
||||
"evidence_refs": [
|
||||
"evidence-a75a27e0669c49da1db8b615",
|
||||
"evidence-af37a8d489b0038a7a6b5575",
|
||||
"evidence-3f0e3b9f829c7ff912b335d0"
|
||||
],
|
||||
"next_action": "manter regra como gate de release",
|
||||
"reason": "WAF nao bloqueou os probes atuais; HTTP/runtime classificados separadamente",
|
||||
"rule_id": "waf.classification.explicit",
|
||||
"status": "passed"
|
||||
},
|
||||
{
|
||||
"evidence_refs": [
|
||||
"evidence-a75a27e0669c49da1db8b615",
|
||||
"evidence-af37a8d489b0038a7a6b5575",
|
||||
"evidence-3f0e3b9f829c7ff912b335d0"
|
||||
],
|
||||
"next_action": "manter regra como gate de release",
|
||||
"reason": "traceId e auditId presentes em todos os probes",
|
||||
"rule_id": "evidence.trace-audit-required",
|
||||
"status": "passed"
|
||||
},
|
||||
{
|
||||
"evidence_refs": [
|
||||
"evidence-a75a27e0669c49da1db8b615",
|
||||
"evidence-af37a8d489b0038a7a6b5575",
|
||||
"evidence-3f0e3b9f829c7ff912b335d0"
|
||||
],
|
||||
"next_action": "manter regra como gate de release",
|
||||
"reason": "hashes de request/response presentes",
|
||||
"rule_id": "evidence.hashes-required",
|
||||
"status": "passed"
|
||||
},
|
||||
{
|
||||
"evidence_refs": [
|
||||
"evidence-a75a27e0669c49da1db8b615",
|
||||
"evidence-af37a8d489b0038a7a6b5575",
|
||||
"evidence-3f0e3b9f829c7ff912b335d0"
|
||||
],
|
||||
"next_action": "manter regra como gate de release",
|
||||
"reason": "nenhum formato de segredo bruto detectado nas evidencias",
|
||||
"rule_id": "redaction.no-secret-shapes",
|
||||
"status": "passed"
|
||||
},
|
||||
{
|
||||
"evidence_refs": [
|
||||
"evidence-a75a27e0669c49da1db8b615",
|
||||
"evidence-af37a8d489b0038a7a6b5575",
|
||||
"evidence-3f0e3b9f829c7ff912b335d0"
|
||||
],
|
||||
"next_action": "manter regra como gate de release",
|
||||
"reason": "regra institucional materializada no artefato de politica",
|
||||
"rule_id": "rate-limit.default",
|
||||
"status": "passed"
|
||||
},
|
||||
{
|
||||
"evidence_refs": [
|
||||
"evidence-a75a27e0669c49da1db8b615",
|
||||
"evidence-af37a8d489b0038a7a6b5575",
|
||||
"evidence-3f0e3b9f829c7ff912b335d0"
|
||||
],
|
||||
"next_action": "manter regra como gate de release",
|
||||
"reason": "regra institucional materializada no artefato de politica",
|
||||
"rule_id": "retention.logs",
|
||||
"status": "passed"
|
||||
},
|
||||
{
|
||||
"evidence_refs": [
|
||||
"evidence-a75a27e0669c49da1db8b615",
|
||||
"evidence-af37a8d489b0038a7a6b5575",
|
||||
"evidence-3f0e3b9f829c7ff912b335d0"
|
||||
],
|
||||
"next_action": "manter regra como gate de release",
|
||||
"reason": "regra institucional materializada no artefato de politica",
|
||||
"rule_id": "transit.required-fields",
|
||||
"status": "passed"
|
||||
},
|
||||
{
|
||||
"evidence_refs": [
|
||||
"evidence-a75a27e0669c49da1db8b615",
|
||||
"evidence-af37a8d489b0038a7a6b5575",
|
||||
"evidence-3f0e3b9f829c7ff912b335d0"
|
||||
],
|
||||
"next_action": "manter regra como gate de release",
|
||||
"reason": "regra institucional materializada no artefato de politica",
|
||||
"rule_id": "governance.plugin-not-operational-path",
|
||||
"status": "passed"
|
||||
}
|
||||
],
|
||||
"endpoint": "https://mcps-gateway.ami-app.workers.dev/v1/execute",
|
||||
"generated_at": "2026-05-02T02:17:13+00:00",
|
||||
"liveReady": true,
|
||||
"log_retention_days": 30,
|
||||
"policy_version": "mcp-gateway-access-policy.v1",
|
||||
"probes": [
|
||||
{
|
||||
"audit_id": "audit-a75a27e0669c49da1db8b615",
|
||||
"authorization_present": true,
|
||||
"authorization_redacted": true,
|
||||
"content_type": "application/json",
|
||||
"endpoint": "https://mcps-gateway.ami-app.workers.dev/v1/execute",
|
||||
"evidence_id": "evidence-a75a27e0669c49da1db8b615",
|
||||
"http_status": 200,
|
||||
"method": "POST",
|
||||
"observed_at": "2026-05-02T02:17:12+00:00",
|
||||
"ok": true,
|
||||
"request_hash": "3e1c8f057ac439f4b9b3eb7f8f5be9ac36323f08adc23db6fc7d51633076b79a",
|
||||
"response_excerpt": {
|
||||
"__truncated__": true,
|
||||
"actorId": "codex.service-order-round",
|
||||
"auditId": "audit:mcps-gateway:codex.service-order-round:mais_humana.rulebook.compact",
|
||||
"blockers": "[]",
|
||||
"consumption": "None",
|
||||
"nextActions": "[]",
|
||||
"ok": "True",
|
||||
"organizationId": "None",
|
||||
"productId": "None",
|
||||
"providerId": "mais_humana",
|
||||
"readiness": "None",
|
||||
"sampleData": "False",
|
||||
"simulated": "False",
|
||||
"status": "ok",
|
||||
"traceId": "trace:mcps-gateway:codex.service-order-round:mais_humana.rulebook.compact",
|
||||
"userId": "None",
|
||||
"workspaceId": "None"
|
||||
},
|
||||
"response_hash": "a75a27e0669c49da1db8b6157757c0615eed06c32674c7ed87a6db5d071359de",
|
||||
"tool_id": "mais_humana.rulebook.compact",
|
||||
"trace_id": "trace-3e1c8f057ac439f4b9b3eb7f",
|
||||
"user_agent": "Codex-Mais-Humana-MCP-Publication-Gate/1.0"
|
||||
},
|
||||
{
|
||||
"audit_id": "audit-af37a8d489b0038a7a6b5575",
|
||||
"authorization_present": true,
|
||||
"authorization_redacted": true,
|
||||
"content_type": "application/json",
|
||||
"endpoint": "https://mcps-gateway.ami-app.workers.dev/v1/execute",
|
||||
"evidence_id": "evidence-af37a8d489b0038a7a6b5575",
|
||||
"http_status": 200,
|
||||
"method": "POST",
|
||||
"observed_at": "2026-05-02T02:17:12+00:00",
|
||||
"ok": true,
|
||||
"request_hash": "17e7d8039c8c34e3f570b6de8b386edc1cfd0c079084b0c7013016d2c76b388c",
|
||||
"response_excerpt": {
|
||||
"__truncated__": true,
|
||||
"actorId": "codex.service-order-round",
|
||||
"auditId": "audit:mcps-gateway:codex.service-order-round:mais_humana.admin_ui.same_source",
|
||||
"blockers": "[]",
|
||||
"consumption": "None",
|
||||
"nextActions": "[]",
|
||||
"ok": "True",
|
||||
"organizationId": "None",
|
||||
"productId": "None",
|
||||
"providerId": "mais_humana",
|
||||
"readiness": "None",
|
||||
"sampleData": "False",
|
||||
"simulated": "False",
|
||||
"status": "ok",
|
||||
"traceId": "trace:mcps-gateway:codex.service-order-round:mais_humana.admin_ui.same_source",
|
||||
"userId": "None",
|
||||
"workspaceId": "None"
|
||||
},
|
||||
"response_hash": "af37a8d489b0038a7a6b5575970ec69855dd0f0e0ab09cf38b0e7658d3678195",
|
||||
"tool_id": "mais_humana.admin_ui.same_source",
|
||||
"trace_id": "trace-17e7d8039c8c34e3f570b6de",
|
||||
"user_agent": "Codex-Mais-Humana-MCP-Publication-Gate/1.0"
|
||||
},
|
||||
{
|
||||
"audit_id": "audit-3f0e3b9f829c7ff912b335d0",
|
||||
"authorization_present": true,
|
||||
"authorization_redacted": true,
|
||||
"content_type": "application/json",
|
||||
"endpoint": "https://mcps-gateway.ami-app.workers.dev/v1/execute",
|
||||
"evidence_id": "evidence-3f0e3b9f829c7ff912b335d0",
|
||||
"http_status": 200,
|
||||
"method": "POST",
|
||||
"observed_at": "2026-05-02T02:17:12+00:00",
|
||||
"ok": true,
|
||||
"request_hash": "dae7d91a59e37901d50c027d3a0792f697902bd4289801edb2a508f3baf177fe",
|
||||
"response_excerpt": {
|
||||
"__truncated__": true,
|
||||
"actorId": "codex.service-order-round",
|
||||
"auditId": "audit:mcps-gateway:codex.service-order-round:mais_humana.mcp_transit.ledger",
|
||||
"blockers": "[]",
|
||||
"consumption": "None",
|
||||
"nextActions": "[]",
|
||||
"ok": "True",
|
||||
"organizationId": "None",
|
||||
"productId": "None",
|
||||
"providerId": "mais_humana",
|
||||
"readiness": "None",
|
||||
"sampleData": "False",
|
||||
"simulated": "False",
|
||||
"status": "ok",
|
||||
"traceId": "trace:mcps-gateway:codex.service-order-round:mais_humana.mcp_transit.ledger",
|
||||
"userId": "None",
|
||||
"workspaceId": "None"
|
||||
},
|
||||
"response_hash": "3f0e3b9f829c7ff912b335d01afb5e78acdaa331bd984713dfca757072be6bbf",
|
||||
"tool_id": "mais_humana.mcp_transit.ledger",
|
||||
"trace_id": "trace-dae7d91a59e37901d50c027d",
|
||||
"user_agent": "Codex-Mais-Humana-MCP-Publication-Gate/1.0"
|
||||
}
|
||||
],
|
||||
"rate_limit_per_minute": 30,
|
||||
"report_id": "mcp-gateway-access-policy-a787db3755906de2",
|
||||
"required_content_type": "application/json",
|
||||
"required_method": "POST",
|
||||
"required_user_agent": "Codex-Mais-Humana-MCP-Publication-Gate/1.0",
|
||||
"rules": [
|
||||
{
|
||||
"evidence_fields": [
|
||||
"method",
|
||||
"endpoint"
|
||||
],
|
||||
"failure_status": "blocked",
|
||||
"kind": "http",
|
||||
"owner": "tudo-para-ia-mcps-internos-plataform",
|
||||
"required": true,
|
||||
"requirement": "Toda chamada GPT/MCP deve usar POST em /v1/execute.",
|
||||
"rule_id": "http.method.post",
|
||||
"title": "Metodo HTTP fixo",
|
||||
"validation": "Comparar metodo observado com POST."
|
||||
},
|
||||
{
|
||||
"evidence_fields": [
|
||||
"content_type"
|
||||
],
|
||||
"failure_status": "blocked",
|
||||
"kind": "header",
|
||||
"owner": "tudo-para-ia-mcps-internos-plataform",
|
||||
"required": true,
|
||||
"requirement": "Toda chamada deve enviar Content-Type application/json.",
|
||||
"rule_id": "header.content-type.json",
|
||||
"title": "Content-Type JSON",
|
||||
"validation": "Comparar content_type observado."
|
||||
},
|
||||
{
|
||||
"evidence_fields": [
|
||||
"user_agent"
|
||||
],
|
||||
"failure_status": "partial",
|
||||
"kind": "header",
|
||||
"owner": "tudo-para-ia-mcps-internos-plataform",
|
||||
"required": true,
|
||||
"requirement": "Probes Codex devem usar User-Agent Codex-Mais-Humana-MCP-Publication-Gate/1.0.",
|
||||
"rule_id": "header.user-agent.codex",
|
||||
"title": "User-Agent operacional",
|
||||
"validation": "Comparar User-Agent observado para separar WAF de runtime."
|
||||
},
|
||||
{
|
||||
"evidence_fields": [
|
||||
"authorization_present",
|
||||
"authorization_redacted"
|
||||
],
|
||||
"failure_status": "blocked",
|
||||
"kind": "auth",
|
||||
"owner": "tudo-para-ia-mcps-internos-plataform",
|
||||
"required": true,
|
||||
"requirement": "Authorization Bearer pode ser usado no probe, mas relatorios devem guardar apenas existencia, hash e credentialRef.",
|
||||
"rule_id": "auth.bearer.present-redacted",
|
||||
"title": "Bearer presente e nunca persistido bruto",
|
||||
"validation": "Confirmar authorization_present e authorization_redacted."
|
||||
},
|
||||
{
|
||||
"evidence_fields": [
|
||||
"http_status",
|
||||
"response_excerpt"
|
||||
],
|
||||
"failure_status": "partial",
|
||||
"kind": "waf",
|
||||
"owner": "tudo-para-ia-mcps-internos-plataform",
|
||||
"required": true,
|
||||
"requirement": "HTTP 403/1010 e bloqueios WAF devem ser separados de tool_not_found, erro de runtime e erro de contrato.",
|
||||
"rule_id": "waf.classification.explicit",
|
||||
"title": "Classificacao WAF explicita",
|
||||
"validation": "Usar http_status e response_excerpt redigido para classificar falha."
|
||||
},
|
||||
{
|
||||
"evidence_fields": [
|
||||
"trace_id",
|
||||
"audit_id",
|
||||
"evidence_id"
|
||||
],
|
||||
"failure_status": "blocked",
|
||||
"kind": "evidence",
|
||||
"owner": "tudo-para-ia-mcps-internos-plataform",
|
||||
"required": true,
|
||||
"requirement": "Toda resposta aceita deve possuir traceId e auditId reais ou derivados de hash de evidencia.",
|
||||
"rule_id": "evidence.trace-audit-required",
|
||||
"title": "Trace e audit obrigatorios",
|
||||
"validation": "Confirmar trace_id e audit_id por probe."
|
||||
},
|
||||
{
|
||||
"evidence_fields": [
|
||||
"request_hash",
|
||||
"response_hash"
|
||||
],
|
||||
"failure_status": "blocked",
|
||||
"kind": "evidence",
|
||||
"owner": "tudo-para-ia-mcps-internos-plataform",
|
||||
"required": true,
|
||||
"requirement": "Toda evidencia deve guardar request_hash e response_hash sem payload sensivel bruto.",
|
||||
"rule_id": "evidence.hashes-required",
|
||||
"title": "Hashes de payload e resposta",
|
||||
"validation": "Confirmar hashes preenchidos por probe."
|
||||
},
|
||||
{
|
||||
"evidence_fields": [
|
||||
"response_excerpt"
|
||||
],
|
||||
"failure_status": "blocked",
|
||||
"kind": "redaction",
|
||||
"owner": "tudo-para-ia-mcps-internos-plataform",
|
||||
"required": true,
|
||||
"requirement": "Evidencias nao podem conter cfat_, Authorization Bearer cru, tokens longos ou bearer numerico bruto.",
|
||||
"rule_id": "redaction.no-secret-shapes",
|
||||
"title": "Sem segredo bruto em evidencia",
|
||||
"validation": "Varrer response_excerpt e campos textuais por formatos proibidos."
|
||||
},
|
||||
{
|
||||
"evidence_fields": [
|
||||
"rate_limit_per_minute"
|
||||
],
|
||||
"failure_status": "partial",
|
||||
"kind": "rate_limit",
|
||||
"owner": "tudo-para-ia-mcps-internos-plataform",
|
||||
"required": true,
|
||||
"requirement": "Probes automatizados devem respeitar limite padrao de 30 chamadas/minuto por ator.",
|
||||
"rule_id": "rate-limit.default",
|
||||
"title": "Limite operacional padrao",
|
||||
"validation": "Registrar limite no contrato e bloquear suites que excedam o teto."
|
||||
},
|
||||
{
|
||||
"evidence_fields": [
|
||||
"log_retention_days"
|
||||
],
|
||||
"failure_status": "partial",
|
||||
"kind": "retention",
|
||||
"owner": "tudo-para-ia-mcps-internos-plataform",
|
||||
"required": true,
|
||||
"requirement": "Logs de evidencia operacional devem reter metadados redigidos por 30 dias.",
|
||||
"rule_id": "retention.logs",
|
||||
"title": "Retencao de logs",
|
||||
"validation": "Registrar politica no artefato de acesso."
|
||||
},
|
||||
{
|
||||
"evidence_fields": [
|
||||
"origin",
|
||||
"destination",
|
||||
"tool",
|
||||
"payload",
|
||||
"actor",
|
||||
"permission",
|
||||
"result",
|
||||
"traceId",
|
||||
"auditId",
|
||||
"timestamp"
|
||||
],
|
||||
"failure_status": "blocked",
|
||||
"kind": "transit",
|
||||
"owner": "tudo-para-ia-mcps-internos-plataform",
|
||||
"required": true,
|
||||
"requirement": "Fluxos interplataforma devem preservar origin, destination, tool, payload, actor, permission, result, traceId, auditId e timestamp.",
|
||||
"rule_id": "transit.required-fields",
|
||||
"title": "Ledger MCP obrigatorio",
|
||||
"validation": "Validar campos exigidos no contrato de transito MCP."
|
||||
},
|
||||
{
|
||||
"evidence_fields": [
|
||||
"policy_version"
|
||||
],
|
||||
"failure_status": "partial",
|
||||
"kind": "governance",
|
||||
"owner": "tudo-para-ia-mcps-internos-plataform",
|
||||
"required": true,
|
||||
"requirement": "Falha ou aceite do plugin Cloudflare fica fora do diagnostico de Workers; trabalho real usa wrangler ou validacao HTTP live.",
|
||||
"rule_id": "governance.plugin-not-operational-path",
|
||||
"title": "Plugin Cloudflare nao substitui caminho operacional",
|
||||
"validation": "Confirmar que o artefato nao transforma plugin em blocker operacional."
|
||||
}
|
||||
],
|
||||
"secretSafe": true,
|
||||
"status": "passed",
|
||||
"summary": [
|
||||
"Probes live avaliados: 3.",
|
||||
"Probes live OK: 3/3.",
|
||||
"Regras aprovadas: 12/12.",
|
||||
"Bearer bruto persistido: False.",
|
||||
"Falha do plugin Cloudflare nao e blocker operacional: True."
|
||||
]
|
||||
}
|
||||
@@ -8,7 +8,7 @@
|
||||
"path": "G:\\_codex-git\\nucleo-gestao-operacional\\central-de-ordem-de-servico\\projects\\15_repo_tudo-para-ia-mais-humana-platform\\reports\\executivos\\MCP-PUBLICATION-GATE-MAIS-HUMANA__RODADA015.md"
|
||||
}
|
||||
],
|
||||
"generatedAt": "2026-05-02T02:13:32+00:00",
|
||||
"generatedAt": "2026-05-02T02:17:12+00:00",
|
||||
"ok": false,
|
||||
"policy": "falha de escrita central nao aborta artefatos do projeto real"
|
||||
}
|
||||
@@ -149,7 +149,7 @@
|
||||
"mais_humana.mcp_transit.ledger"
|
||||
]
|
||||
},
|
||||
"generated_at": "2026-05-02T02:13:32+00:00",
|
||||
"generated_at": "2026-05-02T02:17:12+00:00",
|
||||
"liveReady": true,
|
||||
"live_probes": [
|
||||
{
|
||||
@@ -158,7 +158,7 @@
|
||||
"error_code": "",
|
||||
"evidence_id": "evidence-a75a27e0669c49da1db8b615",
|
||||
"http_status": 200,
|
||||
"observed_at": "2026-05-02T02:13:32+00:00",
|
||||
"observed_at": "2026-05-02T02:17:12+00:00",
|
||||
"ok": true,
|
||||
"response_excerpt": {
|
||||
"__truncated__": true,
|
||||
@@ -191,7 +191,7 @@
|
||||
"error_code": "",
|
||||
"evidence_id": "evidence-af37a8d489b0038a7a6b5575",
|
||||
"http_status": 200,
|
||||
"observed_at": "2026-05-02T02:13:32+00:00",
|
||||
"observed_at": "2026-05-02T02:17:12+00:00",
|
||||
"ok": true,
|
||||
"response_excerpt": {
|
||||
"__truncated__": true,
|
||||
@@ -224,7 +224,7 @@
|
||||
"error_code": "",
|
||||
"evidence_id": "evidence-3f0e3b9f829c7ff912b335d0",
|
||||
"http_status": 200,
|
||||
"observed_at": "2026-05-02T02:13:32+00:00",
|
||||
"observed_at": "2026-05-02T02:17:12+00:00",
|
||||
"ok": true,
|
||||
"response_excerpt": {
|
||||
"__truncated__": true,
|
||||
@@ -254,7 +254,7 @@
|
||||
],
|
||||
"localReady": true,
|
||||
"provider_id": "mais_humana",
|
||||
"report_id": "mcp-publication-gate-2026-05-02t0213320000",
|
||||
"report_id": "mcp-publication-gate-2026-05-02t0217120000",
|
||||
"status": "partial",
|
||||
"summary": [
|
||||
"Provider local Mais Humana pronto: True.",
|
||||
|
||||
173
ecossistema/MCP-GATEWAY-ACCESS-POLICY.md
Normal file
173
ecossistema/MCP-GATEWAY-ACCESS-POLICY.md
Normal file
@@ -0,0 +1,173 @@
|
||||
# Politica de acesso GPT/MCP Gateway
|
||||
|
||||
- report_id: `mcp-gateway-access-policy-a787db3755906de2`
|
||||
- generated_at: `2026-05-02T02:17:13+00:00`
|
||||
- policy_version: `mcp-gateway-access-policy.v1`
|
||||
- endpoint: `https://mcps-gateway.ami-app.workers.dev/v1/execute`
|
||||
- status: `passed`
|
||||
- live_ready: `True`
|
||||
- secret_safe: `True`
|
||||
- method: `POST`
|
||||
- content_type: `application/json`
|
||||
- user_agent: `Codex-Mais-Humana-MCP-Publication-Gate/1.0`
|
||||
- auth_scheme: `Bearer credentialRef; raw token forbidden in artifacts`
|
||||
- rate_limit_per_minute: `30`
|
||||
- log_retention_days: `30`
|
||||
|
||||
## Sumario
|
||||
|
||||
- Probes live avaliados: 3.
|
||||
- Probes live OK: 3/3.
|
||||
- Regras aprovadas: 12/12.
|
||||
- Bearer bruto persistido: False.
|
||||
- Falha do plugin Cloudflare nao e blocker operacional: True.
|
||||
|
||||
## Regras
|
||||
|
||||
### http.method.post
|
||||
|
||||
- kind: `http`
|
||||
- required: `True`
|
||||
- requisito: Toda chamada GPT/MCP deve usar POST em /v1/execute.
|
||||
- validacao: Comparar metodo observado com POST.
|
||||
|
||||
### header.content-type.json
|
||||
|
||||
- kind: `header`
|
||||
- required: `True`
|
||||
- requisito: Toda chamada deve enviar Content-Type application/json.
|
||||
- validacao: Comparar content_type observado.
|
||||
|
||||
### header.user-agent.codex
|
||||
|
||||
- kind: `header`
|
||||
- required: `True`
|
||||
- requisito: Probes Codex devem usar User-Agent Codex-Mais-Humana-MCP-Publication-Gate/1.0.
|
||||
- validacao: Comparar User-Agent observado para separar WAF de runtime.
|
||||
|
||||
### auth.bearer.present-redacted
|
||||
|
||||
- kind: `auth`
|
||||
- required: `True`
|
||||
- requisito: Authorization Bearer pode ser usado no probe, mas relatorios devem guardar apenas existencia, hash e credentialRef.
|
||||
- validacao: Confirmar authorization_present e authorization_redacted.
|
||||
|
||||
### waf.classification.explicit
|
||||
|
||||
- kind: `waf`
|
||||
- required: `True`
|
||||
- requisito: HTTP 403/1010 e bloqueios WAF devem ser separados de tool_not_found, erro de runtime e erro de contrato.
|
||||
- validacao: Usar http_status e response_excerpt redigido para classificar falha.
|
||||
|
||||
### evidence.trace-audit-required
|
||||
|
||||
- kind: `evidence`
|
||||
- required: `True`
|
||||
- requisito: Toda resposta aceita deve possuir traceId e auditId reais ou derivados de hash de evidencia.
|
||||
- validacao: Confirmar trace_id e audit_id por probe.
|
||||
|
||||
### evidence.hashes-required
|
||||
|
||||
- kind: `evidence`
|
||||
- required: `True`
|
||||
- requisito: Toda evidencia deve guardar request_hash e response_hash sem payload sensivel bruto.
|
||||
- validacao: Confirmar hashes preenchidos por probe.
|
||||
|
||||
### redaction.no-secret-shapes
|
||||
|
||||
- kind: `redaction`
|
||||
- required: `True`
|
||||
- requisito: Evidencias nao podem conter cfat_, Authorization Bearer cru, tokens longos ou bearer numerico bruto.
|
||||
- validacao: Varrer response_excerpt e campos textuais por formatos proibidos.
|
||||
|
||||
### rate-limit.default
|
||||
|
||||
- kind: `rate_limit`
|
||||
- required: `True`
|
||||
- requisito: Probes automatizados devem respeitar limite padrao de 30 chamadas/minuto por ator.
|
||||
- validacao: Registrar limite no contrato e bloquear suites que excedam o teto.
|
||||
|
||||
### retention.logs
|
||||
|
||||
- kind: `retention`
|
||||
- required: `True`
|
||||
- requisito: Logs de evidencia operacional devem reter metadados redigidos por 30 dias.
|
||||
- validacao: Registrar politica no artefato de acesso.
|
||||
|
||||
### transit.required-fields
|
||||
|
||||
- kind: `transit`
|
||||
- required: `True`
|
||||
- requisito: Fluxos interplataforma devem preservar origin, destination, tool, payload, actor, permission, result, traceId, auditId e timestamp.
|
||||
- validacao: Validar campos exigidos no contrato de transito MCP.
|
||||
|
||||
### governance.plugin-not-operational-path
|
||||
|
||||
- kind: `governance`
|
||||
- required: `True`
|
||||
- requisito: Falha ou aceite do plugin Cloudflare fica fora do diagnostico de Workers; trabalho real usa wrangler ou validacao HTTP live.
|
||||
- validacao: Confirmar que o artefato nao transforma plugin em blocker operacional.
|
||||
|
||||
## Probes
|
||||
|
||||
- `mais_humana.rulebook.compact` http `200` ok `True`
|
||||
- evidenceId: `evidence-a75a27e0669c49da1db8b615`
|
||||
- traceId: `trace-3e1c8f057ac439f4b9b3eb7f`
|
||||
- auditId: `audit-a75a27e0669c49da1db8b615`
|
||||
- requestHash: `3e1c8f057ac439f4b9b3eb7f8f5be9ac36323f08adc23db6fc7d51633076b79a`
|
||||
- responseHash: `a75a27e0669c49da1db8b6157757c0615eed06c32674c7ed87a6db5d071359de`
|
||||
- `mais_humana.admin_ui.same_source` http `200` ok `True`
|
||||
- evidenceId: `evidence-af37a8d489b0038a7a6b5575`
|
||||
- traceId: `trace-17e7d8039c8c34e3f570b6de`
|
||||
- auditId: `audit-af37a8d489b0038a7a6b5575`
|
||||
- requestHash: `17e7d8039c8c34e3f570b6de8b386edc1cfd0c079084b0c7013016d2c76b388c`
|
||||
- responseHash: `af37a8d489b0038a7a6b5575970ec69855dd0f0e0ab09cf38b0e7658d3678195`
|
||||
- `mais_humana.mcp_transit.ledger` http `200` ok `True`
|
||||
- evidenceId: `evidence-3f0e3b9f829c7ff912b335d0`
|
||||
- traceId: `trace-dae7d91a59e37901d50c027d`
|
||||
- auditId: `audit-3f0e3b9f829c7ff912b335d0`
|
||||
- requestHash: `dae7d91a59e37901d50c027d3a0792f697902bd4289801edb2a508f3baf177fe`
|
||||
- responseHash: `3f0e3b9f829c7ff912b335d01afb5e78acdaa331bd984713dfca757072be6bbf`
|
||||
|
||||
## Checks
|
||||
|
||||
- `http.method.post`: `passed`
|
||||
- motivo: todos os probes usaram POST
|
||||
- proxima_acao: manter regra como gate de release
|
||||
- `header.content-type.json`: `passed`
|
||||
- motivo: todos os probes usaram application/json
|
||||
- proxima_acao: manter regra como gate de release
|
||||
- `header.user-agent.codex`: `passed`
|
||||
- motivo: User-Agent operacional aplicado
|
||||
- proxima_acao: manter regra como gate de release
|
||||
- `auth.bearer.present-redacted`: `passed`
|
||||
- motivo: bearer usado como credencial de probe e redigido nos artefatos
|
||||
- proxima_acao: manter regra como gate de release
|
||||
- `waf.classification.explicit`: `passed`
|
||||
- motivo: WAF nao bloqueou os probes atuais; HTTP/runtime classificados separadamente
|
||||
- proxima_acao: manter regra como gate de release
|
||||
- `evidence.trace-audit-required`: `passed`
|
||||
- motivo: traceId e auditId presentes em todos os probes
|
||||
- proxima_acao: manter regra como gate de release
|
||||
- `evidence.hashes-required`: `passed`
|
||||
- motivo: hashes de request/response presentes
|
||||
- proxima_acao: manter regra como gate de release
|
||||
- `redaction.no-secret-shapes`: `passed`
|
||||
- motivo: nenhum formato de segredo bruto detectado nas evidencias
|
||||
- proxima_acao: manter regra como gate de release
|
||||
- `rate-limit.default`: `passed`
|
||||
- motivo: regra institucional materializada no artefato de politica
|
||||
- proxima_acao: manter regra como gate de release
|
||||
- `retention.logs`: `passed`
|
||||
- motivo: regra institucional materializada no artefato de politica
|
||||
- proxima_acao: manter regra como gate de release
|
||||
- `transit.required-fields`: `passed`
|
||||
- motivo: regra institucional materializada no artefato de politica
|
||||
- proxima_acao: manter regra como gate de release
|
||||
- `governance.plugin-not-operational-path`: `passed`
|
||||
- motivo: regra institucional materializada no artefato de politica
|
||||
- proxima_acao: manter regra como gate de release
|
||||
|
||||
## Blockers
|
||||
|
||||
- Nenhum blocker tecnico na politica local.
|
||||
@@ -1,7 +1,7 @@
|
||||
# Gate de publicacao MCP Mais Humana
|
||||
|
||||
- report_id: `mcp-publication-gate-2026-05-02t0213320000`
|
||||
- generated_at: `2026-05-02T02:13:32+00:00`
|
||||
- report_id: `mcp-publication-gate-2026-05-02t0217120000`
|
||||
- generated_at: `2026-05-02T02:17:12+00:00`
|
||||
- provider_id: `mais_humana`
|
||||
- current_project_id: `tudo-para-ia-mais-humana`
|
||||
- canonical_project_id: `tudo-para-ia-mais-humana-platform`
|
||||
|
||||
13
matrizes/mcp-gateway-access-policy.csv
Normal file
13
matrizes/mcp-gateway-access-policy.csv
Normal file
@@ -0,0 +1,13 @@
|
||||
rule_id,kind,status,required,reason,next_action,evidence_refs
|
||||
http.method.post,http,passed,yes,todos os probes usaram POST,manter regra como gate de release,evidence-a75a27e0669c49da1db8b615; evidence-af37a8d489b0038a7a6b5575; evidence-3f0e3b9f829c7ff912b335d0
|
||||
header.content-type.json,header,passed,yes,todos os probes usaram application/json,manter regra como gate de release,evidence-a75a27e0669c49da1db8b615; evidence-af37a8d489b0038a7a6b5575; evidence-3f0e3b9f829c7ff912b335d0
|
||||
header.user-agent.codex,header,passed,yes,User-Agent operacional aplicado,manter regra como gate de release,evidence-a75a27e0669c49da1db8b615; evidence-af37a8d489b0038a7a6b5575; evidence-3f0e3b9f829c7ff912b335d0
|
||||
auth.bearer.present-redacted,auth,passed,yes,bearer usado como credencial de probe e redigido nos artefatos,manter regra como gate de release,evidence-a75a27e0669c49da1db8b615; evidence-af37a8d489b0038a7a6b5575; evidence-3f0e3b9f829c7ff912b335d0
|
||||
waf.classification.explicit,waf,passed,yes,WAF nao bloqueou os probes atuais; HTTP/runtime classificados separadamente,manter regra como gate de release,evidence-a75a27e0669c49da1db8b615; evidence-af37a8d489b0038a7a6b5575; evidence-3f0e3b9f829c7ff912b335d0
|
||||
evidence.trace-audit-required,evidence,passed,yes,traceId e auditId presentes em todos os probes,manter regra como gate de release,evidence-a75a27e0669c49da1db8b615; evidence-af37a8d489b0038a7a6b5575; evidence-3f0e3b9f829c7ff912b335d0
|
||||
evidence.hashes-required,evidence,passed,yes,hashes de request/response presentes,manter regra como gate de release,evidence-a75a27e0669c49da1db8b615; evidence-af37a8d489b0038a7a6b5575; evidence-3f0e3b9f829c7ff912b335d0
|
||||
redaction.no-secret-shapes,redaction,passed,yes,nenhum formato de segredo bruto detectado nas evidencias,manter regra como gate de release,evidence-a75a27e0669c49da1db8b615; evidence-af37a8d489b0038a7a6b5575; evidence-3f0e3b9f829c7ff912b335d0
|
||||
rate-limit.default,rate_limit,passed,yes,regra institucional materializada no artefato de politica,manter regra como gate de release,evidence-a75a27e0669c49da1db8b615; evidence-af37a8d489b0038a7a6b5575; evidence-3f0e3b9f829c7ff912b335d0
|
||||
retention.logs,retention,passed,yes,regra institucional materializada no artefato de politica,manter regra como gate de release,evidence-a75a27e0669c49da1db8b615; evidence-af37a8d489b0038a7a6b5575; evidence-3f0e3b9f829c7ff912b335d0
|
||||
transit.required-fields,transit,passed,yes,regra institucional materializada no artefato de politica,manter regra como gate de release,evidence-a75a27e0669c49da1db8b615; evidence-af37a8d489b0038a7a6b5575; evidence-3f0e3b9f829c7ff912b335d0
|
||||
governance.plugin-not-operational-path,governance,passed,yes,regra institucional materializada no artefato de politica,manter regra como gate de release,evidence-a75a27e0669c49da1db8b615; evidence-af37a8d489b0038a7a6b5575; evidence-3f0e3b9f829c7ff912b335d0
|
||||
|
@@ -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,16 +828,14 @@ 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(
|
||||
central_record = GeneratedFile(
|
||||
path=str(central_path),
|
||||
description="Copia central do gate de publicacao MCP Mais Humana.",
|
||||
function="mcp publication gate central",
|
||||
@@ -846,11 +844,13 @@ def write_publication_gate_artifacts(
|
||||
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(
|
||||
|
||||
124
tests/test_mcp_gateway_access_policy.py
Normal file
124
tests/test_mcp_gateway_access_policy.py
Normal file
@@ -0,0 +1,124 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import unittest
|
||||
|
||||
from mais_humana.cli import main
|
||||
from mais_humana.mcp_gateway_access_policy import (
|
||||
AccessPolicyStatus,
|
||||
build_access_policy_report,
|
||||
has_secret_shape,
|
||||
probes_from_publication_gate_payload,
|
||||
run_access_policy_gate,
|
||||
)
|
||||
from tests.helpers import make_tmp
|
||||
|
||||
|
||||
def publication_gate_payload() -> dict[str, object]:
|
||||
return {
|
||||
"report": {
|
||||
"live_probes": [
|
||||
{
|
||||
"tool_id": "mais_humana.rulebook.compact",
|
||||
"endpoint": "https://mcps-gateway.ami-app.workers.dev/v1/execute",
|
||||
"http_status": 200,
|
||||
"ok": True,
|
||||
"trace_id": "trace:mcps-gateway:actor:mais_humana.rulebook.compact",
|
||||
"audit_id": "audit:mcps-gateway:actor:mais_humana.rulebook.compact",
|
||||
"evidence_id": "evidence-rulebook",
|
||||
"source_payload_hash": "hash-request-rulebook",
|
||||
"source_records_hash": "hash-response-rulebook",
|
||||
"response_excerpt": {"ok": "True", "providerId": "mais_humana"},
|
||||
"observed_at": "2026-05-02T00:00:00+00:00",
|
||||
},
|
||||
{
|
||||
"tool_id": "mais_humana.admin_ui.same_source",
|
||||
"endpoint": "https://mcps-gateway.ami-app.workers.dev/v1/execute",
|
||||
"http_status": 200,
|
||||
"ok": True,
|
||||
"trace_id": "trace:mcps-gateway:actor:mais_humana.admin_ui.same_source",
|
||||
"audit_id": "audit:mcps-gateway:actor:mais_humana.admin_ui.same_source",
|
||||
"evidence_id": "evidence-same-source",
|
||||
"source_payload_hash": "hash-request-same-source",
|
||||
"source_records_hash": "hash-response-same-source",
|
||||
"response_excerpt": {"ok": "True", "sameSource": "True"},
|
||||
"observed_at": "2026-05-02T00:00:00+00:00",
|
||||
},
|
||||
{
|
||||
"tool_id": "mais_humana.mcp_transit.ledger",
|
||||
"endpoint": "https://mcps-gateway.ami-app.workers.dev/v1/execute",
|
||||
"http_status": 200,
|
||||
"ok": True,
|
||||
"trace_id": "trace:mcps-gateway:actor:mais_humana.mcp_transit.ledger",
|
||||
"audit_id": "audit:mcps-gateway:actor:mais_humana.mcp_transit.ledger",
|
||||
"evidence_id": "evidence-ledger",
|
||||
"source_payload_hash": "hash-request-ledger",
|
||||
"source_records_hash": "hash-response-ledger",
|
||||
"response_excerpt": {"ok": "True", "records": "3"},
|
||||
"observed_at": "2026-05-02T00:00:00+00:00",
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class McpGatewayAccessPolicyTests(unittest.TestCase):
|
||||
def test_policy_from_publication_gate_payload_passes_without_secret_leak(self) -> None:
|
||||
probes = probes_from_publication_gate_payload(publication_gate_payload())
|
||||
report = build_access_policy_report(probes=probes)
|
||||
self.assertEqual(len(report.probes), 3)
|
||||
self.assertEqual(report.status, AccessPolicyStatus.PASSED)
|
||||
self.assertTrue(report.live_ready)
|
||||
self.assertTrue(report.secret_safe)
|
||||
self.assertFalse(report.blockers)
|
||||
by_rule = {check.rule_id: check for check in report.checks}
|
||||
self.assertEqual(by_rule["auth.bearer.present-redacted"].status, AccessPolicyStatus.PASSED)
|
||||
self.assertEqual(by_rule["redaction.no-secret-shapes"].status, AccessPolicyStatus.PASSED)
|
||||
|
||||
def test_secret_shapes_block_redaction_rule(self) -> None:
|
||||
self.assertTrue(has_secret_shape("Authorization: Bearer rawtoken123456"))
|
||||
self.assertTrue(has_secret_shape("cfat_abc123"))
|
||||
payload = publication_gate_payload()
|
||||
live_probes = payload["report"]["live_probes"] # type: ignore[index]
|
||||
live_probes[0]["response_excerpt"] = {"authorization": "Bearer rawtoken123456"} # type: ignore[index]
|
||||
report = build_access_policy_report(probes=probes_from_publication_gate_payload(payload))
|
||||
by_rule = {check.rule_id: check for check in report.checks}
|
||||
self.assertEqual(by_rule["redaction.no-secret-shapes"].status, AccessPolicyStatus.BLOCKED)
|
||||
|
||||
def test_run_access_policy_gate_writes_project_and_central_artifacts(self) -> None:
|
||||
tmp = make_tmp()
|
||||
project = tmp / "tudo-para-ia-mais-humana"
|
||||
central = tmp / "central" / "projects" / "15_repo_tudo-para-ia-mais-humana-platform"
|
||||
gate_json = project / "dados" / "mcp-publication-gate-mais-humana.json"
|
||||
gate_json.parent.mkdir(parents=True, exist_ok=True)
|
||||
gate_json.write_text(json.dumps(publication_gate_payload()), encoding="utf-8")
|
||||
report, records = run_access_policy_gate(project_root=project, central_platform_folder=central, publication_gate_json=gate_json)
|
||||
self.assertEqual(report.status, AccessPolicyStatus.PASSED)
|
||||
self.assertTrue((project / "dados" / "mcp-gateway-access-policy.json").exists())
|
||||
self.assertTrue((project / "matrizes" / "mcp-gateway-access-policy.csv").exists())
|
||||
self.assertTrue((central / "reports" / "MCP-GATEWAY-ACCESS-POLICY__RODADA015.md").exists())
|
||||
self.assertGreaterEqual(len(records), 4)
|
||||
|
||||
def test_cli_access_policy_writes_payload(self) -> None:
|
||||
tmp = make_tmp()
|
||||
project = tmp / "tudo-para-ia-mais-humana"
|
||||
gate_json = project / "dados" / "mcp-publication-gate-mais-humana.json"
|
||||
gate_json.parent.mkdir(parents=True, exist_ok=True)
|
||||
gate_json.write_text(json.dumps(publication_gate_payload()), encoding="utf-8")
|
||||
code = main(
|
||||
[
|
||||
"mcp-access-policy",
|
||||
"--project-root",
|
||||
str(project),
|
||||
"--publication-gate-json",
|
||||
str(gate_json),
|
||||
]
|
||||
)
|
||||
self.assertEqual(code, 0)
|
||||
payload = json.loads((project / "dados" / "mcp-gateway-access-policy.json").read_text(encoding="utf-8"))
|
||||
self.assertEqual(payload["status"], "passed")
|
||||
self.assertTrue(payload["secretSafe"])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -113,6 +113,15 @@ class McpProviderContractTests(unittest.TestCase):
|
||||
self.assertGreater(len(ui_screens), 20)
|
||||
self.assertTrue(all(contract.report_model_id for contract in report_models))
|
||||
|
||||
def test_access_policy_contracts_cover_profiles_and_gateway_rules(self) -> None:
|
||||
access_contracts = contracts_for_kind(McpContractKind.ACCESS_POLICY)
|
||||
self.assertGreater(len(access_contracts), 500)
|
||||
sample = access_contracts[0]
|
||||
self.assertIn("authorizationCredentialRef", sample.required_payload_fields)
|
||||
self.assertIn("pluginCloudflareDiagnosticIgnored", sample.required_payload_fields)
|
||||
self.assertTrue(any("WAF" in step or "waf" in step for step in sample.validation_steps))
|
||||
self.assertTrue(all("0045_GERENCIAL__pactuar-politica-acesso-waf-gpt-mcp-gateway" in item.order_ids for item in access_contracts))
|
||||
|
||||
def test_cli_mcp_provider_returns_json(self) -> None:
|
||||
root = make_tmp()
|
||||
self.make_repo(
|
||||
|
||||
@@ -141,6 +141,48 @@ def redaction_requirements(platform_id: str) -> tuple[str, ...]:
|
||||
)
|
||||
|
||||
|
||||
ACCESS_POLICY_SURFACES = (
|
||||
("gpt-execute-probe", "governance", "mais_humana.gateway.access_policy.gpt_probe"),
|
||||
("admin-ui-render", "experience", "mais_humana.gateway.access_policy.admin_ui"),
|
||||
("automation-smoke", "observability", "mais_humana.gateway.access_policy.smoke"),
|
||||
)
|
||||
|
||||
|
||||
def unique_tuple(values: Iterable[object]) -> tuple[str, ...]:
|
||||
seen: set[str] = set()
|
||||
output: list[str] = []
|
||||
for value in values:
|
||||
text = str(value)
|
||||
if not text or text in seen:
|
||||
continue
|
||||
seen.add(text)
|
||||
output.append(text)
|
||||
return tuple(output)
|
||||
|
||||
|
||||
def access_policy_payload_fields(platform_id: str, profile_id: str, surface: str, category: str) -> tuple[str, ...]:
|
||||
return unique_tuple(
|
||||
payload_fields(platform_id, profile_id, surface, category)
|
||||
+ (
|
||||
"httpMethod",
|
||||
"contentType",
|
||||
"userAgent",
|
||||
"authorizationCredentialRef",
|
||||
"authorizationRawPersisted",
|
||||
"wafDecision",
|
||||
"wafRuleId",
|
||||
"rateLimitPerMinute",
|
||||
"logRetentionDays",
|
||||
"requestHash",
|
||||
"responseHash",
|
||||
"redactionPolicyId",
|
||||
"secretSafe",
|
||||
"pluginCloudflareDiagnosticIgnored",
|
||||
"wranglerOperationalReference",
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def validation_steps(platform_id: str, profile_id: str, surface: str, kind: str) -> tuple[str, ...]:
|
||||
return (
|
||||
f"chamar {platform_id} somente via tudo-para-ia-mcps-internos-plataform",
|
||||
@@ -152,6 +194,17 @@ def validation_steps(platform_id: str, profile_id: str, surface: str, kind: str)
|
||||
)
|
||||
|
||||
|
||||
def access_policy_validations(platform_id: str, profile_id: str, surface: str) -> tuple[str, ...]:
|
||||
return (
|
||||
f"executar {surface} de {platform_id}/{profile_id} por /v1/execute com POST application/json",
|
||||
"confirmar User-Agent operacional e separar WAF de erro runtime",
|
||||
"confirmar Authorization via credentialRef sem persistir bearer bruto",
|
||||
"validar traceId, auditId, requestHash e responseHash",
|
||||
"validar redaction contra cfat_, bearer bruto e tokens numericos longos",
|
||||
"registrar rate limit, retencao de logs e decisao WAF no MCP",
|
||||
)
|
||||
|
||||
|
||||
def contract_block(
|
||||
name: str,
|
||||
*,
|
||||
@@ -398,6 +451,50 @@ def build_contracts() -> tuple[list[str], list[str]]:
|
||||
)
|
||||
index += 1
|
||||
|
||||
for profile in HUMAN_PROFILES:
|
||||
profile_categories = category_values(profile.priority_needs)
|
||||
audience = audience_for_profile(profile.profile_id, profile_categories)
|
||||
for surface, category, tool_id in ACCESS_POLICY_SURFACES:
|
||||
name = f"CONTRACT_{index:04d}"
|
||||
names.append(name)
|
||||
blocks.append(
|
||||
contract_block(
|
||||
name,
|
||||
contract_id=f"{platform.platform_id}.{profile.profile_id}.{surface}.access-policy",
|
||||
kind="ACCESS_POLICY",
|
||||
platform_id=platform.platform_id,
|
||||
profile_id=profile.profile_id,
|
||||
tool_id=tool_id,
|
||||
title=f"Politica de acesso {surface} para {platform.title} e {profile.name}",
|
||||
purpose=(
|
||||
f"Garantir que chamadas GPT/MCP de {platform.title} para {profile.name} "
|
||||
"usem headers, bearer redigido, WAF classificado, hashes e ledger auditavel."
|
||||
),
|
||||
source_tool_id="mais_humana.gateway.access_policy",
|
||||
payload=access_policy_payload_fields(platform.platform_id, profile.profile_id, surface, category),
|
||||
truth=truth,
|
||||
panel_ready=panel_ready,
|
||||
gpt_explainable=True,
|
||||
report_model_id=f"access.{platform.platform_id}.{profile.profile_id}.{normalize(surface)}",
|
||||
audience=audience,
|
||||
redaction=redaction_requirements(platform.platform_id)
|
||||
+ (
|
||||
"bloquear persistencia de Authorization Bearer bruto",
|
||||
"registrar apenas credentialRef e hashes de evidencia",
|
||||
),
|
||||
validations=access_policy_validations(platform.platform_id, profile.profile_id, surface),
|
||||
pending=f"homologar politica de acesso {surface} para {platform.platform_id}/{profile.profile_id}",
|
||||
order_ids=(
|
||||
"0032_EXECUTIVA__validar-live-tools-mais-humana-v1-execute-com-evidencia",
|
||||
"0045_GERENCIAL__pactuar-politica-acesso-waf-gpt-mcp-gateway",
|
||||
),
|
||||
policy_tags=("access_policy", "waf", "redaction", "same_source", normalize(surface)),
|
||||
maturity=max(7, maturity),
|
||||
generated_from="platform_profile_access_policy_contract",
|
||||
)
|
||||
)
|
||||
index += 1
|
||||
|
||||
special_contracts = [
|
||||
(
|
||||
"docs.formal-exception.docs-catalogonly",
|
||||
|
||||
Reference in New Issue
Block a user