Bootstrap EDEN: Fase 0 (arquitetura) e Fase 1 (monorepo + infra)
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>
This commit is contained in:
84
.claude/hooks/guard_bash.py
Executable file
84
.claude/hooks/guard_bash.py
Executable file
@@ -0,0 +1,84 @@
|
||||
#!/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()
|
||||
77
.claude/hooks/guard_file.py
Executable file
77
.claude/hooks/guard_file.py
Executable file
@@ -0,0 +1,77 @@
|
||||
#!/usr/bin/env python3
|
||||
"""PreToolUse guard for Read/Write/Edit/Glob/Grep (EDEN, Master Prompt §2.1/§3.4).
|
||||
|
||||
Blocks access to paths outside the project root and to ~/.ssh or /etc, and
|
||||
asks for confirmation before reading a .env/credential-looking file. Fails
|
||||
open on any internal error.
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
|
||||
PROJECT_ROOT = "/opt/eden"
|
||||
HOME = os.path.expanduser("~")
|
||||
|
||||
SECRET_NAME_RE = re.compile(r"(^|/)(\.env(\..+)?|.*credential.*|.*secret.*)$", re.IGNORECASE)
|
||||
|
||||
|
||||
def extract_path(tool_input: dict) -> str:
|
||||
for key in ("file_path", "path", "notebook_path"):
|
||||
if key in tool_input:
|
||||
return tool_input[key] or ""
|
||||
# Grep/Glob use "path" for the search root; pattern itself isn't a filesystem path.
|
||||
return ""
|
||||
|
||||
|
||||
def decide(path: str):
|
||||
if not path:
|
||||
return "allow", None
|
||||
abspath = os.path.abspath(path)
|
||||
|
||||
ssh_dir = os.path.join(HOME, ".ssh")
|
||||
if abspath == ssh_dir or abspath.startswith(ssh_dir + os.sep):
|
||||
return "deny", "acesso a ~/.ssh"
|
||||
if abspath == "/etc" or abspath.startswith("/etc" + os.sep):
|
||||
return "deny", "acesso a /etc"
|
||||
if abspath == "/root" or abspath.startswith("/root" + os.sep):
|
||||
return "deny", "acesso a /root"
|
||||
|
||||
if abspath.startswith("/opt/") and not (
|
||||
abspath == PROJECT_ROOT or abspath.startswith(PROJECT_ROOT + os.sep)
|
||||
):
|
||||
return "deny", f"caminho fora de {PROJECT_ROOT} (outro projeto no host)"
|
||||
|
||||
basename = os.path.basename(abspath)
|
||||
if SECRET_NAME_RE.match(basename):
|
||||
return "ask", f"leitura/escrita de arquivo que parece conter segredo ({basename})"
|
||||
|
||||
return "allow", None
|
||||
|
||||
|
||||
def main():
|
||||
try:
|
||||
payload = json.load(sys.stdin)
|
||||
tool_input = payload.get("tool_input") or {}
|
||||
path = extract_path(tool_input)
|
||||
except Exception:
|
||||
print(json.dumps({}))
|
||||
return
|
||||
|
||||
decision, reason = decide(path)
|
||||
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()
|
||||
44
.claude/hooks/post_write_check.sh
Executable file
44
.claude/hooks/post_write_check.sh
Executable file
@@ -0,0 +1,44 @@
|
||||
#!/usr/bin/env bash
|
||||
# PostToolUse hook for Write|Edit (EDEN, Master Prompt §3.4).
|
||||
# Best-effort incremental lint/typecheck for files under apps/, packages/, infra/.
|
||||
# Fails open: if pnpm/node/turbo aren't installed yet (Fase 0 / early Fase 1),
|
||||
# this prints a short notice and exits 0 without blocking anything.
|
||||
set -euo pipefail
|
||||
|
||||
PROJECT_ROOT="/opt/eden"
|
||||
INPUT_JSON="$(cat)"
|
||||
|
||||
FILE_PATH="$(python3 -c '
|
||||
import json, sys
|
||||
try:
|
||||
data = json.load(sys.stdin)
|
||||
ti = data.get("tool_input") or {}
|
||||
print(ti.get("file_path") or ti.get("notebook_path") or "")
|
||||
except Exception:
|
||||
print("")
|
||||
' <<< "$INPUT_JSON")"
|
||||
|
||||
# Only act on files under apps/, packages/, or infra/.
|
||||
case "$FILE_PATH" in
|
||||
"$PROJECT_ROOT"/apps/*|"$PROJECT_ROOT"/packages/*|"$PROJECT_ROOT"/infra/*) ;;
|
||||
*) exit 0 ;;
|
||||
esac
|
||||
|
||||
if ! command -v pnpm >/dev/null 2>&1; then
|
||||
echo '{"systemMessage":"[eden-hook] pnpm ainda não está instalado neste ambiente — lint/typecheck incremental pulado (esperado na Fase 0/início da Fase 1)."}'
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ ! -f "$PROJECT_ROOT/package.json" ]; then
|
||||
echo '{"systemMessage":"[eden-hook] Monorepo ainda não inicializado (sem package.json na raiz) — lint/typecheck incremental pulado."}'
|
||||
exit 0
|
||||
fi
|
||||
|
||||
cd "$PROJECT_ROOT"
|
||||
if pnpm turbo run lint typecheck --filter="...[HEAD^1]" >/tmp/eden-post-write-check.log 2>&1; then
|
||||
exit 0
|
||||
else
|
||||
TAIL="$(tail -n 20 /tmp/eden-post-write-check.log | tr '\n' ' ' | cut -c1-800)"
|
||||
echo "{\"systemMessage\":\"[eden-hook] lint/typecheck incremental falhou: ${TAIL}\"}"
|
||||
exit 0
|
||||
fi
|
||||
35
.claude/hooks/stop_secret_check.sh
Executable file
35
.claude/hooks/stop_secret_check.sh
Executable file
@@ -0,0 +1,35 @@
|
||||
#!/usr/bin/env bash
|
||||
# Stop hook (EDEN, Master Prompt §2.2/§3.4). Advisory only — never blocks.
|
||||
# If the project is under git, scans the working tree diff (staged+unstaged)
|
||||
# for obvious secret patterns and for critical TODO/FIXME markers introduced
|
||||
# without a tracking reference. Silent (no output) when there's nothing to
|
||||
# flag or when git/the repo isn't set up yet (expected in Fase 0).
|
||||
set -uo pipefail
|
||||
|
||||
PROJECT_ROOT="/opt/eden"
|
||||
cd "$PROJECT_ROOT" 2>/dev/null || exit 0
|
||||
|
||||
if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
DIFF="$(git diff HEAD 2>/dev/null; git diff --cached 2>/dev/null)"
|
||||
[ -z "$DIFF" ] && exit 0
|
||||
|
||||
SECRET_HITS="$(echo "$DIFF" | grep -E -i '^\+.*(AKIA[0-9A-Z]{16}|BEGIN (RSA|EC|OPENSSH|PRIVATE) KEY|password\s*=\s*["'"'"'][^"'"'"']+|api[_-]?key\s*=\s*["'"'"'][^"'"'"']+|secret\s*=\s*["'"'"'][^"'"'"']+)' || true)"
|
||||
TODO_HITS="$(echo "$DIFF" | grep -E -i '^\+.*(TODO|FIXME).*(CRITICAL|SECURITY|URGENT)' || true)"
|
||||
|
||||
if [ -z "$SECRET_HITS" ] && [ -z "$TODO_HITS" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
MSG="[eden-hook] Verificação de fim de etapa (Master Prompt DoD):"
|
||||
if [ -n "$SECRET_HITS" ]; then
|
||||
MSG="$MSG Possível segredo em claro no diff (revisar antes de commitar)."
|
||||
fi
|
||||
if [ -n "$TODO_HITS" ]; then
|
||||
MSG="$MSG TODO/FIXME crítico introduzido sem rastreamento (issue/ADR)."
|
||||
fi
|
||||
|
||||
python3 -c "import json,sys; print(json.dumps({'systemMessage': sys.argv[1]}))" "$MSG"
|
||||
exit 0
|
||||
Reference in New Issue
Block a user