Skip to content
←Return to Projects
Case Study

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

TypeScriptNext.js 15Node.jsSQLiteDrizzle ORMDPAPITelegram Bot APIDocker

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

Interaction Channels
Next.js 15 Web App (:3333)
Voice HUD (:4321)
Telegram Bot
Embed Bridge (:3234)
Gateway & Protocol
Private Grove API (:8431)
Device Control API
DPAPI Token Auth
Cognitive Engine ("The Grove")
Mind Core
Behavior & Persona
Memory Tiering (STM/MTM/LTM)
Skill Registry
Execution & Safety
Host Action Policy
Exact-Action Ledger
Backup & Rollback Ledger
Distributed Node Runtime
Windows Companion Daemon
Local File Executor
SQLite / Drizzle State

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.

  1. 1Implement automated regression tests for memory graph decay sweeps earlier in the development lifecycle.
  2. 2Consolidate SQLite schema migrations between better-sqlite3 and pg-schema under a single unified migration runner.
  3. 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.

icelegends-interactive-shell ~ [kiwi]
History: ↑ / ↓
kiwi@the-grove:~/voices$

Technical Deep-Dive

Architecture, specifications, and implementation details.

P2 — Distribuție și clientul CLI

Revizia 21, 2026-09-09. Prima felie reală de P2, aleasă fiindcă operatorul n-are un Windows disponibil în această sesiune (acceptarea fizică a pilotului P1 de dispozitive Windows rămâne ea însăși blocată) și ownership-ul numelui de pachet (npm install kiwi) e nerezolvat — vezi „Confirmat pe registry-ul npm" mai jos. Cod local și verificare automată; fără lansare în producție, fără pachet publicat.

#Defect confirmat și reparat — abonații Pro nu puteau folosi CLI-ul pentru care plătesc

web/src/app/(dashboard)/dashboard/cli/page.tsx e blocat cu requirePlan('pro'), iar textul spune „Pro plan — Kiwi in your terminal, next to your code"; clientul livrat web/public/kiwi.mjs însuși afișează „No machine token issued — CLI access requires the Pro plan." la eșec; web/src/lib/utils.ts's PLANS.pro.features listează explicit „CLI access — connect Kiwi to your terminal" ca feature de nivel Pro. Toate trei sunt independent de acord că pragul e Pro. Dar POST /api/os/login (web/src/app/api/os/login/route.ts) emitea un token de mașină doar când plan === 'enterprise' && status === 'active', sau când contul n-avea niciun rând de abonament (o excepție legacy). Un abonat Pro activ, plătitor, se autentifica cu succes și nu primea niciun token — incapabil să folosească feature-ul pe care propriul lui plan îl promovează.

Reparat folosind allowedForPlan(plan, status).cli (web/src/lib/plan-guard.ts) — aceeași regulă care deja, corect, blochează pagina /dashboard/cli (paid = planul e pro sau enterprise, status activ) — în loc de verificarea proprie a rutei, mai îngustă, doar-Enterprise. Excepția legacy !sub a rămas neschimbată. Am verificat că asta nu lărgește un feature Enterprise-only nelegat: hasEnterprise() din web/src/app/api/download/iso/route.ts reinterogă independent abonamentul apelantului la fiecare cerere, în loc să presupună că un token de agent valid implică vreun plan anume — deci un token emis pentru Pro tot primește 403 Kiwi OS is an Enterprise feature acolo. /api/chat și /api/kiwi/status (cele două endpoint-uri pe care kiwi.mjs chiar le apelează) rezolvă identitatea prin currentUserIdFrom, fără nicio reverificare de plan — orice token de agent valid funcționa deja pentru ele; singura poartă reală a fost mereu la emiterea tokenului.

Teste noi: web/tests/os-login-entitlement.test.ts (7 — un Pro activ primește un token care chiar autentifică /api/chat; un Enterprise activ funcționează în continuare; un cont legacy fără abonament funcționează în continuare; un Pro anulat și un Free activ nu primesc token; parola greșită e respinsă indiferent de plan; un token emis pentru Pro tot e refuzat de endpoint-ul ISO, blocat independent).

#Întărirea clientului — web/public/kiwi.mjs

Clientul CLI livrat, fără dependențe, descărcabil prin curl, avea patru probleme, niciuna acoperită vreodată de vreun test:

  • main() rula necondiționat la importul modulului, făcând fișierul netestabil fără să declanșeze cereri de rețea reale și process.exit. Acum păzit: const isMain = argv[1] && import.meta.url === pathToFileURL(argv[1]).href; if (isMain) main()... — importul modulului (cum face noul fișier de test) nu rulează nimic.
  • Prompt-ul de parolă folosea readline obișnuit, care ecouă fiecare caracter tastat pe ecran. askHidden() pune stdin în mod raw și îl consumă manual (Enter trimite, Backspace/DEL editează, Ctrl+C iese ca restul CLI-ului), revenind la prompt-ul vizibil existent când stdin nu e un TTY (nimic de ascuns de la un apelant neinteractiv, iar modul raw cere un terminal interactiv).
  • KIWI_API permitea suprascrierea originii serverului cu orice, inclusiv http:// simplu, în timp ce răspunsul poartă un token Bearer de lungă durată — o origine necriptată l-ar fi scurs. resolveApiOrigin() cere acum HTTPS, cu excepția unei gazde loopback, oglindind exact regula pe care serverOrigin() a companion-ului Windows (src/devices/agent.ts) o aplică deja pentru același motiv. Fiecare apel de rețea a mai câștigat un timeout de 15s și redirect: 'error', din nou ca la companion.
  • Textele controlate de server (mesaje de eroare, răspunsuri de chat, câmpuri de status) erau afișate pe ecran nesanitizate. clean() elimină exact același interval de octeți de control (\x00-\x08, \x0b-\x1f, \x7f) pe care clean()-ul propriu al companion-ului din src/devices/agent.ts îl elimină — scoate octetul ESC care transformă octeții următori într-o secvență de control activă, nu o pretenție de a elimina orice urmă vizibilă a uneia.

Adăugat și: --version (lipsea; kiwi-device din companion are deja unul).

Teste noi: web/tests/kiwi-cli-client.test.ts (5 — respinge origini non-HTTPS/non-loopback, acceptă HTTPS și HTTP loopback, implicit originea de producție, elimină octeții de control, exportă un string de versiune). Prompt-ul TTY interactiv și apelurile reale de rețea nu sunt testate unitar — aceeași graniță ca executeFileEdit-ul companion-ului (testat) față de shell-ul lui condus de readline (netestat).

#Poarta de împachetare — packages/companion și un fix de artefact învechit

packages/companion/dist/ (companion-ul @kiwi-local/companion compilat) era învechit la intrarea în această sesiune — precede modul restore din revizia 19 și fix-ul de reconciliere markAcknowledged din revizia 20, fiindcă nimic din munca anterioară a sesiunii nu-l recompilase (npm run test/typecheck verifică doar src/, niciodată ieșirea compilată a companion-ului). Recompilat (npx tsc -p tsconfig.companion.json) și testat rapid (--help/--version). packages/companion/dist/ e în .gitignore — un artefact de build, nu ceva de comis; ținerea lui la zi e o grijă de build-time, adresată de scriptul nou din această secțiune, de acum înainte.

Noul scripts/verify-companion-package.mjs (npm run verify:companion) e o poartă de împachetare care rulează pe orice OS, fiindcă împachetarea/inspectarea unui tarball e neutră la platformă chiar dacă pachetul însuși doar se instalează pe win32 (os:["win32"] din package.json, cerut de stocarea de credențiale DPAPI de care src/devices/credentials.ts are nevoie). Verifică, în ordine: (1) pachetul se compilează curat din src/devices/*; (2) nu există niciun script preinstall/install/postinstall/prepare/prepublish/prepack — planul P2 interzice explicit lansarea unei aplicații interactive dintr-un hook de instalare npm, iar asta transformă interdicția într-un invariant verificat, nu doar o afirmație de o singură dată; (3) lista reală de fișiere din npm pack --dry-run --json chiar conține intrarea bin declarată și fiecare intrare files declarată — nu doar ce pretinde package.json; (4) ieșirea --version compilată se potrivește cu câmpul version din package.json, exact clasa de drift a problemei de mai sus cu dist-ul învechit; (5) --help menționează fiecare mod documentat. Doar pe win32, mai face un ciclu real npm pack → npm install -g <tarball> --prefix <director temporar izolat> → --help → npm uninstall → confirmă că bin-ul din prefix e gol; pe orice alt OS acest pas e afișat ca sărit, cu motivul (câmpul propriu os al npm refuză instalarea acolo — un refuz corect, nu un gol de acoperit). Rulat pe acest Windows: toate cele 10 verificări trecute, inclusiv ciclul real de instalare/dezinstalare (raport JSON stil .verify-work afișat la stdout, codul de ieșire reflectă trecut/eșuat).

Am eliminat și "bin": {"kiwi": "./src/cli/kiwi.ts"} din package.json-ul rădăcină — repository-ul e "private": true și n-a fost instalat niciodată pe acea cale; câmpul indica un fișier .ts brut, fără shebang, care nu poate rula efectiv ca script bin, iar singura cale reală, susținută, de a rula acel fișier e npm run cli (node --import tsx src/cli/kiwi.ts, chiar din propriul docstring al src/cli/kiwi.ts). Asta n-a schimbat cum se invocă CLI-ul de dezvoltare — a eliminat o declarație care n-a funcționat niciodată și nu era punctul de intrare documentat al fișierului.

#Ce este, și ce NU este src/cli/kiwi.ts

src/cli/kiwi.ts (npm run cli) e un instrument de chat terminal separat, preexistent, cu un singur operator, care vorbește direct cu createKiwi() (obiectul motor local, în proces) pe acest calculator — fără login de cont, fără apel API remote, nu e multi-tenant. Nu e candidat să devină produsul distribuibil P2 fără să se decidă întâi să apeleze API-ul autentificat al web-ului de la distanță, în loc de motorul în proces — asta e ea însăși o decizie reală de design, pe care această revizie n-a luat-o.

#Verificări

Motor: 138 teste (137 trecute + 1 skip semantic opțional; neschimbat — src/ în afara src/devices/ neatins). Web/mind: 249 teste în 39 suite (+12 teste, +2 suite: os-login-entitlement.test.ts, kiwi-cli-client.test.ts). Ambele typecheck-uri trecute. npm run verify:companion: 10/10 verificări trecute pe acest Windows, inclusiv ciclul real de instalare/dezinstalare. Fără lansare în producție; fix-ul de entitlement din os/login nu a fost deployat.

#Ce rămâne în P2

TUI complet de codare/aprobare-diff (deliberat în afara scopului acestei revizii — e înlocuitorul sigur, viitor, al lui dev_task, un efort de design separat, sensibil la securitate); ownership-ul numelui/registry-ului npm (kiwi e deja luat de altcineva pe registry-ul public, confirmat prin npm view kiwi; scope-ul @kiwi-local e liber, dar cererea lui e o acțiune de cont npmjs.com, nu de cod); experiența TUI de login browser dincolo de fluxul existent kiwi.mjs login; distribuție publică și orice verificare reală de instalare/actualizare pe sistem curat (nu doar prefix izolat); review de cod; lansare controlată.

Surse: web/src/app/api/os/login/route.ts, web/src/lib/plan-guard.ts, web/src/lib/agent-keys.ts, web/src/app/api/download/iso/route.ts, web/public/kiwi.mjs, packages/companion/, scripts/verify-companion-package.mjs, pilot Windows, checkpoint.

#Revizia 38 (2026-09-10): kiwisan publicat pe npm; un bug real de login în producție găsit și reparat; felia 1 a TUI-ului complet

Întrebarea numelui npm, rezolvată efectiv. kiwi (fără scope) e definitiv ocupat pe registry-ul public de un pachet terț, activ, întreținut (un template engine) — confirmat imposibil de obținut (nu e abandon, nu e dispută de marcă pe care npm ar acționa-o). La cererea operatorului, revendicat kiwisan în loc de asta și publicat live sub contul propriu: packages/kiwi-cli/ împachetează scriptul kiwi.mjs deja securizat ca binar global real kiwisan, nu un placeholder. npm install -g kiwisan verificat end-to-end direct pe registry-ul real. packages/companion rămâne privat/local, per decizia explicită a operatorului — nimic din el n-a fost publicat.

Un bug real, live, care rupea login-ul în producție, găsit și reparat pe drum, printr-un pas de investigație Opus (per regula de workflow standard a acestei sesiuni — orice bug raportat primește întâi un diagnostic Opus read-only, nu un fix auto-diagnosticat): lista PUBLIC_API din web/src/middleware.ts nu a inclus niciodată /api/os/login — orice tentativă de login neautentificată (singurul tip pe care-l primește vreodată acel endpoint, fiindcă tot scopul lui e să emită primul token) era interceptată la nivel de middleware cu un {"error":"Unauthorized"} generic, înainte ca logica reală de email+parolă/bcrypt a rutei să ruleze vreodată. Un blocaj de tip ou-și-găină: ai nevoie de token ca să obții un token. Login-ul CLI n-a funcționat niciodată în producție, pentru nimeni, până la acest fix. Nu era problemă de plan/entitlement (contul care l-a lovit era deja enterprise/active) și nici de logica rutei înseși (mesajele ei reale de eroare sunt "Invalid credentials"/"Email and password are required", niciodată "Unauthorized" — dovadă că cererea n-a ajuns niciodată la ea). os-login-entitlement.test.ts n-a putut prinde asta: apelează route.POST() direct, ocolind complet middleware-ul. Noul web/tests/os-login-middleware.test.ts verifică că POST-ul neautentificat trece acum, și că o cale os/* neînrudită rămâne protejată. Reparat adăugând string-ul exact '/api/os/login' (nu un prefix /api/os/) în PUBLIC_API. Deployat în producție și verificat live prin curl ({"error":"Invalid credentials"} la credențiale greșite, nu mai "Unauthorized").

Două crash-uri reale, live, pe Windows, în kiwi.mjs însuși, găsite și reparate în aceeași zi, de operator, lovindu-le într-un terminal real: (1) login() citea email-ul printr-o instanță readline.Interface (creată/închisă la fiecare apel) și parola prin manipulare directă stdin.setRawMode() imediat după — pe Windows, închiderea handle-ului stdin al unei interfețe readline și comutarea modului raw pe același handle imediat după se cursează cu propria curățenie asincronă a lui libuv (Assertion failed: !(handle->flags & UV_HANDLE_CLOSING), src/win/async.c). Reparat rutând ambele prompt-uri printr-o singură implementare raw-mode (promptLine), fără să se mai toace readline.createInterface deloc în timpul login-ului. (2) Același crash s-a reprodus totuși după aceea, dintr-o cauză diferită: process.exit() cursându-se cu închiderea ORICĂRUI handle asincron — o comutare raw-mode, un timer de timeout de la fetch, un socket — nu doar readline specific, chiar după ce un login eșuat își afișa eroarea și apela exit(1) în urma unui fetch() așteptat. Reparat eliminând complet apelurile process.exit() din fișier: fiecare cale de ieșire aruncă acum CliExit(code), prins o singură dată la finalul fișierului, care setează process.exitCode și lasă Node să se termine natural odată ce bucla de evenimente se golește.

Felia 1 din cererea operatorului pentru TUI complet, planificată de un pas Opus și apoi construită: kiwisan/kiwi.mjs intră acum într-un cadru persistent, cu ANSI scris manual (buffer de ecran alternativ, același personaj pixelat 16×16 pe care-l arată și desktop-ul web /os, un transcript cu istoric, o linie de input încadrată, o bară de status) după un login reușit sau când e rulat gol într-un terminal real — input-ul pipe, output-ul pipe și mesajul unic kiwisan "mesaj" rămân complet neafectate, folosind exact aceleași căi de cod ca înainte. Doar chat: fără editare de fișiere, fără execuție de comenzi shell — asta rămâne o felie deliberat separată, viitoare, revizuită din perspectiva securității, menită să se afișeze prin executorul de aprobare deja livrat (src/devices/local-executor.ts, din pilotul P1 de dispozitive), nu unul nou. Persona fixată pe forge (Papa Ko) implicit, prin sintaxa de mențiune deja funcțională @forge <mesaj> (detectIntent din chat-router.ts) — nicio schimbare de server pentru asta; /persona <nume> comută printre toate cele șase. Vezi docs/adr/0011-kiwisan-tui-hand-rolled-ansi.md pentru motivul pentru care e ANSI scris manual, nu o librărie de terminal (canalul de distribuție prin curl al web/public/kiwi.mjs nu poate structural căra dependențe npm, iar librăriile din familia ink conduc exact suprafața de stdin raw-mode care a crăpat deja de două ori). Noul scripts/verify-cli-mirror.mjs (npm run verify:cli) — cele două copii kiwi.mjs n-aveau nicio verificare automată de sincronizare înainte de asta; acum eșuează dacă diverg dincolo de textul cunoscut specific canalului, sau dacă VERSION se dezalinieze de packages/kiwi-cli/package.json (a prins exact acest drift o dată, chiar în timp ce scriam această felie).

Verificări: npm run verify:imports — 678 fișiere curat. npm run verify:cli — curat. web/tests/kiwi-cli-client.test.ts extins la 15 teste (erau 5), acoperind funcțiile pure noi (renderSprite, wrapToWidth, composeFrame, visibleLen, padRight) — inclusiv un test de regresie pentru un bug real de buclă infinită pe care wrapToWidth îl avea în prima versiune (un cuvânt mai lung decât lățimea panoului, când linia curentă era deja goală). npm run test:mind — 52 suite / 364 teste, toate curate. npx tsc --noEmit (web) curat.

~ End of Document ~