luma-audit 0.3.0__py3-none-any.whl

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/__init__.py ADDED
@@ -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
+ ]
luma_audit/auditor.py ADDED
@@ -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
luma_audit/canal.py ADDED
@@ -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
+ }
luma_audit/cli.py ADDED
@@ -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()
luma_audit/funil.py ADDED
@@ -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)
luma_audit/skills.py ADDED
@@ -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
luma_audit/zbit.py ADDED
@@ -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
+ [![Python 3.10+](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/downloads/)
28
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
29
+ [![Status: Alpha](https://img.shields.io/badge/status-alpha-orange.svg)](#)
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,14 @@
1
+ luma_audit/__init__.py,sha256=Of9xQmWl7Vni8Yb452QhVbOfM-Z2H89OtlFPdfODt18,719
2
+ luma_audit/auditor.py,sha256=R-ahv8XrJ4ZrW40mgW6Do_YXBiiDNN1CklMlzPsayQs,1773
3
+ luma_audit/canal.py,sha256=7q_YMYRjM7t9heNn3HOcejesb_ZHPxiadWI7NOCRQjI,3424
4
+ luma_audit/cli.py,sha256=6NNmugS1X5jhK-W7sLVCRkwcOawrNfHv5qAuq15wtfo,1956
5
+ luma_audit/funil.py,sha256=ZI76RGxbVUrrBAbu_qJheFgaIwKyRyQLB86adJyZCwM,3460
6
+ luma_audit/skills.py,sha256=0UAI-Zm5n4-iYeiOEErn9xyFM6WGOFQBfwzlTPFIL0Q,3212
7
+ luma_audit/trajetoria.py,sha256=Hi96zaIpZx_Zp_RNjGwVn1tf0AoEIjbYdfdPnDPTyWo,8706
8
+ luma_audit/zbit.py,sha256=C9_l4Tp-cOX4LF5zIgz9cfIRefC6-PkCZ5-XNHjWHTo,3029
9
+ luma_audit-0.3.0.dist-info/licenses/LICENSE,sha256=gJae39xAeG4pseF-K8Qm3QKMM955bpTYHrlN3eoMueA,1089
10
+ luma_audit-0.3.0.dist-info/METADATA,sha256=KdURyY-DPJXlYtZ9P7B8rOAVF2xibLScGegoC7_r5Ro,3131
11
+ luma_audit-0.3.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
12
+ luma_audit-0.3.0.dist-info/entry_points.txt,sha256=I_N7BOes1tlz7RiWnJP487PGj3iRRuvHtr3rT5gsep4,45
13
+ luma_audit-0.3.0.dist-info/top_level.txt,sha256=m8GuvtrpEK0v7nw9UeixTLo8X8mHnjB5iHLmziunWY0,11
14
+ luma_audit-0.3.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ luma = luma_audit.cli:main
@@ -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 @@
1
+ luma_audit