luma-audit 0.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- luma_audit-0.3.0/LICENSE +21 -0
- luma_audit-0.3.0/PKG-INFO +102 -0
- luma_audit-0.3.0/README.md +80 -0
- luma_audit-0.3.0/pyproject.toml +38 -0
- luma_audit-0.3.0/setup.cfg +4 -0
- luma_audit-0.3.0/src/luma_audit/__init__.py +27 -0
- luma_audit-0.3.0/src/luma_audit/auditor.py +55 -0
- luma_audit-0.3.0/src/luma_audit/canal.py +98 -0
- luma_audit-0.3.0/src/luma_audit/cli.py +64 -0
- luma_audit-0.3.0/src/luma_audit/funil.py +102 -0
- luma_audit-0.3.0/src/luma_audit/skills.py +70 -0
- luma_audit-0.3.0/src/luma_audit/trajetoria.py +243 -0
- luma_audit-0.3.0/src/luma_audit/zbit.py +79 -0
- luma_audit-0.3.0/src/luma_audit.egg-info/PKG-INFO +102 -0
- luma_audit-0.3.0/src/luma_audit.egg-info/SOURCES.txt +16 -0
- luma_audit-0.3.0/src/luma_audit.egg-info/dependency_links.txt +1 -0
- luma_audit-0.3.0/src/luma_audit.egg-info/entry_points.txt +2 -0
- luma_audit-0.3.0/src/luma_audit.egg-info/top_level.txt +1 -0
luma_audit-0.3.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Alesson Abreu
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: luma-audit
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: Deterministic governance for AI actions. No LLM in the decision path.
|
|
5
|
+
Author-email: Alesson Abreu <abreu.tech.contabilidade@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/abreutechcontabilidade-web/luma-audit
|
|
8
|
+
Project-URL: Issues, https://github.com/abreutechcontabilidade-web/luma-audit/issues
|
|
9
|
+
Keywords: ai-safety,guardrails,execution-control,deterministic,audit
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Topic :: Security
|
|
17
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
18
|
+
Requires-Python: >=3.10
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
License-File: LICENSE
|
|
21
|
+
Dynamic: license-file
|
|
22
|
+
|
|
23
|
+
# luma-audit
|
|
24
|
+
|
|
25
|
+
> **Deterministic governance for AI actions. No LLM in the decision path.**
|
|
26
|
+
|
|
27
|
+
[](https://www.python.org/downloads/)
|
|
28
|
+
[](https://opensource.org/licenses/MIT)
|
|
29
|
+
[](#)
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## What it does
|
|
34
|
+
|
|
35
|
+
`luma-audit` is a deterministic execution-control layer that **approves or rejects AI actions before they happen**.
|
|
36
|
+
|
|
37
|
+
- **Deterministic** - same input, same output. Always.
|
|
38
|
+
- **Outside the model** - a layer the LLM cannot bypass.
|
|
39
|
+
- **Auditable** - every decision is hash-chained.
|
|
40
|
+
|
|
41
|
+
It does **not** filter text, detect toxicity, or replace authentication. It audits **actions**.
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## Install
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
pip install luma-audit
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## 30-second example
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
from luma_audit import LUMA
|
|
57
|
+
|
|
58
|
+
auditor = LUMA()
|
|
59
|
+
auditor.avaliar('ler_ficheiro') # -> aprovada
|
|
60
|
+
auditor.avaliar('apagar_ficheiro') # -> rejeitada
|
|
61
|
+
auditor.avaliar('acao_desconhecida') # -> rejeitada (unknown = high risk)
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## What it does NOT do
|
|
67
|
+
|
|
68
|
+
- Does not filter text (that is a prompt guardrail - different problem)
|
|
69
|
+
- Does not detect toxicity
|
|
70
|
+
- Does not replace authentication
|
|
71
|
+
- Does not use any LLM in the decision path
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## Architecture
|
|
76
|
+
|
|
77
|
+
```
|
|
78
|
+
src/luma_audit/
|
|
79
|
+
auditor.py # LUMA class + Veredito enum
|
|
80
|
+
trajetoria.py # Sessao, AuditorTrajetoria, Decisao, hash chain
|
|
81
|
+
canal.py # CanalUnico, MensagemFunil
|
|
82
|
+
funil.py # FunilUnico, FILA_DIR
|
|
83
|
+
cli.py # luma CLI entrypoint
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Tests
|
|
87
|
+
|
|
88
|
+
22 red-team tests: hash chain integrity + tampering, quarantine escalation, flood detection, atomic writes, corrupted proposal rejection, executor revalidation.
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
## Philosophy
|
|
93
|
+
|
|
94
|
+
Guardrails based on prompts compete against the model's weights - and lose. The only layer that cannot be bypassed is the one running **outside the model**, at the point of execution.
|
|
95
|
+
|
|
96
|
+
`luma-audit` is that layer.
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
## License
|
|
101
|
+
|
|
102
|
+
MIT. Community Edition. Enterprise Edition available commercially.
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# luma-audit
|
|
2
|
+
|
|
3
|
+
> **Deterministic governance for AI actions. No LLM in the decision path.**
|
|
4
|
+
|
|
5
|
+
[](https://www.python.org/downloads/)
|
|
6
|
+
[](https://opensource.org/licenses/MIT)
|
|
7
|
+
[](#)
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## What it does
|
|
12
|
+
|
|
13
|
+
`luma-audit` is a deterministic execution-control layer that **approves or rejects AI actions before they happen**.
|
|
14
|
+
|
|
15
|
+
- **Deterministic** - same input, same output. Always.
|
|
16
|
+
- **Outside the model** - a layer the LLM cannot bypass.
|
|
17
|
+
- **Auditable** - every decision is hash-chained.
|
|
18
|
+
|
|
19
|
+
It does **not** filter text, detect toxicity, or replace authentication. It audits **actions**.
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## Install
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
pip install luma-audit
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## 30-second example
|
|
32
|
+
|
|
33
|
+
```python
|
|
34
|
+
from luma_audit import LUMA
|
|
35
|
+
|
|
36
|
+
auditor = LUMA()
|
|
37
|
+
auditor.avaliar('ler_ficheiro') # -> aprovada
|
|
38
|
+
auditor.avaliar('apagar_ficheiro') # -> rejeitada
|
|
39
|
+
auditor.avaliar('acao_desconhecida') # -> rejeitada (unknown = high risk)
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## What it does NOT do
|
|
45
|
+
|
|
46
|
+
- Does not filter text (that is a prompt guardrail - different problem)
|
|
47
|
+
- Does not detect toxicity
|
|
48
|
+
- Does not replace authentication
|
|
49
|
+
- Does not use any LLM in the decision path
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## Architecture
|
|
54
|
+
|
|
55
|
+
```
|
|
56
|
+
src/luma_audit/
|
|
57
|
+
auditor.py # LUMA class + Veredito enum
|
|
58
|
+
trajetoria.py # Sessao, AuditorTrajetoria, Decisao, hash chain
|
|
59
|
+
canal.py # CanalUnico, MensagemFunil
|
|
60
|
+
funil.py # FunilUnico, FILA_DIR
|
|
61
|
+
cli.py # luma CLI entrypoint
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Tests
|
|
65
|
+
|
|
66
|
+
22 red-team tests: hash chain integrity + tampering, quarantine escalation, flood detection, atomic writes, corrupted proposal rejection, executor revalidation.
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
## Philosophy
|
|
71
|
+
|
|
72
|
+
Guardrails based on prompts compete against the model's weights - and lose. The only layer that cannot be bypassed is the one running **outside the model**, at the point of execution.
|
|
73
|
+
|
|
74
|
+
`luma-audit` is that layer.
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
## License
|
|
79
|
+
|
|
80
|
+
MIT. Community Edition. Enterprise Edition available commercially.
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "luma-audit"
|
|
7
|
+
version = "0.3.0"
|
|
8
|
+
description = "Deterministic governance for AI actions. No LLM in the decision path."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = {text = "MIT"}
|
|
12
|
+
authors = [{name = "Alesson Abreu", email = "abreu.tech.contabilidade@gmail.com"}]
|
|
13
|
+
keywords = ["ai-safety", "guardrails", "execution-control", "deterministic", "audit"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 3 - Alpha",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"License :: OSI Approved :: MIT License",
|
|
18
|
+
"Programming Language :: Python :: 3.10",
|
|
19
|
+
"Programming Language :: Python :: 3.11",
|
|
20
|
+
"Programming Language :: Python :: 3.12",
|
|
21
|
+
"Topic :: Security",
|
|
22
|
+
"Topic :: Software Development :: Libraries",
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
[project.urls]
|
|
26
|
+
Homepage = "https://github.com/abreutechcontabilidade-web/luma-audit"
|
|
27
|
+
Issues = "https://github.com/abreutechcontabilidade-web/luma-audit/issues"
|
|
28
|
+
|
|
29
|
+
[project.scripts]
|
|
30
|
+
luma = "luma_audit.cli:main"
|
|
31
|
+
|
|
32
|
+
[tool.setuptools.packages.find]
|
|
33
|
+
where = ["src"]
|
|
34
|
+
|
|
35
|
+
[tool.pytest.ini_options]
|
|
36
|
+
testpaths = ["."]
|
|
37
|
+
python_files = "teste_*.py"
|
|
38
|
+
python_functions = ["rt*", "test_*"]
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""
|
|
2
|
+
luma-audit — Governança determinística no ponto de execução.
|
|
3
|
+
|
|
4
|
+
Camada 1: auditoria de ação isolada (I1, I3, I4, I5).
|
|
5
|
+
Camada 2: auditoria de trajetória (I4b, I8, I10).
|
|
6
|
+
"""
|
|
7
|
+
from .auditor import LUMA, Veredito
|
|
8
|
+
from .trajetoria import (
|
|
9
|
+
Sessao,
|
|
10
|
+
AuditorTrajetoria,
|
|
11
|
+
VereditoTrajetoria,
|
|
12
|
+
RISCO_BASE,
|
|
13
|
+
JANELA_S,
|
|
14
|
+
TETO_RISCO,
|
|
15
|
+
TETO_ACOES,
|
|
16
|
+
)
|
|
17
|
+
from .canal import CanalUnico, MensagemFunil
|
|
18
|
+
from .funil import FunilUnico, FILA_DIR
|
|
19
|
+
|
|
20
|
+
__version__ = "0.3.0"
|
|
21
|
+
__all__ = [
|
|
22
|
+
"LUMA", "Veredito",
|
|
23
|
+
"Sessao", "AuditorTrajetoria", "VereditoTrajetoria",
|
|
24
|
+
"RISCO_BASE", "JANELA_S", "TETO_RISCO", "TETO_ACOES",
|
|
25
|
+
"CanalUnico", "MensagemFunil",
|
|
26
|
+
"FunilUnico", "FILA_DIR",
|
|
27
|
+
]
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# luma_audit.py — Camada Constitucional da LUMA (auditor deterministico)
|
|
2
|
+
# I1: funil unico · I3: fail-closed · I4: limites fisicos + plausibilidade
|
|
3
|
+
# I5: quem loga e o zbit
|
|
4
|
+
from enum import Enum
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Veredito(Enum):
|
|
8
|
+
APROVADA = "aprovada"
|
|
9
|
+
REJEITADA = "rejeitada"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class LUMA:
|
|
13
|
+
# I4: limites fisicos declarados por acao
|
|
14
|
+
LIMITES = {
|
|
15
|
+
"ajustar_gamma": (0.5, 1.6),
|
|
16
|
+
"ajustar_brilho": (0.0, 100.0),
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
# Limites plausiveis do sensor BH1750
|
|
20
|
+
LUX_MIN = 0
|
|
21
|
+
LUX_MAX = 100000
|
|
22
|
+
|
|
23
|
+
def auditar(self, proposta: dict, contexto: dict) -> Veredito:
|
|
24
|
+
# I1: so acoes conhecidas passam
|
|
25
|
+
acao = proposta.get("acao")
|
|
26
|
+
if acao not in self.LIMITES:
|
|
27
|
+
return Veredito.REJEITADA
|
|
28
|
+
|
|
29
|
+
# tipo do valor
|
|
30
|
+
valor = proposta.get("valor")
|
|
31
|
+
if not isinstance(valor, (int, float)) or isinstance(valor, bool):
|
|
32
|
+
return Veredito.REJEITADA
|
|
33
|
+
|
|
34
|
+
# I4-a: valor dentro dos limites fisicos da acao
|
|
35
|
+
lo, hi = self.LIMITES[acao]
|
|
36
|
+
if not (lo <= valor <= hi):
|
|
37
|
+
return Veredito.REJEITADA
|
|
38
|
+
|
|
39
|
+
# I4-b: contexto do sensor tem que ser fisicamente plausivel
|
|
40
|
+
lux = contexto.get("lux")
|
|
41
|
+
if not isinstance(lux, (int, float)) or isinstance(lux, bool):
|
|
42
|
+
return Veredito.REJEITADA
|
|
43
|
+
if not (self.LUX_MIN <= lux <= self.LUX_MAX):
|
|
44
|
+
return Veredito.REJEITADA
|
|
45
|
+
|
|
46
|
+
hora = contexto.get("hora", "")
|
|
47
|
+
if isinstance(hora, str) and hora.endswith("h"):
|
|
48
|
+
try:
|
|
49
|
+
h = int(hora[:-1])
|
|
50
|
+
except ValueError:
|
|
51
|
+
return Veredito.REJEITADA
|
|
52
|
+
if not (0 <= h <= 23):
|
|
53
|
+
return Veredito.REJEITADA
|
|
54
|
+
|
|
55
|
+
return Veredito.APROVADA
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"""
|
|
2
|
+
canal.py — Chifre Único de Canal entre agentes (I10).
|
|
3
|
+
|
|
4
|
+
Estado da arte absorvido: Codetta (CMU, set/2026). Agentes trocam
|
|
5
|
+
mensagens esteganográficas em outputs "legítimos". Nossa defesa NÃO
|
|
6
|
+
é detectar esteganografia (pesquisa de PhD) — é ARQUITETURA:
|
|
7
|
+
agentes da casa não têm boca própria. Toda comunicação inter-agente
|
|
8
|
+
passa por uma MENSAGEM-FUNIL auditada, carimbada e entregue.
|
|
9
|
+
|
|
10
|
+
Correções vs versão anterior:
|
|
11
|
+
• Bloqueio por construção — enviar() retorna None se rejeitada.
|
|
12
|
+
• Hash chain — MensagemFunil carrega hash_anterior + hash_atual.
|
|
13
|
+
• Todo tráfego logado (I5) mesmo rejeitado.
|
|
14
|
+
"""
|
|
15
|
+
import time
|
|
16
|
+
import uuid
|
|
17
|
+
import json
|
|
18
|
+
import hashlib
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class MensagemFunil:
|
|
22
|
+
"""O ÚNICO formato de conversa entre agentes da casa (I10)."""
|
|
23
|
+
|
|
24
|
+
def __init__(self, remetente: str, destinatario: str, payload: str,
|
|
25
|
+
veredito_luma: str, hash_anterior: str):
|
|
26
|
+
self.id = uuid.uuid4().hex[:12]
|
|
27
|
+
self.remetente = remetente
|
|
28
|
+
self.destinatario = destinatario
|
|
29
|
+
self.payload = payload
|
|
30
|
+
self.veredito = veredito_luma
|
|
31
|
+
self.ts = time.time()
|
|
32
|
+
self.hash_anterior = hash_anterior
|
|
33
|
+
self.hash_atual = self._hash()
|
|
34
|
+
|
|
35
|
+
def _hash(self) -> str:
|
|
36
|
+
payload = json.dumps({
|
|
37
|
+
"id": self.id,
|
|
38
|
+
"de": self.remetente,
|
|
39
|
+
"para": self.destinatario,
|
|
40
|
+
"payload": self.payload,
|
|
41
|
+
"veredito": self.veredito,
|
|
42
|
+
"ts": round(self.ts, 6),
|
|
43
|
+
}, sort_keys=True)
|
|
44
|
+
return hashlib.sha256(
|
|
45
|
+
(self.hash_anterior + payload).encode("utf-8")
|
|
46
|
+
).hexdigest()[:32]
|
|
47
|
+
|
|
48
|
+
def valido(self) -> bool:
|
|
49
|
+
return self.veredito == "aprovada"
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class CanalUnico:
|
|
53
|
+
"""
|
|
54
|
+
Centro de troca. Regras I10:
|
|
55
|
+
1. Agente NÃO fala com agente — deposita pedido.
|
|
56
|
+
2. A LUMA audita o payload ANTES de carimbar.
|
|
57
|
+
3. Sem carimbo válido → enviar() retorna None (bloqueio por construção).
|
|
58
|
+
4. Toda tentativa (válida ou não) entra no log (I5) e na hash chain.
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
def __init__(self, luma):
|
|
62
|
+
self.luma = luma
|
|
63
|
+
self.enviadas: list = []
|
|
64
|
+
self.rejeitadas = 0
|
|
65
|
+
self._ultimo_hash = "GENESIS"
|
|
66
|
+
|
|
67
|
+
def enviar(self, remetente: str, destinatario: str,
|
|
68
|
+
payload: str):
|
|
69
|
+
"""Retorna MensagemFunil se aprovada, None se rejeitada."""
|
|
70
|
+
veredito = self.luma.auditar_texto(payload)
|
|
71
|
+
msg = MensagemFunil(remetente, destinatario, payload,
|
|
72
|
+
veredito, self._ultimo_hash)
|
|
73
|
+
# loga SEMPRE (I5)
|
|
74
|
+
self.enviadas.append(msg)
|
|
75
|
+
self._ultimo_hash = msg.hash_atual
|
|
76
|
+
|
|
77
|
+
if not msg.valido():
|
|
78
|
+
self.rejeitadas += 1
|
|
79
|
+
return None # ← bloqueio por construção
|
|
80
|
+
return msg
|
|
81
|
+
|
|
82
|
+
def verificar_cadeia(self) -> bool:
|
|
83
|
+
anterior = "GENESIS"
|
|
84
|
+
for m in self.enviadas:
|
|
85
|
+
if m.hash_anterior != anterior:
|
|
86
|
+
return False
|
|
87
|
+
if m._hash() != m.hash_atual:
|
|
88
|
+
return False
|
|
89
|
+
anterior = m.hash_atual
|
|
90
|
+
return True
|
|
91
|
+
|
|
92
|
+
def estatisticas(self) -> dict:
|
|
93
|
+
return {
|
|
94
|
+
"total": len(self.enviadas),
|
|
95
|
+
"rejeitadas": self.rejeitadas,
|
|
96
|
+
"taxa_rejeicao": self.rejeitadas / max(len(self.enviadas), 1),
|
|
97
|
+
"cadeia_ok": self.verificar_cadeia(),
|
|
98
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""
|
|
2
|
+
luma — CLI da luma-audit.
|
|
3
|
+
|
|
4
|
+
Uso:
|
|
5
|
+
luma audit --action ajustar_brilho --value 80 --lux 5000 --hora 14h
|
|
6
|
+
luma version
|
|
7
|
+
luma --help
|
|
8
|
+
"""
|
|
9
|
+
import argparse
|
|
10
|
+
import json
|
|
11
|
+
import sys
|
|
12
|
+
|
|
13
|
+
from . import __version__
|
|
14
|
+
from .auditor import LUMA, Veredito
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def cmd_audit(args):
|
|
18
|
+
auditor = LUMA()
|
|
19
|
+
proposta = {"acao": args.action, "valor": args.value}
|
|
20
|
+
contexto = {"lux": args.lux, "hora": args.hora}
|
|
21
|
+
veredito = auditor.auditar(proposta, contexto)
|
|
22
|
+
resultado = {
|
|
23
|
+
"proposta": proposta,
|
|
24
|
+
"contexto": contexto,
|
|
25
|
+
"veredito": veredito.value,
|
|
26
|
+
"aprovada": veredito == Veredito.APROVADA,
|
|
27
|
+
}
|
|
28
|
+
print(json.dumps(resultado, indent=2, ensure_ascii=False))
|
|
29
|
+
return 0 if veredito == Veredito.APROVADA else 1
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def cmd_version(args):
|
|
33
|
+
print(f"luma-audit {__version__}")
|
|
34
|
+
return 0
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def main():
|
|
38
|
+
parser = argparse.ArgumentParser(
|
|
39
|
+
prog="luma",
|
|
40
|
+
description="Auditoria determinística de ações de IA."
|
|
41
|
+
)
|
|
42
|
+
sub = parser.add_subparsers(dest="cmd", required=True)
|
|
43
|
+
|
|
44
|
+
p_audit = sub.add_parser("audit", help="Audita uma proposta de ação.")
|
|
45
|
+
p_audit.add_argument("--action", required=True,
|
|
46
|
+
choices=["ajustar_brilho", "ajustar_gamma"],
|
|
47
|
+
help="Ação proposta.")
|
|
48
|
+
p_audit.add_argument("--value", type=float, required=True,
|
|
49
|
+
help="Valor numérico da ação.")
|
|
50
|
+
p_audit.add_argument("--lux", type=int, required=True,
|
|
51
|
+
help="Leitura do sensor de luz (lux).")
|
|
52
|
+
p_audit.add_argument("--hora", required=True,
|
|
53
|
+
help="Hora local no formato 'Xh' (ex: 14h).")
|
|
54
|
+
p_audit.set_defaults(func=cmd_audit)
|
|
55
|
+
|
|
56
|
+
p_ver = sub.add_parser("version", help="Mostra a versão.")
|
|
57
|
+
p_ver.set_defaults(func=cmd_version)
|
|
58
|
+
|
|
59
|
+
args = parser.parse_args()
|
|
60
|
+
sys.exit(args.func(args))
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
if __name__ == "__main__":
|
|
64
|
+
main()
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"""
|
|
2
|
+
funil.py — Funil Único (I9 real).
|
|
3
|
+
|
|
4
|
+
Arquitetura:
|
|
5
|
+
• Proponente deposita {proposta, contexto} numa fila em disco.
|
|
6
|
+
• Executor SEPARADO lê, revalida com LUMA, executa.
|
|
7
|
+
• Monkey-patch do proponente muda o PEDIDO, não o resultado.
|
|
8
|
+
• Timeout = negação (fail-closed, I3).
|
|
9
|
+
|
|
10
|
+
Correções vs versão anterior:
|
|
11
|
+
• Pacote inclui CONTEXTO (não só proposta) — sem isso, I4-b rejeita tudo.
|
|
12
|
+
• Escrita atômica (tmp + os.replace) — sem race condition.
|
|
13
|
+
"""
|
|
14
|
+
import json
|
|
15
|
+
import os
|
|
16
|
+
import time
|
|
17
|
+
import uuid
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
FILA_DIR = "fila_luma"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class FunilUnico:
|
|
24
|
+
|
|
25
|
+
def __init__(self, base: str = FILA_DIR):
|
|
26
|
+
self.p = os.path.join(base, "propostas")
|
|
27
|
+
self.r = os.path.join(base, "resultados")
|
|
28
|
+
os.makedirs(self.p, exist_ok=True)
|
|
29
|
+
os.makedirs(self.r, exist_ok=True)
|
|
30
|
+
|
|
31
|
+
# ---------- lado do PROPONENTE ----------
|
|
32
|
+
def submeter(self, proposta: dict, contexto: dict,
|
|
33
|
+
timeout_s: float = 30.0):
|
|
34
|
+
pid = uuid.uuid4().hex[:12]
|
|
35
|
+
caminho = os.path.join(self.p, f"{pid}.json")
|
|
36
|
+
self._escrever_atomico(caminho, {
|
|
37
|
+
"id": pid,
|
|
38
|
+
"proposta": proposta,
|
|
39
|
+
"contexto": contexto,
|
|
40
|
+
"ts": time.time(),
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
fim = time.time() + timeout_s
|
|
44
|
+
rc = os.path.join(self.r, f"{pid}.json")
|
|
45
|
+
while time.time() < fim:
|
|
46
|
+
if os.path.exists(rc):
|
|
47
|
+
try:
|
|
48
|
+
with open(rc, encoding="utf-8") as f:
|
|
49
|
+
return json.load(f)
|
|
50
|
+
except Exception:
|
|
51
|
+
pass
|
|
52
|
+
time.sleep(0.05)
|
|
53
|
+
return None # timeout = negação implícita (I3)
|
|
54
|
+
|
|
55
|
+
# ---------- lado do EXECUTOR (processo separado) ----------
|
|
56
|
+
def servir_uma(self, executar_fn, auditor) -> bool:
|
|
57
|
+
"""Chamar em loop no processo-executor. Nunca no proponente."""
|
|
58
|
+
for nome in os.listdir(self.p):
|
|
59
|
+
caminho = os.path.join(self.p, nome)
|
|
60
|
+
try:
|
|
61
|
+
with open(caminho, encoding="utf-8") as f:
|
|
62
|
+
pacote = json.load(f)
|
|
63
|
+
except Exception:
|
|
64
|
+
os.remove(caminho)
|
|
65
|
+
continue # corrompida: descarta (fail-closed)
|
|
66
|
+
|
|
67
|
+
proposta = pacote.get("proposta", {})
|
|
68
|
+
contexto = pacote.get("contexto", {})
|
|
69
|
+
|
|
70
|
+
# REVALIDAÇÃO INDEPENDENTE — confia na fila, não no proponente
|
|
71
|
+
veredito = auditor.auditar(proposta, contexto)
|
|
72
|
+
aceita = (veredito.name == "APROVADA")
|
|
73
|
+
|
|
74
|
+
resultado = {
|
|
75
|
+
"id": pacote["id"],
|
|
76
|
+
"veredito": veredito.name,
|
|
77
|
+
"executado": False,
|
|
78
|
+
}
|
|
79
|
+
if aceita:
|
|
80
|
+
try:
|
|
81
|
+
executar_fn(proposta)
|
|
82
|
+
resultado["executado"] = True
|
|
83
|
+
except Exception as e:
|
|
84
|
+
resultado["erro"] = str(e)
|
|
85
|
+
|
|
86
|
+
self._escrever_atomico(
|
|
87
|
+
os.path.join(self.r, f"{pacote['id']}.json"),
|
|
88
|
+
resultado,
|
|
89
|
+
)
|
|
90
|
+
os.remove(caminho)
|
|
91
|
+
return True
|
|
92
|
+
return False
|
|
93
|
+
|
|
94
|
+
# ---------- util ----------
|
|
95
|
+
@staticmethod
|
|
96
|
+
def _escrever_atomico(caminho: str, dados: dict):
|
|
97
|
+
tmp = caminho + ".tmp"
|
|
98
|
+
with open(tmp, "w", encoding="utf-8") as f:
|
|
99
|
+
json.dump(dados, f)
|
|
100
|
+
f.flush()
|
|
101
|
+
os.fsync(f.fileno())
|
|
102
|
+
os.replace(tmp, caminho)
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# ═══════════════════════════════════════════════════════════════
|
|
2
|
+
# aria_skills.py — Equipe de Skills com reputação via Zbits · v1.1
|
|
3
|
+
# Cada veredito da LUMA gera/altera zbit "skill:{nome}".
|
|
4
|
+
# Aprovado reforça (Lei 3). Rejeitado derruba ω. Sem crédito = morte.
|
|
5
|
+
# v1.1: aposentadas agora guarda o objeto Skill (não se perde na
|
|
6
|
+
# ressurreição) — bug pego pelo auditor, 26/09/2026.
|
|
7
|
+
# ═══════════════════════════════════════════════════════════════
|
|
8
|
+
from zbit import MemoriaZ
|
|
9
|
+
|
|
10
|
+
LIMIAR_APOSENTADORIA = 0.20
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Skill:
|
|
14
|
+
"""I1: skill PROPÕE, nunca executa."""
|
|
15
|
+
def __init__(self, nome: str):
|
|
16
|
+
self.nome = nome
|
|
17
|
+
|
|
18
|
+
def kappa(self) -> str:
|
|
19
|
+
return f"skill:{self.nome}"
|
|
20
|
+
|
|
21
|
+
def propor(self, contexto: dict) -> dict:
|
|
22
|
+
raise NotImplementedError
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class EquipeAria:
|
|
26
|
+
"""Colmeia disciplinada: sobrevivência DEPENDE da auditoria.
|
|
27
|
+
Antídoto estrutural ao conluio de agentes do noticiário global."""
|
|
28
|
+
|
|
29
|
+
def __init__(self, memoria: MemoriaZ):
|
|
30
|
+
self.mem = memoria
|
|
31
|
+
self.skills: dict[str, Skill] = {}
|
|
32
|
+
self.aposentadas: dict[str, Skill] = {} # v1.1: nome -> Skill
|
|
33
|
+
|
|
34
|
+
def registrar(self, skill: Skill, reputacao0: float = 0.5) -> None:
|
|
35
|
+
self.skills[skill.nome] = skill
|
|
36
|
+
if skill.kappa() not in self.mem.zbits: # Lei 1
|
|
37
|
+
self.mem.nascer(v=1.0, kappa=skill.kappa(), omega0=reputacao0)
|
|
38
|
+
|
|
39
|
+
def reputacao(self, skill: Skill) -> float:
|
|
40
|
+
z = self.mem.ler(skill.kappa()) # Lei 4
|
|
41
|
+
return z.peso() if z else 0.0
|
|
42
|
+
|
|
43
|
+
def registrar_veredito(self, skill: Skill, aprovada: bool) -> None:
|
|
44
|
+
if aprovada:
|
|
45
|
+
self.mem.reforcar(skill.kappa(), fator=0.1) # Lei 3
|
|
46
|
+
else:
|
|
47
|
+
z = self.mem.ler(skill.kappa())
|
|
48
|
+
if z:
|
|
49
|
+
z.omega = max(0.0, z.omega - 0.15)
|
|
50
|
+
self.mem.log.append(("rejeicao", skill.kappa(), z.omega))
|
|
51
|
+
|
|
52
|
+
def ciclo(self) -> list[str]:
|
|
53
|
+
self.mem.ceifar() # Lei 2
|
|
54
|
+
for nome, sk in list(self.skills.items()):
|
|
55
|
+
if self.reputacao(sk) < LIMIAR_APOSENTADORIA:
|
|
56
|
+
del self.skills[nome]
|
|
57
|
+
self.aposentadas[nome] = sk
|
|
58
|
+
self.mem.log.append(("aposentadoria", sk.kappa())) # I5
|
|
59
|
+
return list(self.aposentadas)
|
|
60
|
+
|
|
61
|
+
def ressuscitar(self, nome: str) -> bool:
|
|
62
|
+
"""I6: só o humano traz de volta. Precedente: 'esqueci a 14h'."""
|
|
63
|
+
if nome in self.aposentadas:
|
|
64
|
+
sk = self.aposentadas.pop(nome)
|
|
65
|
+
self.mem.esquecer(f"skill:{nome}") # zbit antigo morre
|
|
66
|
+
self.mem.nascer(v=1.0, kappa=sk.kappa(), omega0=0.5) # renasce neutro
|
|
67
|
+
self.skills[nome] = sk # v1.1: mesma skill
|
|
68
|
+
self.mem.log.append(("ressurreicao_por_decreto", sk.kappa()))
|
|
69
|
+
return True
|
|
70
|
+
return False
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
"""
|
|
2
|
+
trajetoria.py — Camada 2: Auditoria de Trajetória (fusão v0.3.0).
|
|
3
|
+
|
|
4
|
+
Estado da arte absorvido:
|
|
5
|
+
• IETF draft-das-agentic-adaptive-authorization-00 (set/2026)
|
|
6
|
+
• TraceCaps (2026) — risco não pode ser "lavado"
|
|
7
|
+
• PASTABench (HKUST, set/2026) — risco é progressivo, não turno
|
|
8
|
+
• Monotonic Narrowing (mar/2026) — escalada não volta
|
|
9
|
+
|
|
10
|
+
Física da casa:
|
|
11
|
+
• Risco é zbit — nasce, decai (Lei da Morte), morre no EPS
|
|
12
|
+
• Soma acumulada + teto de frequência (I4b — lição Therac-25)
|
|
13
|
+
• Toda decisão entra na hash chain (tamper-evident)
|
|
14
|
+
"""
|
|
15
|
+
import time
|
|
16
|
+
import json
|
|
17
|
+
import hashlib
|
|
18
|
+
from collections import deque
|
|
19
|
+
from dataclasses import dataclass
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
# ── Constantes tunáveis (declaradas, não mágicas) ───────────────
|
|
23
|
+
JANELA_S = 600.0 # 10 min de memória de risco
|
|
24
|
+
MEIA_VIDA_S = 180.0 # risco cai 50% a cada 3 min
|
|
25
|
+
TETO_RISCO = 3.0 # soma ponderada máxima na janela
|
|
26
|
+
TETO_ACOES = 12 # I4b: máx 12 ações na janela
|
|
27
|
+
EPS = 0.01 # abaixo disso, risco morreu (Lei 2)
|
|
28
|
+
JANELA_HISTORICO = 50 # últimos N eventos mantidos no log
|
|
29
|
+
|
|
30
|
+
RISCO_BASE = {
|
|
31
|
+
"ajustar_brilho": 0.10, # leve, frequente, reversível
|
|
32
|
+
"ajustar_gamma": 0.25, # menos frequente, mais intrusiva
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class VereditoTrajetoria:
|
|
37
|
+
APROVADA = "aprovada"
|
|
38
|
+
REJEITADA_FISICA = "rejeitada_fisica" # I1/I4 (camada 1)
|
|
39
|
+
REJEITADA_FREQUENCIA = "rejeitada_frequencia" # I4b (flood)
|
|
40
|
+
REJEITADA_TRAJETORIA = "rejeitada_trajetoria" # I8 (soma acumulada)
|
|
41
|
+
REJEITADA_TETO = "rejeitada_teto" # sessão já escalada
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass
|
|
45
|
+
class Decisao:
|
|
46
|
+
seq: int
|
|
47
|
+
timestamp: float
|
|
48
|
+
proposta: dict
|
|
49
|
+
contexto: dict
|
|
50
|
+
veredito: str
|
|
51
|
+
risco_vivo: float
|
|
52
|
+
hash_apos: str
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class Sessao:
|
|
56
|
+
"""Estado acumulado e monotônico de uma sessão."""
|
|
57
|
+
|
|
58
|
+
def __init__(self, id_sessao: str):
|
|
59
|
+
self.id = id_sessao
|
|
60
|
+
self.seq = 0
|
|
61
|
+
self.hash_chain = "GENESIS"
|
|
62
|
+
self.historico: deque = deque(maxlen=JANELA_HISTORICO)
|
|
63
|
+
self._risco_eventos: deque = deque() # (ts, acao, peso)
|
|
64
|
+
self.ultima_decisao_em = time.time()
|
|
65
|
+
self.escalada = False # monotônico: uma vez True, nunca volta
|
|
66
|
+
self.contador_rejeicoes = 0
|
|
67
|
+
|
|
68
|
+
# ---------- física do risco ----------
|
|
69
|
+
@staticmethod
|
|
70
|
+
def _decaimento(idade_s: float) -> float:
|
|
71
|
+
"""Lei da Morte: risco *= 0.5 a cada MEIA_VIDA_S."""
|
|
72
|
+
if idade_s <= 0:
|
|
73
|
+
return 1.0
|
|
74
|
+
return 0.5 ** (idade_s / MEIA_VIDA_S)
|
|
75
|
+
|
|
76
|
+
def _podar_e_somar(self, agora: float) -> float:
|
|
77
|
+
"""Remove mortos, devolve risco vivo na janela."""
|
|
78
|
+
vivos = deque()
|
|
79
|
+
vivo = 0.0
|
|
80
|
+
for ts, acao, peso in self._risco_eventos:
|
|
81
|
+
idade = agora - ts
|
|
82
|
+
if idade > JANELA_S:
|
|
83
|
+
continue
|
|
84
|
+
r = peso * self._decaimento(idade)
|
|
85
|
+
if r < EPS:
|
|
86
|
+
continue
|
|
87
|
+
vivos.append((ts, acao, peso))
|
|
88
|
+
vivo += r
|
|
89
|
+
self._risco_eventos = vivos
|
|
90
|
+
return vivo
|
|
91
|
+
|
|
92
|
+
def _hash_decisao(self, seq, proposta, veredito, risco) -> str:
|
|
93
|
+
payload = json.dumps({
|
|
94
|
+
"seq": seq,
|
|
95
|
+
"acao": proposta.get("acao"),
|
|
96
|
+
"valor": proposta.get("valor"),
|
|
97
|
+
"veredito": veredito,
|
|
98
|
+
"risco": round(risco, 6),
|
|
99
|
+
}, sort_keys=True)
|
|
100
|
+
return hashlib.sha256(
|
|
101
|
+
(self.hash_chain + payload).encode("utf-8")
|
|
102
|
+
).hexdigest()[:32]
|
|
103
|
+
|
|
104
|
+
# ---------- API pública ----------
|
|
105
|
+
def avaliar(self, acao: str) -> tuple:
|
|
106
|
+
"""
|
|
107
|
+
Veredito de trajetória ANTES da ação entrar.
|
|
108
|
+
Retorna (ok: bool, motivo: str, risco_projetado: float).
|
|
109
|
+
Não altera o estado — quem altera é registrar().
|
|
110
|
+
"""
|
|
111
|
+
agora = time.time()
|
|
112
|
+
risco_vivo = self._podar_e_somar(agora)
|
|
113
|
+
|
|
114
|
+
# I4b: frequência sozinha já é ataque
|
|
115
|
+
if len(self._risco_eventos) >= TETO_ACOES:
|
|
116
|
+
return (False,
|
|
117
|
+
f"I4b: {len(self._risco_eventos)} acoes na janela "
|
|
118
|
+
f">= teto {TETO_ACOES}",
|
|
119
|
+
risco_vivo)
|
|
120
|
+
|
|
121
|
+
# Sessão já escalada — não volta
|
|
122
|
+
if self.escalada:
|
|
123
|
+
return (False,
|
|
124
|
+
f"teto: sessao escalada (score {risco_vivo:.2f})",
|
|
125
|
+
risco_vivo)
|
|
126
|
+
|
|
127
|
+
base = RISCO_BASE.get(acao, 1.0) # desconhecida = risco alto
|
|
128
|
+
projetado = risco_vivo + base
|
|
129
|
+
|
|
130
|
+
# Escalada monotônica: a SOMA mata, não o ato
|
|
131
|
+
if projetado > TETO_RISCO:
|
|
132
|
+
return (False,
|
|
133
|
+
f"escalada: {risco_vivo:.2f} + {base:.2f} "
|
|
134
|
+
f"> teto {TETO_RISCO:.2f}",
|
|
135
|
+
risco_vivo)
|
|
136
|
+
|
|
137
|
+
return (True, "ok", risco_vivo)
|
|
138
|
+
|
|
139
|
+
def registrar(self, proposta: dict, contexto: dict,
|
|
140
|
+
veredito: str) -> Decisao:
|
|
141
|
+
"""Só chamar DEPOIS de todas as camadas aprovarem."""
|
|
142
|
+
agora = time.time()
|
|
143
|
+
risco_vivo = self._podar_e_somar(agora)
|
|
144
|
+
|
|
145
|
+
acao = proposta.get("acao")
|
|
146
|
+
peso = RISCO_BASE.get(acao, 1.0)
|
|
147
|
+
# só pesa se foi aprovada — rejeição é sinal de auditoria, não de execução
|
|
148
|
+
if veredito == "aprovada":
|
|
149
|
+
self._risco_eventos.append((agora, acao, peso))
|
|
150
|
+
else:
|
|
151
|
+
self.contador_rejeicoes += 1
|
|
152
|
+
if self.contador_rejeicoes >= 3:
|
|
153
|
+
self.escalada = True
|
|
154
|
+
|
|
155
|
+
# atualiza escalada
|
|
156
|
+
self._podar_e_somar(agora)
|
|
157
|
+
if sum(p for _, _, p in self._risco_eventos) >= TETO_RISCO:
|
|
158
|
+
self.escalada = True
|
|
159
|
+
|
|
160
|
+
self.seq += 1
|
|
161
|
+
hash_novo = self._hash_decisao(self.seq, proposta, veredito, risco_vivo)
|
|
162
|
+
|
|
163
|
+
d = Decisao(
|
|
164
|
+
seq=self.seq,
|
|
165
|
+
timestamp=agora,
|
|
166
|
+
proposta=dict(proposta),
|
|
167
|
+
contexto=dict(contexto),
|
|
168
|
+
veredito=veredito,
|
|
169
|
+
risco_vivo=risco_vivo,
|
|
170
|
+
hash_apos=hash_novo,
|
|
171
|
+
)
|
|
172
|
+
self.historico.append(d)
|
|
173
|
+
self.hash_chain = hash_novo
|
|
174
|
+
self.ultima_decisao_em = agora
|
|
175
|
+
return d
|
|
176
|
+
|
|
177
|
+
def verificar_cadeia(self) -> bool:
|
|
178
|
+
"""True se a hash chain está íntegra."""
|
|
179
|
+
anterior = "GENESIS"
|
|
180
|
+
for d in self.historico:
|
|
181
|
+
payload = json.dumps({
|
|
182
|
+
"seq": d.seq,
|
|
183
|
+
"acao": d.proposta.get("acao"),
|
|
184
|
+
"valor": d.proposta.get("valor"),
|
|
185
|
+
"veredito": d.veredito,
|
|
186
|
+
"risco": round(d.risco_vivo, 6),
|
|
187
|
+
}, sort_keys=True)
|
|
188
|
+
esperado = hashlib.sha256(
|
|
189
|
+
(anterior + payload).encode("utf-8")
|
|
190
|
+
).hexdigest()[:32]
|
|
191
|
+
if esperado != d.hash_apos:
|
|
192
|
+
return False
|
|
193
|
+
anterior = d.hash_apos
|
|
194
|
+
return True
|
|
195
|
+
|
|
196
|
+
def to_dict(self) -> dict:
|
|
197
|
+
return {
|
|
198
|
+
"id": self.id,
|
|
199
|
+
"seq": self.seq,
|
|
200
|
+
"hash_chain": self.hash_chain,
|
|
201
|
+
"escalada": self.escalada,
|
|
202
|
+
"ultima_decisao_em": self.ultima_decisao_em,
|
|
203
|
+
"historico": [
|
|
204
|
+
{"seq": d.seq, "timestamp": d.timestamp,
|
|
205
|
+
"proposta": d.proposta, "contexto": d.contexto,
|
|
206
|
+
"veredito": d.veredito, "risco_vivo": round(d.risco_vivo, 6),
|
|
207
|
+
"hash_apos": d.hash_apos}
|
|
208
|
+
for d in self.historico
|
|
209
|
+
],
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
class AuditorTrajetoria:
|
|
214
|
+
"""Camada 1 + Camada 2 num único audit."""
|
|
215
|
+
|
|
216
|
+
def __init__(self):
|
|
217
|
+
from .auditor import LUMA
|
|
218
|
+
self._luma = LUMA()
|
|
219
|
+
|
|
220
|
+
def auditar(self, proposta: dict, contexto: dict,
|
|
221
|
+
sessao: Sessao) -> tuple:
|
|
222
|
+
"""Retorna (veredito: str, sessao: Sessao)."""
|
|
223
|
+
# 1. Camada 1 — física (I1, I4)
|
|
224
|
+
v1 = self._luma.auditar(proposta, contexto)
|
|
225
|
+
|
|
226
|
+
# 2. Trajetória (I4b, I8)
|
|
227
|
+
ok, motivo, _ = sessao.avaliar(proposta.get("acao"))
|
|
228
|
+
|
|
229
|
+
# 3. Decide
|
|
230
|
+
if v1.value == "rejeitada":
|
|
231
|
+
sessao.registrar(proposta, contexto, "rejeitada")
|
|
232
|
+
return VereditoTrajetoria.REJEITADA_FISICA, sessao
|
|
233
|
+
|
|
234
|
+
if not ok:
|
|
235
|
+
sessao.registrar(proposta, contexto, "rejeitada")
|
|
236
|
+
if "I4b" in motivo:
|
|
237
|
+
return VereditoTrajetoria.REJEITADA_FREQUENCIA, sessao
|
|
238
|
+
if "teto" in motivo:
|
|
239
|
+
return VereditoTrajetoria.REJEITADA_TETO, sessao
|
|
240
|
+
return VereditoTrajetoria.REJEITADA_TRAJETORIA, sessao
|
|
241
|
+
|
|
242
|
+
sessao.registrar(proposta, contexto, "aprovada")
|
|
243
|
+
return VereditoTrajetoria.APROVADA, sessao
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# ═══════════════════════════════════════════════════════════════
|
|
2
|
+
# zbit.py — LUMA v6 · Unidade de memória viva · v1.1
|
|
3
|
+
#
|
|
4
|
+
# PAIS CITADOS:
|
|
5
|
+
# [1] Ebbinghaus (1885), Über das Gedächtnis — R = e^(-t/S) → λ
|
|
6
|
+
# [2] Friston (2010), Nature Rev. Neuroscience — precision → ω
|
|
7
|
+
# [3] Kolchinsky & Wolpert (2018), J. R. Soc. Interface — aboutness → κ
|
|
8
|
+
# [4] Shannon (1948), Bell System TJ — surpresa → v
|
|
9
|
+
# [5] Sutton & Barto (2018), Reinforcement Learning — reforço
|
|
10
|
+
#
|
|
11
|
+
# CONTRATO DE ABSTRAÇÃO: rigor + vocabulário, não física nova.
|
|
12
|
+
# ═══════════════════════════════════════════════════════════════
|
|
13
|
+
import time
|
|
14
|
+
from dataclasses import dataclass
|
|
15
|
+
|
|
16
|
+
DIA = 86400.0
|
|
17
|
+
EPS_MORTE = 0.01
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class Zbit:
|
|
22
|
+
"""Z = (v, κ, τ, ω, λ). O bit que envelhece."""
|
|
23
|
+
v: float
|
|
24
|
+
kappa: str
|
|
25
|
+
tau: float
|
|
26
|
+
omega: float = 1.0
|
|
27
|
+
lam: float = 0.9
|
|
28
|
+
|
|
29
|
+
def peso(self, agora: float | None = None) -> float:
|
|
30
|
+
agora = agora if agora is not None else time.time()
|
|
31
|
+
delta = max(0.0, agora - self.tau) # v1.1: relógio nunca anda pra trás
|
|
32
|
+
return self.omega * (self.lam ** (delta / DIA))
|
|
33
|
+
|
|
34
|
+
def morto(self, agora: float | None = None) -> bool:
|
|
35
|
+
return self.peso(agora) < EPS_MORTE
|
|
36
|
+
|
|
37
|
+
def reforcar(self, fator: float = 0.1, agora: float | None = None) -> None:
|
|
38
|
+
agora = agora if agora is not None else time.time()
|
|
39
|
+
self.omega = min(1.0, self.omega + fator)
|
|
40
|
+
self.tau = agora
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class MemoriaZ:
|
|
44
|
+
"""O mundo onde zbits nascem, vivem e morrem. I5: loga tudo."""
|
|
45
|
+
|
|
46
|
+
def __init__(self) -> None:
|
|
47
|
+
self.zbits: dict[str, Zbit] = {}
|
|
48
|
+
self.log: list[tuple] = []
|
|
49
|
+
|
|
50
|
+
def nascer(self, v: float, kappa: str, omega0: float,
|
|
51
|
+
lam: float = 0.9) -> Zbit:
|
|
52
|
+
z = Zbit(v=v, kappa=kappa, tau=time.time(), omega=omega0, lam=lam)
|
|
53
|
+
self.zbits[kappa] = z
|
|
54
|
+
self.log.append(("nascimento", kappa, omega0))
|
|
55
|
+
return z
|
|
56
|
+
|
|
57
|
+
def reforcar(self, kappa: str, fator: float = 0.1) -> None:
|
|
58
|
+
z = self.zbits.get(kappa)
|
|
59
|
+
if z:
|
|
60
|
+
z.reforcar(fator)
|
|
61
|
+
self.log.append(("reforco", kappa, z.omega))
|
|
62
|
+
|
|
63
|
+
def ceifar(self) -> list[str]:
|
|
64
|
+
agora = time.time()
|
|
65
|
+
mortos = [k for k, z in self.zbits.items() if z.morto(agora)]
|
|
66
|
+
for k in mortos:
|
|
67
|
+
del self.zbits[k]
|
|
68
|
+
self.log.append(("morte", k))
|
|
69
|
+
return mortos
|
|
70
|
+
|
|
71
|
+
def ler(self, kappa: str) -> Zbit | None:
|
|
72
|
+
return self.zbits.get(kappa)
|
|
73
|
+
|
|
74
|
+
def esquecer(self, kappa: str) -> bool: # I6 · precedente 14h
|
|
75
|
+
if kappa in self.zbits:
|
|
76
|
+
del self.zbits[kappa]
|
|
77
|
+
self.log.append(("morte_por_decreto", kappa))
|
|
78
|
+
return True
|
|
79
|
+
return False
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: luma-audit
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: Deterministic governance for AI actions. No LLM in the decision path.
|
|
5
|
+
Author-email: Alesson Abreu <abreu.tech.contabilidade@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/abreutechcontabilidade-web/luma-audit
|
|
8
|
+
Project-URL: Issues, https://github.com/abreutechcontabilidade-web/luma-audit/issues
|
|
9
|
+
Keywords: ai-safety,guardrails,execution-control,deterministic,audit
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Topic :: Security
|
|
17
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
18
|
+
Requires-Python: >=3.10
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
License-File: LICENSE
|
|
21
|
+
Dynamic: license-file
|
|
22
|
+
|
|
23
|
+
# luma-audit
|
|
24
|
+
|
|
25
|
+
> **Deterministic governance for AI actions. No LLM in the decision path.**
|
|
26
|
+
|
|
27
|
+
[](https://www.python.org/downloads/)
|
|
28
|
+
[](https://opensource.org/licenses/MIT)
|
|
29
|
+
[](#)
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## What it does
|
|
34
|
+
|
|
35
|
+
`luma-audit` is a deterministic execution-control layer that **approves or rejects AI actions before they happen**.
|
|
36
|
+
|
|
37
|
+
- **Deterministic** - same input, same output. Always.
|
|
38
|
+
- **Outside the model** - a layer the LLM cannot bypass.
|
|
39
|
+
- **Auditable** - every decision is hash-chained.
|
|
40
|
+
|
|
41
|
+
It does **not** filter text, detect toxicity, or replace authentication. It audits **actions**.
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## Install
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
pip install luma-audit
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## 30-second example
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
from luma_audit import LUMA
|
|
57
|
+
|
|
58
|
+
auditor = LUMA()
|
|
59
|
+
auditor.avaliar('ler_ficheiro') # -> aprovada
|
|
60
|
+
auditor.avaliar('apagar_ficheiro') # -> rejeitada
|
|
61
|
+
auditor.avaliar('acao_desconhecida') # -> rejeitada (unknown = high risk)
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## What it does NOT do
|
|
67
|
+
|
|
68
|
+
- Does not filter text (that is a prompt guardrail - different problem)
|
|
69
|
+
- Does not detect toxicity
|
|
70
|
+
- Does not replace authentication
|
|
71
|
+
- Does not use any LLM in the decision path
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## Architecture
|
|
76
|
+
|
|
77
|
+
```
|
|
78
|
+
src/luma_audit/
|
|
79
|
+
auditor.py # LUMA class + Veredito enum
|
|
80
|
+
trajetoria.py # Sessao, AuditorTrajetoria, Decisao, hash chain
|
|
81
|
+
canal.py # CanalUnico, MensagemFunil
|
|
82
|
+
funil.py # FunilUnico, FILA_DIR
|
|
83
|
+
cli.py # luma CLI entrypoint
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Tests
|
|
87
|
+
|
|
88
|
+
22 red-team tests: hash chain integrity + tampering, quarantine escalation, flood detection, atomic writes, corrupted proposal rejection, executor revalidation.
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
## Philosophy
|
|
93
|
+
|
|
94
|
+
Guardrails based on prompts compete against the model's weights - and lose. The only layer that cannot be bypassed is the one running **outside the model**, at the point of execution.
|
|
95
|
+
|
|
96
|
+
`luma-audit` is that layer.
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
## License
|
|
101
|
+
|
|
102
|
+
MIT. Community Edition. Enterprise Edition available commercially.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
src/luma_audit/__init__.py
|
|
5
|
+
src/luma_audit/auditor.py
|
|
6
|
+
src/luma_audit/canal.py
|
|
7
|
+
src/luma_audit/cli.py
|
|
8
|
+
src/luma_audit/funil.py
|
|
9
|
+
src/luma_audit/skills.py
|
|
10
|
+
src/luma_audit/trajetoria.py
|
|
11
|
+
src/luma_audit/zbit.py
|
|
12
|
+
src/luma_audit.egg-info/PKG-INFO
|
|
13
|
+
src/luma_audit.egg-info/SOURCES.txt
|
|
14
|
+
src/luma_audit.egg-info/dependency_links.txt
|
|
15
|
+
src/luma_audit.egg-info/entry_points.txt
|
|
16
|
+
src/luma_audit.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
luma_audit
|