KIWI
Autonomous local AI agent and community of voices living in The Grove with private memory.
The Problem
Autonomous AI agents often suffer from leaky memory isolation, unpredictable side-effects on host systems, and lack of human-in-the-loop governance for high-stakes file operations and external device actions.
My Role
Architected and implemented the cognitive engine ("The Grove"), fail-closed host action safety policies, immutable exact-action plan ledgers, multi-device agent companion protocol, and unified multi-channel gateways.
Highlights
- Community of six voices living in The Grove with local memory running on the user's machine
- Local cognitive core ("The Grove") separating owner memory from credentialed client sessions
- Fail-closed security policy requiring immutable exact plans with rollback for all destructive actions
- DPAPI-secured Windows companion agent with local execution receipts and anti-re-execution guarantees
- Multi-modal interaction surface: Next.js Web UI, Voice HUD (:4321), and Telegram bot gateway with inline approvals
- Deterministic audit ledger tracking every state mutation, file movement, and memory association
Stack
Constraints
- Host safety is non-negotiable: no autonomous file deletion or batch reorganization without human-confirmed exact plans.
- Offline-first engine: core cognitive loops and memory databases must run purely locally without cloud dependencies.
- Multi-tenant memory isolation: administrative sessions and API token holders must never access the owner private mind context.
System Architecture
Key Trade-offs
The decisions worth defending — what I chose, what I turned down, and why.
File management execution model
Chose
Immutable two-phase exact-action plans with hash verification and dry-run preview
Rejected
Direct agent tool execution with broad shell permissions
Agents making speculative shell or file modifications risk catastrophic data loss; two-phase plans guarantee human oversight and rollback capability.
Engine-to-web communication architecture
Chose
Dedicated loopback HTTP/SSE gateway with bearer token authentication
Rejected
Monolithic single-process Next.js application
Decoupling the AI engine process from the Next.js frontend ensures background monitoring, telegram polling, and long-running skills survive web redeployments.
Device pairing security model
Chose
Windows DPAPI machine-bound tokens with polling architecture
Rejected
Inbound SSH or remote socket servers on client machines
Inbound listening ports expose clients to local network scans; outbound HTTPS polling behind DPAPI keeps remote companion devices secure.
What I'd Do Differently
An honest retrospective — the stuff I'd change with more time, more users, or a second pass.
- 1Implement automated regression tests for memory graph decay sweeps earlier in the development lifecycle.
- 2Consolidate SQLite schema migrations between better-sqlite3 and pg-schema under a single unified migration runner.
- 3Add fine-grained telemetry dashboards to observe cognitive latency across multi-turn reasoning chains.
Interactive Autonomous Agent Shell
Test live KIWI autonomous agent supervisor routines (agent status, grove ping, mcp tools, help) in the emulated shell below.
Technical Deep-Dive
Architecture, specifications, and implementation details.
P1 — Pilot de dispozitive Windows
Revizia 20 (2026-09-09): rândurile device_tasks/device_pairings erau doar tranziționate de stare, niciodată eliminate — un dispozitiv offline sau un task în stare terminală se acumula la infinit; un reap programat plafonează asta acum. Bucla de reconciliere la restart a companion-ului retrimitea și ea toate dovezile scrise vreodată la fiecare rulare, la infinit; acum sare peste dovezile deja confirmate de server. Găsit într-o trecere independentă de review de arhitectură (Opus, read-only) care a prins și a reparat și o breșă de auto-consistență în commit-ul anterior (două fișiere de care depindeau sursele tracked de dispozitive rămăseseră netracked) și o indentare YAML invalidă în CI (job-ul security nu se parsase niciodată efectiv). Cod local și verificare automată; fără lansare în producție.
#Revizia 20 — sweep de retenție programat și reconciliere la restart mărginită
Retenție. Nimic din web/src/lib/device-control.ts nu ștergea vreodată un rând: stările terminale (succeeded/failed/cancelled/expired/unknown) și codurile de asociere consumate/expirate rămâneau la infinit, iar un dispozitiv care nu se mai reconectează niciodată lăsa task-uri awaiting_approval/approved/running peste expirarea de 5 minute la nesfârșit — poll mătură doar coada propriului dispozitiv la conectare (device-control.ts:121). DeviceControl.reap(retentionDays) e o metodă nouă de întreținere (nu per cerere, nu per cont): rulează întâi același sweep de expirare ca poll, dar pe toate dispozitivele indiferent cine se conectează, apoi șterge task-urile terminale și asocierile consumate/expirate mai vechi decât pragul. Rândurile user_devices nu sunt atinse niciodată — revoked_at e trasabilitatea, nu ceva de aruncat. Agățat de tick-ul zilnic existent (web/src/app/api/admin/tick/route.ts), aceeași formă ca blocul de retenție analitică deja existent (variabilă de mediu implicită, try/catch, log doar la schimbare nenulă); rulează necondiționat, fiindcă tabelele există mereu (revizia 18) și un reap e no-op inofensiv când pilotul n-a fost folosit niciodată. Implicit: DEVICE_TASK_RETENTION_DAYS (30).
Reconciliere mărginită. Bucla de restart a companion-ului (src/devices/agent.ts) citea fiecare dovadă <taskId>.json din %LOCALAPPDATA%\KIWI\companion\receipts și o retrimitea prin POST la agent/result la fiecare run, la infinit, indiferent dacă serverul o acceptase deja — erorile erau înghițite (.catch(() => {})), deci creșterea era invizibilă. local-executor.ts capătă markAcknowledged(stateDir, taskId), care scrie un flag acknowledged: true pe dovada existentă (fișierul însuși, și path/afterSha256/result-ul lui, rămân neatinse — restaurarea e independentă de faptul că serverul a fost deja notificat, deci listRestorable continuă să întoarcă și intrările confirmate). agent.ts îl apelează după un agent/result reușit, atât în bucla de reconciliere la restart cât și la trimiterea normală per task, iar bucla de reconciliere sare acum peste orice dovadă deja marcată acknowledged.
Teste noi: web 4 în web/tests/device-retention.test.ts (task-urile terminale vechi sunt șterse, cele live și cele terminale recente supraviețuiesc, pe două conturi; un task aprobat dar niciodată preluat e măturat la expired și șters doar după ce trece pragul de retenție; asocierile vechi consumate/expirate sunt șterse, una proaspătă supraviețuiește; user_devices nu e niciodată atins), motor 1 în tests/device-execution.test.ts (markAcknowledged păstrează celelalte câmpuri ale dovezii, iar restaurarea funcționează în continuare pe o intrare confirmată). Verificări: 138 teste motor (137 trecute + 1 skip semantic opțional; +1); 237 teste mind/web în 37 suite (+4); ambele typecheck-uri trecute. Fără lansare în producție.
Revizia 19 (2026-09-09): limitele anti-abuz acoperă acum orice rută mutațională (înainte doar preview/claim la asociere), un plafon de task-uri per dispozitiv limitează o coadă nesupravegheată, iar CLI-ul companion capătă un mod restore care revine la o editare deja executată, din backup-ul local. Cod local și verificare automată; fără lansare în producție.
#Revizia 19 — limite de rată pe orice rută mutațională, plafon de task-uri și restaurare din backup
Limite anti-abuz. Doar pairings/preview/pairings/claim aveau rate-limit (20 cereri/min/IP). Orice altă rută mutațională era deschisă: un cont autentificat putea apela /tasks, /tasks/decision, /revoke fără limită, iar un dispozitiv autentificat putea bombarda /agent/poll|verify|result|revoke. web/src/app/api/devices/[[...path]]/route.ts adaugă un helper limited(key, limit) (fereastră de 60s, reutilizează rateLimit existent) pe fiecare mutație rămasă: rutele browser cheiate pe ID-ul contului — pairings 10/min, tasks 30/min, tasks/decision 60/min, revoke 20/min; rutele agent cheiate pe ID-ul dispozitivului, nu IP (fiecare agent trece prin același tunel) — agent/poll 120/min, agent/verify și agent/result 30/min, agent/revoke 20/min.
Plafon de task-uri în așteptare. DeviceControl.propose din web/src/lib/device-control.ts refuză acum o propunere nouă (429) odată ce un dispozitiv are deja MAX_PENDING_TASKS_PER_DEVICE (20) task-uri în awaiting_approval cu termen neexpirat — limita per minut singură nu plafona cât de mare putea crește o coadă nesupravegheată (fiecare task poate căra până la 64 KiB), și acest plafon e independent de limita per minut a tasks și atins înaintea ei în uz normal.
Restaurare din backup. Companion-ul scria deja un fișier .backup înainte de orice editare a unui fișier existent (src/devices/local-executor.ts), dar n-avea cum să-l recitească — „No automatic restore operation exists yet" (revizia 6). Dovada execuției (<taskId>.json) înregistrează acum și path-ul editat și afterSha256-ul conținutului scris, alături de digest/executionId-ul existent; ambele scrieri (dovada inițială wx și actualizarea finală cu rezultatul) le poartă, iar dovezile vechi fără ele sunt tratate explicit ca fiind dinainte de suportul de restaurare, refuzate, nu ghicite. listRestorable(stateDir) scanează dovezile după un path înregistrat, un rezultat succeeded sau unknown (doar acestea ajung la pasul de scriere) și un fișier .backup corespunzător — un fișier nou-creat (fără conținut anterior) e exclus corect, n-are ce să restaureze. restoreFromBackup(taskId, {workspace, stateDir, confirm}) rerezolvă ținta prin aceeași gardă safeTarget de traversare/symlink ca la execuție, refuză dacă fișierul curent nu mai corespunde afterSha256-ului scris exact de acea execuție (cineva l-a schimbat între timp — restaurarea ar arunca tăcut acea schimbare mai nouă), arată o previzualizare înainte/după prin același tipar de confirmare tastată ca APPLY/UNPAIR, și scrie atomic (fișier temporar + rename). Backup-ul rămâne după o restaurare, ca același punct de recuperare să poată fi inspectat sau refolosit din nou. src/devices/agent.ts expune asta ca mod CLI nou restore (listează editările restaurabile, întreabă care, previzualizează, cere tastarea RESTORE); --help și promptul de mod îl menționează.
Teste noi: motor 5 în tests/device-execution.test.ts (restaurarea revine la conținut și păstrează backup-ul; refuză fără scriere când nu e confirmat; refuză când fișierul s-a schimbat de la editare; listRestorable exclude editările de fișiere noi și dovezile dinainte de suportul de restaurare; exclude task-urile anulate/fără dovadă), web 3 în web/tests/device-rate-limits.test.ts (crearea asocierilor e limitată per cont, nu partajată între conturi; plafonul de task-uri se declanșează înaintea și independent de limita per minut de propuneri; endpoint-urile agent sunt limitate per dispozitiv). Verificări: 137 teste motor (136 trecute + 1 skip semantic opțional; +5); 233 teste mind/web în 36 suite (+3); ambele typecheck-uri trecute. Fără lansare în producție.
Revizia 18 (2026-09-09): ștergerea contului (GDPR) elimină acum și rândurile pilotului de dispozitive, iar tabelele lui au paritate de schemă Drizzle/PG; scriind testul pentru asta a ieșit la iveală și s-a corectat un defect preexistent, fără legătură cu dispozitivele, care făcea ca întregul endpoint de ștergere GDPR să eșueze pentru orice cont. Cod local și verificare automată; fără lansare în producție.
#Revizia 18 — rândurile de dispozitiv în ștergerea contului, paritate PG și un defect de tranzacție GDPR
web/src/lib/db/{sqlite-schema.ts,pg-schema.ts} declară acum devicePairings/userDevices/deviceTasks, identice cu SQL-ul brut din device-control.ts (coloane, constrângeri unice, cheile străine user_devices/device_tasks, toate cele trei indexuri). web/src/lib/db/index.ts creează cele trei tabele necondiționat la pornire, la fel ca message_files și celelalte tabele comune — înainte doar constructorul propriu al DeviceControl le crea, lazy, la prima folosire a API-ului de dispozitive (oprit implicit); un cont care s-ar fi șters înainte să deschidă vreodată Dashboard → Devices ar fi lovit o eroare de tabel lipsă pe o cale de cod fără legătură. web/src/app/api/gdpr/delete/route.ts șterge acum device_tasks întâi (referă user_devices fără ON DELETE CASCADE, iar foreign_keys=ON), apoi user_devices, apoi device_pairings, în tranzacția existentă de ștergere a contului.
Scriind testul pentru asta a ieșit la iveală un defect confirmat, preexistent, fără legătură cu dispozitivele: tranzacția folosea db.transaction(async (tx) => { await tx.delete(...) }). Wrapper-ul sincron de tranzacții al better-sqlite3 refuză direct un callback async (Transaction function cannot return a promise) — orice cerere de ștergere GDPR, pentru orice cont, pe toate cele unsprezece tabele originale, eșua cu HTTP 500. Niciun test nu exersase vreodată această rută. Corectat făcând callback-ul sincron și apelând metoda sincronă .run() a query builder-ului la fiecare ștergere, în loc de await.
Teste noi: web/tests/gdpr-delete-devices.test.ts (2 — izolarea rândurilor de dispozitiv între conturi la ștergere; ștergerea reușește pentru un cont care n-a folosit niciodată API-ul de dispozitive, scenariul pe care bug-ul de creare lazy l-ar fi stricat). Verificări: 132 teste motor neschimbat; 230 teste mind/web în 35 suite (+2); ambele typecheck-uri trecute. Fără lansare în producție.
Plan revizia 6, 2026-09-08. Implementat local sub KIWI_DEVICES_ENABLED=1; implicit oprit. Fără deploy sau acceptare pe două PC-uri. P0 rămâne deschis. Pilotul izolat servește testelor de acceptare, fără a elimina criteriile de lansare.
#Scop și instalare
Dashboard → Devices generează un cod de asociere de unică folosință, valabil zece minute. Companion-ul pentru Windows cu Node.js 20+ verifică identitatea contului și cere acceptul local pentru un workspace existent. Trimite cereri spre server prin HTTPS (HTTP numai pe loopback pentru dezvoltare), cu token individual revocabil. Codurile și tokenurile se păstrează numai ca hash SHA-256 pe server; credențialul local este criptat prin Windows CurrentUser DPAPI.
Din rădăcina proiectului: npx tsc -p tsconfig.companion.json, apoi npm pack ./packages/companion; instalarea arhivei rezultate: npm install -g <archive.tgz>. Pornire: kiwi-device pair, apoi kiwi-device run. Fără argument, terminalul oferă pair/run/status/unpair. --help și --version nu necesită TTY. Instalarea npm nu lansează aplicația. Pachetul este privat, versiunea 0.1.0, numit @kiwi-local/companion; proprietatea/publicarea în registry nu sunt stabilite. Execută JavaScript compilat, fără dependență runtime de repository-ul de dezvoltare.
Pilotul permite crearea/înlocuirea unui fișier UTF-8 per sarcină, maximum 64 KiB, în workspace-ul asociat. Fișierele existente cer hash SHA-256 așteptat, cu litere mici; cele noi folosesc null. Hash local: PowerShell Get-FileHash -Algorithm SHA256 -LiteralPath <file>. Comenzile, ștergerea, programele, accesul arbitrar la disc și TUI complet de codare nu sunt implementate.
#Aprobare și execuție
Proprietarul selectează explicit dispozitivul și verifică pe website calea/conținutul. Aprobarea leagă contul, dispozitivul, amprenta canonică a acțiunii și expirarea sarcinii în cinci minute. Agentul arată conținutul înainte/după și cere APPLY, apoi reverifică autorizarea pe server și hash-ul local. Refuzul, conținutul schimbat, expirarea și revocarea împiedică o scriere nouă.
Înainte de execuție agentul înregistrează o dovadă locală; repetarea cererii nu repetă operația. Fișierele existente primesc backup. Crearea folosește o operație atomică fără suprascriere. După deconectare/restart agentul raportează rezultatul salvat sau necunoscut; serverul nu redistribuie sarcina în execuție. Revocarea anulează sarcinile în coadă și marchează necunoscute cele în execuție. Nu anulează o scriere deja începută. Un mod restore manual, pornit de operator (revizia 19) poate reveni la o editare succeeded/unknown din backup-ul ei, refuzând dacă fișierul s-a schimbat din nou de atunci; rămâne inexistentă restaurarea automată/pornită de server.
Sunt respinse ieșirea relativă din workspace, căile absolute, numele rezervate Windows, symlink/junction observate și țintele cu hard link. Directoarele părinte trebuie să existe. Verificările nu sunt certificate împotriva unui proces local ostil care schimbă căile între controale. Dovezile/backupurile sunt în %LOCALAPPDATA%\KIWI\companion\receipts. kiwi-device unpair revocă tokenul și șterge credențialul local criptat după UNPAIR, păstrând backupurile. Dezinstalare: npm uninstall -g @kiwi-local/companion.
#API și SQLite
Bază: /api/devices; JSON limitat la 100.000 octeți în timpul citirii. Când funcția este oprită, rutele întorc 404. Mutațiile browser cer sesiune, lipsa Authorization și Origin al site-ului configurat prin AUTH_URL/NEXTAUTH_URL (originea cererii numai ca rezervă de dezvoltare); Host redirecționat nu îl poate suprascrie. Numai endpoint-urile POST exacte de asociere/agent delegă autentificarea din middleware către rută.
| Endpoint | Autorizare | Rol |
|---|---|---|
GET /api/devices | Sesiune cont | Dispozitivele proprii și ultimele 100 de sarcini |
POST /pairings | Sesiune + Origin, 10 cereri/min/cont | Cod de unică folosință |
POST /pairings/preview, /pairings/claim | Cod de asociere, 20 cereri/min/IP | Identitate cont; asociere Windows atomică |
POST /tasks | Sesiune + Origin, 30 cereri/min/cont, 20 în așteptare/dispozitiv | {deviceId, action:{kind:'write_file',path,content,expectedSha256}} |
POST /tasks/decision | Sesiune + Origin, 60 cereri/min/cont | {id,digest,approve:boolean} |
POST /revoke | Sesiune + Origin, 20 cereri/min/cont | {deviceId} |
POST /agent/poll | Token dispozitiv Bearer, 120 cereri/min/dispozitiv | Preia o sarcină aprobată cel mult o dată |
POST /agent/verify | Token dispozitiv Bearer, 30 cereri/min/dispozitiv | Reverifică {id,executionId,digest} înainte de scriere |
POST /agent/result | Token dispozitiv Bearer, 30 cereri/min/dispozitiv | Aceeași identitate și {outcome,message}; rezultatele identice sunt idempotente |
POST /agent/revoke | Token dispozitiv Bearer, 20 cereri/min/dispozitiv | Revocă numai acest dispozitiv |
web/src/lib/device-control.ts creează device_pairings, user_devices, device_tasks în constructorul propriu; web/src/lib/db/index.ts le creează și el necondiționat la pornirea web-ului (revizia 18), deci există chiar înainte ca API-ul (oprit implicit) să fie vreodată folosit. ID-uri text, chei străine spre cont, hash-uri ale credențialelor, indexuri pentru proprietar/coadă; timpii sarcinilor sunt întregi în milisecunde. Stări: awaiting_approval, approved, running, succeeded, failed, cancelled, expired, unknown. Tranzacțiile SQLite serializează asocierea, deciziile, preluarea și rezultatele. Un dispozitiv e plafonat la 20 de task-uri awaiting_approval neexpirate (revizia 19). DeviceControl.reap (revizia 20), rulat din tick-ul zilnic, șterge task-urile în stare terminală și asocierile consumate/expirate mai vechi decât DEVICE_TASK_RETENTION_DAYS (implicit 30) — rândurile user_devices se păstrează oricum, ca trasabilitate a revocărilor. Paritate de schemă Drizzle/PG de la revizia 18 (devicePairings/userDevices/deviceTasks); ștergerea contului (GDPR) elimină toate cele trei după ID-ul utilizatorului. Conținutul/rezultatele sunt private contului, fără acces din camerele comune.
#Dovezi și criterii rămase
- Testele SQLite temporare reale verifică două conturi/dispozitive, coduri expirate/reutilizate, decizii exacte, restart, lipsa redistribuirii, rezultate idempotente și revocare. Integrarea handler-elor modifică un fișier Windows temporar real pentru A, inaccesibil lui B; autentificarea și aprobarea locală sunt simulate.
- Testele middleware verifică accesul agentului la autentificarea rutei și mențin login-ul pentru aprobările browser. Executorul este verificat pentru refuz, alterare, fișiere schimbate, backup și junction Windows real. DPAPI verifică un credențial sintetic.
- Verificare web curentă: 177 teste mind/web în 27 suite, typecheck web și compilare companion trecute. Reper motor anterior: 80 trecute, unul semantic opțional sărit, typecheck motor trecut. Instalarea/help/version/dezinstalarea arhivei companion au trecut anterior într-un prefix temporar izolat; corecțiile interne ulterioare au fost recompilate/testate, fără repetarea instalării arhivei. Fără credențiale live sau fișiere reale ale utilizatorului.
- Revizia 6: build-ul Next de producție izolat și proba Edge/HTTP reală au trecut. Două sesiuni sintetice semnate au asociat dispozitive logice distincte; interfața a aprobat modificarea exactă pentru A, B nu a putut aproba/vedea sarcina, fișierul temporar s-a schimbat numai în workspace-ul A, rezultatele au supraviețuit restartului serverului de probă, refuzul a prevenit execuția și revocarea a întors 401. APPLY local a fost simulat pe acest singur PC. Vezi comenzile reproductibile și dovezile păstrate.
- Rămân: două sisteme Windows reale, teste interactive/întreruperi de rețea, instalare/actualizare pe sistem curat, distribuție semnată, review și lansare controlată în producție. Revizia 19 închide golul de limite anti-abuz pentru cereri/cozi și adaugă o cale manuală de restaurare din backup (ambele doar in-memory/single-process — o lansare de producție multi-instanță tot are nevoie de un magazin de rate-limit partajat, conform comentariului propriu din
web/src/lib/rate-limit.ts); revizia 20 închide golurile de creștere nelimitată a rândurilor și de retrimitere nemărginită din ciclul de viață al datelor. Interfața cere momentan pachetul construit local; nu există descărcare publică. Acceptarea P2–P6 rămâne deschisă.
Surse: src/devices/, packages/companion/, web/src/lib/device-control.ts, web/src/app/api/devices/[[...path]]/route.ts, web/src/app/(dashboard)/dashboard/devices/, contractul stării.