← Indice documentazione Microprogettazione › observability

myclaw

observability — logging, audit, metrics, health
Microprogettazione v1.0 — 22 aprile 2026
Quarto e ultimo documento di fase 2.
Reifica la Legge 3 (tracciabilità).

Pubblico: chi implementa il logging e l'audit. Lettura: 18 min.

Indice

  1. Scopo: la Legge 3 in forma operativa
  2. Tre fonti, tre registri
  3. Audit log (append-only JSONL)
  4. Structured logging (operativo)
  5. Metrics (Prometheus-style)
  6. Health check
  7. Activity digest (umanamente leggibile)
  8. Backup e rotation
  9. Contratto Python
  10. Test di conformità
  11. Riferimenti

1. Scopo: la Legge 3 in forma operativa

La Legge 3 — tracciabilità — dice che myclaw preserva il proprio audit log e non occulta le proprie azioni. Qui la implementiamo: append-only, firmata, queryable, backed-up. Più tre canali complementari: structured logging per debug, metrics per monitoring, digest umano perché Roberto possa chiedere a colpo d'occhio "cosa ha combinato oggi?".

Cosa copre

Cosa non copre

2. Tre fonti, tre registri

FonteRegistroConsumatori
Trace + decisioni (agent runtime)Audit JSONL in workspace/.audit/Legge 3, eval, fitness darwiniana, digest, forensica
Eventi operativi (errori, warnings, stati)structlog → stdout → journaldDebug, journalctl, troubleshooting
Counter numerici (trace count, cost, latency)Prometheus metrics su /metricsMonitoring, dashboard
Perché tre canali separati: audit è legge, structured log è per debuggare il codice, metrics è per vedere tendenze. Accorpare sarebbe comodo ma sbagliato: l'audit non deve essere influenzato dal livello di log configurato, le metrics non devono perdere precisione per format JSON.

Gerarchia delle sorgenti di verità

Più componenti scrivono journal (neuroni, sinapsi, trace, approvazioni). Per evitare che il lettore li tratti come equivalenti, qui la gerarchia canonica:

LivelloSorgenteChi scriveDerivabile?
1 — ombrello.audit/*.jsonl (trace, approval, workspace_edit, constitution_modified)Observability — scrive eventi a valle di Gateway/Runtime/Approvalno: è la fonte autoritativa
2 — rollupOggetto ExecutionTrace JSONL immutabile per-turno (contenuto dentro .audit)Runtime a fine turno, poi Observability lo inoltra al JSONLno: è la proiezione per-turno del livello 1
3 — derivatoepisodic.db (memory), co_activations.sqlite (neuron §6), grafo sinapsi (synapse)Memory / Neuron / Synapse come propri worker: in caso di perdita, ricostruibili da (1)+(2) con un replay
Implicazione operativa. Il backup giornaliero copia il livello 1 e basta. I livelli 2 e 3 vengono ricostruiti on-demand. Questo semplifica la retention e impedisce di avere tre verità diverse su "quante volte il neurone foo ha fallito": la risposta è sempre nel livello 1.

3. Audit log (append-only JSONL)

Layout

/opt/myclaw/workspace/.audit/
├── 2026-04.jsonl              # traces del mese corrente
├── 2026-03.jsonl              # precedenti
├── approvals.jsonl            # storico approvazioni (separato per query veloci)
├── workspace.jsonl            # modifiche ai 5 file markdown
├── constitution.jsonl         # modifiche a SOUL.md (raro, importante)
├── budget.json                # contatori correnti (non log)
└── archived/                  # episodi retention-spostati da memory

Formato record

// esempio: un trace chiusa
{
  "kind": "trace",
  "trace_id": "01HQWK3...",
  "session_id": "s_4a7b...",
  "channel": "telegram:@roberto",
  "started_at": "2026-04-22T14:03:22Z",
  "finished_at": "2026-04-22T14:03:28Z",
  "user_message": "scarica il rapporto di marzo",
  "final_response": "Fatto. Scaricati 2.4 MB in ~/downloads/rapporto_marzo.pdf.",
  "steps_count": 5,
  "tool_calls": [
    {"name": "web_fetch", "args_summary": "drive.google.com/...", "outcome": "success"},
    {"name": "fs_write", "args_summary": "~/downloads/rapporto_marzo.pdf", "outcome": "success"}
  ],
  "cost_usd": 0.023,
  "wall_time_ms": 5840,
  "outcome": "success",
  "approvals_requested": 1,
  "approvals_granted": 1
}

// esempio: una approvazione
{
  "kind": "approval",
  "request_id": "ar_...",
  "trace_id": "01HQWK3...",
  "sender": "telegram:@roberto",
  "tool_name": "fs_write",
  "effect_class": "fs_write:~/downloads/*",
  "granted": true,
  "via": "batch",
  "batch_window_expires_at": "2026-04-22T14:13:22Z",
  "at": "2026-04-22T14:03:24Z"
}

// esempio: modifica workspace
{
  "kind": "workspace_edit",
  "file": "MEMORY.md",
  "author": "agent|manual",
  "trace_id": "01HQWK3...",              // null se manual
  "size_before": 3421,
  "size_after": 3498,
  "diff_stat": "+3 -0",
  "approved_by_request_id": "ar_...",
  "at": "2026-04-22T14:03:28Z"
}

// esempio: constitution
{
  "kind": "constitution_modified",
  "hash_before": "sha256:...",
  "hash_after": "sha256:...",
  "diff": "...",                         // pieno
  "at": "2026-04-22T22:15:00Z",
  "notes": "Added Legge 4 omeostasi"
}

Append-only: garanzie

Nessun percorso di codice può cancellare l'audit. Questa è la difesa contro la Legge 3: se un neurone malevolo o una sintesi fallita tentasse rm -f .audit/*, viene bloccato al livello sandbox (forbidden path implicito — l'agente non vede .audit/ del tutto per design).

4. Structured logging (operativo)

Per il debug e il troubleshooting. Non sostituisce l'audit.

import structlog

logger = structlog.get_logger(__name__)

# uso tipico
logger.info(
    "tool_call_completed",
    trace_id=str(trace.trace_id),
    tool_name="fs_read",
    wall_ms=42,
)

logger.warning(
    "rate_limit_hit",
    sender=sender,
    counter=current,
    limit=limit,
)

Destinazioni

Livelli e filtro

LivelloEsempioDefault production
debugDump completo prompt LLMoff
infoEventi normali: tool call, turn start/endon
warningRate limit, budget approaching, retryon
errorTool failure, policy deny con contestoon
criticalConstitution modified, audit corruption detectionon

5. Metrics (Prometheus-style)

Endpoint: GET /metrics (auth admin). Formato Prometheus text exposition.

# HELP myclaw_traces_total Numero totale di trace chiuse
# TYPE myclaw_traces_total counter
myclaw_traces_total{outcome="success"} 1247
myclaw_traces_total{outcome="tool_hallucination"} 3
myclaw_traces_total{outcome="policy_denied"} 12
myclaw_traces_total{outcome="user_aborted"} 5
myclaw_traces_total{outcome="budget_hit"} 1
myclaw_traces_total{outcome="timeout_steps"} 2

# HELP myclaw_cost_eur_today Costo aggregato di oggi
# TYPE myclaw_cost_eur_today gauge
myclaw_cost_eur_today 1.23

# HELP myclaw_trace_latency_ms Latenza wall della trace (p50/p95/p99)
# TYPE myclaw_trace_latency_ms summary
myclaw_trace_latency_ms{quantile="0.5"} 2100
myclaw_trace_latency_ms{quantile="0.95"} 6200
myclaw_trace_latency_ms{quantile="0.99"} 11400

# HELP myclaw_active_sessions Sessioni attualmente attive
# TYPE myclaw_active_sessions gauge
myclaw_active_sessions{channel="telegram"} 1
myclaw_active_sessions{channel="cli"} 0

# HELP myclaw_budget_remaining_eur Budget rimanente oggi
# TYPE myclaw_budget_remaining_eur gauge
myclaw_budget_remaining_eur{cap="soft"} 0.77
myclaw_budget_remaining_eur{cap="hard"} 3.77

# HELP myclaw_tool_calls_total Chiamate per tool
# TYPE myclaw_tool_calls_total counter
myclaw_tool_calls_total{tool="fs_read"} 423
myclaw_tool_calls_total{tool="shell_run"} 128
myclaw_tool_calls_total{tool="web_fetch"} 67
# ...

# HELP myclaw_approvals_total Approvazioni chieste/concesse/negate
# TYPE myclaw_approvals_total counter
myclaw_approvals_total{verdict="granted",via="explicit"} 89
myclaw_approvals_total{verdict="granted",via="batch"} 214
myclaw_approvals_total{verdict="denied"} 7
myclaw_approvals_total{verdict="timeout"} 3

# HELP myclaw_llm_tier_calls_total Chiamate LLM per tier
# TYPE myclaw_llm_tier_calls_total counter
myclaw_llm_tier_calls_total{tier="local-fast"} 1876
myclaw_llm_tier_calls_total{tier="frontier"} 1203
DECISIONE v1: niente TimescaleDB o storage esterno per le metrics. Il gateway espone /metrics e basta. Se un giorno Roberto vuole dashboard ricche, un Prometheus esterno scrapa. Intanto, ordine di grandezza + trend dall'output text è sufficiente.

6. Health check

GET /health

{
  "status": "ok",                       // "ok" | "degraded" | "down"
  "version": "0.0.1",
  "uptime_s": 86430,
  "gateway_pid": 12345,

  "workspace": {
    "mounted": true,
    "soul_hash_matches": true           // SOUL.md non modificato senza restart
  },

  "suprastructure": {
    "reachable": true,
    "llm_default_provider": "anthropic",
    "stt_default": "faster-whisper",
    "last_successful_call_at": "2026-04-22T14:02:18Z"
  },

  "channels": {
    "cli": "listening",
    "telegram": "pump_active"
  },

  "memory": {
    "episodic_db_size_mb": 8.2,
    "embeddings_db_size_mb": 3.4,
    "last_reflection": "2026-04-22T03:00:00Z",
    "pending_proposals": 7
  },

  "budget": {
    "soft_cap_eur": 2.0,
    "hard_cap_eur": 5.0,
    "spent_today_eur": 1.23,
    "override_active": false
  },

  "recent_errors_1h": 0
}

status è "degraded" se: budget soft hit, un canale non risponde, supra non raggiungibile ma riprovabile. "down" se: SOUL.md manca, workspace non scrivibile, supra irraggiungibile per > 5 minuti.

7. Activity digest (umanamente leggibile)

Ogni sera alle 22:00 (o quando Roberto chiede myclaw digest today): un messaggio strutturato, breve, con l'essenziale. Questa è la reificazione della critica #5 del giudizio (audit JSONL illeggibile come "cosa ha fatto oggi il mio maggiordomo").

myclaw · digest · 2026-04-22

📨 Richieste gestite: 23
  ├─ 21 completate con successo
  ├─ 1 annullata (tu hai negato)
  └─ 1 fallita (tool hallucination, LLM inventava un tool)

🔧 Tool usati più di 3 volte:
  - fs_read         (8)
  - supra_llm       (23 — ogni Thought)
  - web_fetch       (4)
  - shell_run       (5)

✅ Approvazioni:
  - 14 concesse esplicitamente
  - 3 approvate via batch (fs_write:~/downloads/*)
  - 1 negata (tentativo di scrivere in /opt/suprastructure/)

💰 Costo: 1.23 € (soft cap 2€)
⏱  Latenza p95: 6.2s (stabile)

🧠 Memoria:
  - 23 nuovi episodi indicizzati
  - 7 proposte di reflection pronte (digest mattutino approverà)

⚠ Cose notevoli:
  - Alle 14:03 tentativo di indirect prompt injection da una pagina web
    (pattern "IGNORA ISTRUZIONI" intercettato dal boundary untrusted). Nessun effetto.
  - Alle 19:47 Tutor mode è scattato per fs_write:~/downloads/*
    (10a approvazione consecutiva). Tu hai confermato, tutto regolare.

Nessuna constitution o workspace edit.
🟢 stato: regolare.

Il digest viene costruito da un job cron notturno che legge audit JSONL del giorno e produce il testo via LLM tier summarize-rich (~0.01€ per digest) con template strutturato.

8. Backup e rotation

Audit rotation

Backup periodico via systemd timer

# ~/.config/systemd/user/myclaw-backup.service
[Unit]
Description=myclaw daily backup
Requires=myclaw-gateway.service

[Service]
Type=oneshot
ExecStart=/bin/bash -c '\
  tar czf /var/backups/myclaw/workspace-$(date +%%Y%%m%%d-%%H%%M).tar.gz \
    /opt/myclaw/workspace/ \
  && find /var/backups/myclaw/ -name "workspace-*.tar.gz" -mtime +30 \
    ! -newermt $(date -d "first day of month" +%%Y-%%m-%%d) \
    -delete'

# ~/.config/systemd/user/myclaw-backup.timer
[Unit]
Description=Daily workspace backup

[Timer]
OnCalendar=daily
Persistent=true

[Install]
WantedBy=timers.target

Retention: ultimi 30 daily + primo di ogni mese per 12 mesi.

9. Contratto Python

from typing import Protocol, Literal

class AuditLog(Protocol):
    async def append(self, record: dict) -> None:
        """Append atomico di un record JSON. record['kind'] obbligatorio."""
        ...

    async def query(
        self,
        kind: str | None = None,
        since: datetime | None = None,
        until: datetime | None = None,
        sender: str | None = None,
        limit: int = 100,
    ) -> list[dict]: ...

    async def verify_integrity(self) -> IntegrityReport:
        """Controlla gaps nei timestamp, JSON malformed, size anomalies."""
        ...

class Metrics(Protocol):
    def inc(self, name: str, labels: dict | None = None, value: float = 1.0) -> None: ...
    def gauge(self, name: str, value: float, labels: dict | None = None) -> None: ...
    def observe(self, name: str, value: float, labels: dict | None = None) -> None: ...
    def render_prometheus(self) -> str: ...

class HealthChecker(Protocol):
    async def check(self) -> HealthStatus: ...

@dataclass
class HealthStatus:
    status: Literal["ok", "degraded", "down"]
    details: dict
    checked_at: datetime

class DigestBuilder(Protocol):
    async def build_daily(self, date: date, sender: str) -> str:
        """Costruisce il digest markdown per un giorno specifico."""
        ...

10. Test di conformità

InvarianteTest
Append atomico2 task concorrenti che appendono 1000 record → file finale ha esattamente 2000 righe valide JSON, ordine temporale coerente.
No delete sull'auditTentativo di rm .audit/* dalla sandbox → bloccato (.audit non bindato nel bwrap).
Integrity check passa su file validoFile JSONL corretto → verify_integrity() ritorna ok=True.
Integrity check segnala gapFile con un record corrotto → report con corrupted_line_numbers.
Metrics endpoint richiede authGET /metrics senza bearer token → 401.
Health down se SOUL.md mancaRinominare SOUL.md → /health restituisce status=down entro 30s.
Backup timer attivosystemctl --user list-timers mostra myclaw-backup.timer.
Digest produttibile offlinemyclaw digest 2026-04-22 funziona anche se supra è down (usa tier local-fast).
Rotation mensilePrimo del mese alle 00:01 → file YYYY-MM.jsonl rinominato .gz, nuovo file nascere.
Records monotoni in timestampAppend di 100 record in 10s → ogni record ha at ≥ del precedente.

11. Riferimenti

RiferimentoCosa abbiamo preso
OpenHands event streamAppend-only come pattern primario di trace.
Prometheus exposition formatMetrics naming convention.
structlogLogging strutturato.
systemd.journaldLog destination + rotation integrata.
Giudizio di fase 0 — critica #5 UIAudit leggibile come "cosa ha fatto oggi" (§7 activity digest).
Legge 3 (tracciabilità)Il motivo per cui esiste questo doc.
Memory §4Trace chiuse → episodes; stessa sorgente serve due consumer.
Policy §5budget.json è scritto dalla Policy, letto da metrics + health + digest.

Continua a leggere

prossimo · fase 3
pairing
Primo doc di fase 3: come gli sconosciuti su canali multi-utente ottengono permesso.
microprogettazione · 20 min
memory
Dove le trace indicizzate da qui alimentano la episodic memory.
indice
Landing
Fase 2 completa. 11/15.

myclaw — observability microprogettazione v1.0 — 2026-04-22
Fase 2 completa. Prossimo: pairing.html (fase 3).