Fase 0 — descoberta e arquitetura:
- Inventário do projeto, glossário de domínio, arquitetura com bounded
contexts e topologia de containers, threat model inicial.
- 12 ADRs cobrindo modular monolith, topologia de containers (Postgres
isolado + eden-core/parceiros/assinante em containers e portas
distintos), auth/sessões, modelo de permissões, criptografia/segredos,
contrato first-class, stock ledger, separação billing/finance/fiscal,
outbox transacional, adapters SaperX e Focus NFe, e identidade
compartilhada entre as 3 apps.
- 14 subagentes e 7 skills especializados por domínio em .claude/.
- Hooks de segurança (PreToolUse/PostToolUse/Stop) testados via pipe.
Fase 1 — plataforma (em andamento):
- Monorepo pnpm workspaces + Turborepo: apps/{api,worker,core-web,
reseller-web,subscriber-web} + 9 packages compartilhados.
- apps/api: NestJS mínimo com /health/live e /health/ready (checando
Postgres real via @eden/database).
- 3 frontends Vite + React + TypeScript + Tailwind, com o favicon
oficial do EDEN.
- packages/database: migration baseline (node-pg-migrate) criando
roles/role_permissions/applications/users/user_applications/sessions/
audit_log — audit log append-only com hash-chain, testado ao vivo
(UPDATE/DELETE bloqueados pelo trigger).
- compose.yaml implementando a topologia da ADR-0002, validada de ponta
a ponta: os 6 containers sobem e ficam saudáveis com um único
`docker compose up`.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
85 lines
3.1 KiB
Python
Executable File
85 lines
3.1 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""PreToolUse guard for the Bash tool (EDEN, Master Prompt §2.1/§3.4).
|
|
|
|
Reads the hook input JSON on stdin, inspects tool_input.command, and emits a
|
|
PreToolUse decision (allow/ask/deny) as JSON on stdout. Fails open (allow)
|
|
on any internal error so this hook can never itself break a legitimate
|
|
command — it only ever tightens, never crashes the turn.
|
|
"""
|
|
import json
|
|
import re
|
|
import sys
|
|
|
|
PROJECT_ROOT = "/opt/eden"
|
|
|
|
DENY_PATTERNS = [
|
|
(r"\brm\s+-rf\s+/(\s|$)", "rm -rf / — apagaria o filesystem inteiro"),
|
|
(r"\brm\s+-rf\s+/\*", "rm -rf /* — apagaria o filesystem inteiro"),
|
|
(r"\bdocker\s+system\s+prune\b", "docker system prune — remoção ampla, pode afetar outros projetos no host"),
|
|
(r"\bdocker\s+volume\s+prune\b", "docker volume prune — remoção ampla de volumes, pode afetar outros projetos"),
|
|
]
|
|
|
|
ASK_PATTERNS = [
|
|
(r"\bdocker\s+volume\s+rm\b", "remoção de volume Docker — confirme que o volume pertence ao EDEN"),
|
|
(r"(^|\s)ssh\s+\S+@", "SSH para host externo"),
|
|
(r"(^|\s)scp\s+.*:", "SCP para/de host externo"),
|
|
(r"(^|\s)rsync\s+.*\S+@\S+:", "rsync para/de host externo"),
|
|
(r"\b(cat|less|more|head|tail)\s+[^\n|;&]*\.env(\.[a-zA-Z0-9_]+)?\b", "leitura/impressão de arquivo .env"),
|
|
(r"\b(cat|less|more|head|tail)\s+[^\n|;&]*(credential|secret)", "leitura/impressão de arquivo de credencial/segredo"),
|
|
(r"~/\.ssh", "acesso a ~/.ssh"),
|
|
(r"(^|\s)/etc(/|\s|$)", "acesso a /etc"),
|
|
(r"(^|\s)/root(/|\s|$)", "acesso a /root"),
|
|
]
|
|
|
|
|
|
def outside_project_paths(command: str):
|
|
"""Find absolute paths referenced in the command that live outside /opt/eden
|
|
but under /opt/ (i.e. plausibly another project on this host)."""
|
|
hits = []
|
|
for m in re.finditer(r"/opt/([a-zA-Z0-9_.\-]+)(/\S*)?", command):
|
|
full = m.group(0)
|
|
if not full.startswith(PROJECT_ROOT):
|
|
hits.append(full)
|
|
return hits
|
|
|
|
|
|
def decide(command: str):
|
|
for pattern, reason in DENY_PATTERNS:
|
|
if re.search(pattern, command, re.IGNORECASE):
|
|
return "deny", reason
|
|
other_projects = outside_project_paths(command)
|
|
if other_projects:
|
|
return "deny", f"referencia caminho fora de {PROJECT_ROOT}: {', '.join(other_projects[:3])}"
|
|
for pattern, reason in ASK_PATTERNS:
|
|
if re.search(pattern, command, re.IGNORECASE):
|
|
return "ask", reason
|
|
return "allow", None
|
|
|
|
|
|
def main():
|
|
try:
|
|
payload = json.load(sys.stdin)
|
|
command = (payload.get("tool_input") or {}).get("command", "") or ""
|
|
except Exception:
|
|
# Fail open: if we can't parse input, don't block the tool call.
|
|
print(json.dumps({}))
|
|
return
|
|
|
|
decision, reason = decide(command)
|
|
if decision == "allow":
|
|
print(json.dumps({}))
|
|
return
|
|
|
|
output = {
|
|
"hookSpecificOutput": {
|
|
"hookEventName": "PreToolUse",
|
|
"permissionDecision": decision,
|
|
"permissionDecisionReason": f"[eden-guard] {reason} (EDEN_MASTER_PROMPT_CLAUDE.md §2.1/§3.4)",
|
|
}
|
|
}
|
|
print(json.dumps(output))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|