neuron — anatomia di una capacità auto-sintetizzata
Un neurone è la materializzazione di una capacità che myclaw
non aveva e che ha sintetizzato al bisogno. Questo documento definisce la sua
anatomia: come è fatto, come vive, come è firmato. La pipeline di
sintesi è in synthesizer.html; il
grafo dei rapporti fra neuroni è in synapse.html.
Ogni neurone vive in una directory dedicata sotto
workspace/neurons/<nome>/. La directory è self-contained:
corpo, manifest, test, firma e journal stanno insieme. Per spostare o
archiviare un neurone basta spostare la sua directory.
workspace/neurons/log-parser/
├── manifest.yaml # schema dichiarativo (capability, quote, sinapsi)
├── body.py # il codice Python: funzione run(...)
├── test_birth.py # test di nascita (deve passare in sandbox)
├── signature.hmac # HMAC-SHA256 su manifest+body
├── journal.sqlite # counter, esiti, sinapsi osservate
└── README.md # opzionale: dettagli generati dal synthesizer
# workspace/neurons/log-parser/manifest.yaml
name: log-parser # ^[a-z][a-z0-9_-]{2,31}$, univoco
version: 1.4.0 # semver
created_at: 2026-04-22T21:11:00+02:00
created_by: synthesizer@myclaw # o "manual" se scritto a mano
parent_trace: 7c3f...d21 # trace_id che ha innescato la sintesi
purpose: |
Estrae errori e warning da log strutturati in JSONL,
raggruppa per servizio e ritorna un riassunto.
# --- contratto di invocazione ---
entry: body:run # modulo:funzione
input_schema: # JSON Schema subset
type: object
required: [path]
properties:
path: { type: string, format: "local-readable-path" }
since: { type: string, format: "date-time" }
output_schema:
type: object
required: [by_service]
properties:
by_service: { type: object }
# --- capability richieste (match esatto con policy.html §4) ---
capabilities:
- fs-read:~/logs
- fs-read:~/downloads/logs
# no network, no shell, no fs-write
# --- quote al runtime ---
quotas:
wall_time_s: 20
cpu_time_s: 10
ram_mb: 256
max_stdout_kb: 512
max_invocations_per_hour: 60
# --- sinapsi dichiarate (vincolo massimo; vedi synapse.html) ---
may_call:
- summarizer # può chiedere un riassunto LLM
- fs-read # tool base
# --- metadati per retrieval ---
tags: [log, observability, parsing]
embeddings_version: supra-embed-v1 # per invalidare l'index se cambia il modello
| Campo | Vincolo |
|---|---|
name | slug, univoco nel workspace. Case-sensitive NO. |
version | semver stretto. Bump patch per fix, minor per estensione input compatibile, major per breaking. |
purpose | Linguaggio naturale, 1–4 frasi. Usato per retrieval semantico. |
entry | Formato modulo:funzione. Funzione DEVE essere async. |
input_schema / output_schema | JSON Schema Draft 2020-12. Validato a ogni invocazione. |
capabilities | Lista di capability. Ciascuna risolve rispetto al registro in policy §4. Vuota → neurone puramente computazionale (no I/O). |
quotas | Limiti applicati in sandbox. Default conservativi se omessi (15s wall, 128 MB ram). |
may_call | Lista di neuroni/tool base che il corpo può invocare. Chiamate fuori da questa lista → errore UndeclaredSynapseError. |
jsonschema e ci protegge dai neuroni che "ritornano
quello che capita".
Tool
Il corpo di un neurone implementa lo stesso Protocol Python dei tool nativi
(tool.html §3). Non c'è distinzione a livello
di runtime: un neurone è un tool che vive nel filesystem invece che in
src/myclaw/tools/.
# workspace/neurons/log-parser/body.py
from myclaw.runtime import NeuronContext, ToolResult
async def run(ctx: NeuronContext, path: str, since: str | None = None) -> ToolResult:
# ctx espone: ctx.open(path, "r"), ctx.call("summarizer", ...),
# ctx.log, ctx.quota_remaining(). Niente import di sistema diretti.
data = {}
async with ctx.open(path, "r") as f:
async for line in f:
rec = ctx.json_loads(line)
if since and rec["ts"] < since: continue
if rec.get("level") in ("ERROR", "WARN"):
data.setdefault(rec["service"], []).append(rec)
return ToolResult(ok=True, value={"by_service": data})
import dalla stdlib o dal namespace myclaw.runtime. Qualsiasi altro import fa fallire l'analisi statica.os, subprocess, socket. L'I/O passa da ctx.run sempre async, firma tipata.payload = manifest.yaml_bytes + b"\n---\n"
+ body.py_bytes + b"\n---\n"
+ test_birth.py_bytes
signature = HMAC-SHA256(key=neuron_signing_key, msg=payload)
# scritto come hex in signature.hmac, una sola riga, trailing newline.
~/.config/myclaw/neuron.key, 32 byte random, mode 0600.myclaw init. Mai versionata, mai esportata.myclaw neuron rotate-key → rifirma tutti i neuroni attivi dopo richiesta di approvazione singola (batch).neuron.signature.invalid.body.py non ha la chiave, quindi
la firma non torna, quindi al prossimo load il neurone è disattivato. HMAC è
sufficiente, semplice, veloce. Se un domani dovessimo condividere neuroni
fuori dal workspace, passeremo a firma asimmetrica (Ed25519) con ceremony di
trust esplicita.
Il journal è uno SQLite per neurone. Tenere i diari separati evita contention e permette di spostare/archiviare una directory senza toccare un DB centrale. Il grafo globale — unione dei journal — è costruito dal componente synapse a partire da queste sorgenti.
-- workspace/neurons/<nome>/journal.sqlite
CREATE TABLE invocations (
invocation_id TEXT PRIMARY KEY,
trace_id TEXT NOT NULL,
started_at DATETIME NOT NULL,
duration_ms INTEGER NOT NULL,
outcome TEXT NOT NULL, -- success | error | timeout | quota_exceeded
error_class TEXT,
input_hash TEXT, -- sha256 input normalizzato (privacy)
goal_hash TEXT, -- sha256 dello scopo corrente (per Gap)
gap_pre REAL, -- riempito dal caller
gap_post REAL,
cost_cents REAL DEFAULT 0.0
);
CREATE TABLE co_activations (
trace_id TEXT NOT NULL,
peer_name TEXT NOT NULL, -- neurone chiamato nella stessa trace
direction TEXT NOT NULL, -- 'calls' | 'called_by'
observed_at DATETIME NOT NULL
);
CREATE INDEX idx_inv_time ON invocations(started_at DESC);
CREATE INDEX idx_coact_peer ON co_activations(peer_name, observed_at);
L'utilità di una singola invocazione (vedi
synapse §3) è u = gap_pre - gap_post;
il journal non pre-aggrega per non congelare la formula.
| Stato | Condizione di entrata | Effetto operativo |
|---|---|---|
| neonato | Firmato da < 7 giorni e < 20 invocazioni. | Selezionato con priorità per retrieval esplorativo; fitness non pesa ancora. |
| attivo | Firma valida, fitness ≥ soglia, silenzio < 90 gg. | Candidato normale nell'arena di selezione. |
| dormiente | 90 giorni senza invocazioni. | Scompare dal retriever normale; ancora raggiungibile via myclaw neuron invoke <name>. |
| quarantena | Firma mismatch o 3 strike consecutivi (outcome=error nelle ultime N invocazioni) o capability scaduta. | Non invocabile. Appare in myclaw neuron status con motivo. Revoca o riabilitazione richiedono approvazione esplicita. |
| estinto | 30 giorni di quarantena senza riabilitazione, o rimozione manuale. | Directory spostata in .audit/neurons_extinct/YYYY-MM/. File di sola lettura, preservato per forensica. |
from typing import Protocol, Literal
from dataclasses import dataclass
from datetime import datetime
@dataclass(frozen=True)
class NeuronManifest:
name: str
version: str
purpose: str
entry: str # "body:run"
input_schema: dict
output_schema: dict
capabilities: list[str]
quotas: dict
may_call: list[str]
tags: list[str]
created_at: datetime
parent_trace: str | None
NeuronState = Literal["neonato", "attivo", "dormiente", "quarantena", "estinto"]
@dataclass
class NeuronHandle:
manifest: NeuronManifest
path: str # workspace/neurons/<nome>/
state: NeuronState
signature_ok: bool
last_invocation: datetime | None
class NeuronLoader(Protocol):
async def load_all(self) -> list[NeuronHandle]: ...
async def reload(self, name: str) -> NeuronHandle: ...
async def verify(self, name: str) -> bool:
"""Ricalcola HMAC, confronta con signature.hmac, aggiorna stato."""
...
async def invoke(
self,
name: str,
ctx: "NeuronContext",
**kwargs,
) -> "ToolResult":
"""Valida input, esegue in sandbox, registra nel journal. Solleva
SignatureError, QuotaExceededError, UndeclaredSynapseError, ..."""
...
async def set_state(self, name: str, state: NeuronState, reason: str) -> None: ...
# Errori sollevabili dal loader
class SignatureError(Exception): ...
class UndeclaredSynapseError(Exception): ...
class QuotaExceededError(Exception): ...
class NeuronNotFoundError(Exception): ...
class InputSchemaError(Exception): ...
class OutputSchemaError(Exception): ...
| Aspetto | Scelta v1 | Motivazione |
|---|---|---|
| Storage manifest | YAML (ruamel.yaml) | Commenti preservati; human-editable in caso di review manuale. |
| Schema validator | jsonschema Draft 2020-12 | Standard, zero-dep pesante, errori leggibili. |
| Firma | HMAC-SHA256 (stdlib hmac) | Semplice, basta per rilevare tampering locale (vedi §5). |
| Journal | SQLite per-neurone | Nessuna contention cross-neurone; scriptabile via CLI sqlite3. |
| Loader | Scan eager all'avvio + watcher inotify | Hot-reload senza riavvio del gateway. |
| Analisi statica | AST whitelist + ruff/bandit | Dettagli in synthesizer §5. |
| Alternativa | Perché scartata (o rimandata) |
|---|---|
Un monolite di neuroni in un unico neurons.db | Perdiamo la possibilità di spostare/archiviare un neurone come unità atomica. L'audit file-by-file diventa ostico. |
| WASM invece di Python | Isolamento migliore ma tooling più pesante, perdita di accesso ai tool base del runtime. Rimandato a una v2 eventuale. |
| Firma asimmetrica (Ed25519) | Overhead di gestione chiavi pubbliche; non ci serve fin quando i neuroni restano locali. Pronti a migrare quando servirà condividere. |
| Manifest in JSON | Meno leggibile nei diff, nessun commento. YAML vince per use-case human-in-the-loop. |
| Giornale centralizzato in Postgres | Dipendenza pesante per uso domestico. SQLite basta per anni. |
| Nessuna distinzione "neonato" | Senza un periodo di prova, la fitness fragile dei nuovi neuroni li farebbe estinguere subito. Corregge un bias di selezione (early-mortality). |
| Invariante | Test |
|---|---|
| Manifest obbligatori presenti | Caricare un neurone senza name/entry/capabilities → InputSchemaError all'avvio. |
| Firma valida = attivo | Dopo load_all(), handle.signature_ok = True, state ∈ {neonato, attivo, dormiente}. |
| Tampering = quarantena | Modificare 1 byte di body.py → verify() False, state = quarantena, evento neuron.signature.invalid emesso. |
| Input schema enforced | Invocazione con input mancante → InputSchemaError, nessuna esecuzione, nessun entry in invocations. |
| Output schema enforced | Body che ritorna oggetto non conforme → OutputSchemaError, outcome error loggato. |
| Capability enforcement | Body che tenta fs-read fuori dalla whitelist → sandbox blocca, outcome error, error_class = "CapabilityDenied". |
| Sinapsi dichiarata | Body che chiama un neurone non in may_call → UndeclaredSynapseError. |
| Quote enforced | Body con time.sleep(wall_time_s+1) → timeout, outcome timeout, quota_exceeded=true. |
| Journal append-only | Dopo K invocazioni: SELECT COUNT(*) FROM invocations = K, nessun DELETE/UPDATE tollerato. |
| Hot reload | Rifirma dopo bump di versione + touch manifest.yaml → il loader ricarica senza riavvio entro 2s. |
| Transizione a quarantena dopo 3 strike | 3 outcome=error consecutivi → state = quarantena automaticamente. |
| Journal fuori dal payload firmato | Bump di invocations non invalida la firma (il journal non è nel payload HMAC). |
| Riferimento | Cosa abbiamo preso |
|---|---|
| Neuroni+Memoria v1.1 §2, §3, §10 | Definizione narrativa di "neurone", ciclo di sintesi, anatomia. |
| Voyager (Wang et al. 2023) | Skill library persistente come pattern fondamentale. |
| Toolformer / Gorilla | Tool-schema validation (input/output) come prima difesa. |
| CoALA §procedural memory | Collocazione dei neuroni nella tassonomia della memoria. |
| Synthesizer | Pipeline che produce corpo + manifest + test (doc dedicato). |
| Synapse | Come il journal di ogni neurone contribuisce al grafo globale. |
| Policy §4 | Registro delle capability e vincoli applicabili. |
| Sandbox | Profilo bwrap «tight» che esegue body e test. |
myclaw — neuron microprogettazione v1.0 — 2026-04-22
Primo doc dell'estensione neuroni. Prossimo: synthesizer.html.