Skip to content

Architecture — Jarela

C4 — Container

C4Container
    title Containers — Jarela
    Person(user, "Developer", "Browser / installed PWA")
    Person_Ext(wa_user, "WhatsApp peer", "Phone number paired via Baileys")

    System_Boundary(b, "Jarela (Next.js process)") {
    Container(ui, "Web UI", "React 19 + Tailwind", "Chat, agents, models, memory, connections, and tools panels")
      Container(guard, "Origin / CSRF Guard", "lib/auth", "Rejects cross-origin mutating requests; same-origin enforcement")
      Container(routes, "API Routes", "Next.js Route Handlers", "REST + SSE endpoints under /api/v1")
      Container(agents, "Agent Runtime", "LangGraph + @langchain/*", "State-machine orchestration of LLM + tools; streams completions through providers")
      Container(providers, "Provider Adapters", "lib/providers", "Per-vendor SDK glue (Anthropic, OpenAI, Google, Cohere, DeepSeek)")
      Container(embed, "Embeddings", "lib/embeddings", "Vector embedding generator for semantic memory recall")
      Container(docs, "Document RAG", "lib/documents (+ lib/documents/remote)", "Folder indexer + remote indexers for Jira/Confluence; chunked + embedded into the documents tables; surfaced via documents_search (ADR-0024, ADR-0026)")
      Container(voice, "Voice", "lib/voice", "Gemini STT (push-to-talk) + TTS for the generate_voice tool (ADR-0017)")
      Container(mcp, "MCP Adapter", "@langchain/mcp-adapters", "Discovers & invokes external MCP tool servers")
      Container(sched, "Scheduler", "cron-parser", "Runs scheduled tasks + polls event-driven watchers; persists in DB (ADR-0022, ADR-0025, ADR-0027)")
      Container(bridges, "Bridges", "lib/bridges", "Inbound transports (WhatsApp/Baileys) routed to agents")
      Container(registry, "Run Registry", "lib/agents/run-registry", "In-memory pub/sub of in-flight agent chunks; replay buffer for reattaching EventSource clients")
      Container(crypto, "Crypto Envelope", "lib/crypto", "AES-GCM-at-rest for sensitive memory + OAuth tokens; OS keychain or .secret-key fallback")
      Container(proxy, "Proxy Dispatcher", "lib/proxy", "undici GlobalDispatcher; reads HTTP_PROXY env vars + encrypted proxy_config row; gates all outbound HTTP (ADR-0009)")
      Container(envsync, "Env Sync", "lib/env", "Probes user shell rc / Windows User-scope env on boot + on demand: writes credential vars into the encrypted store (ADR-0016, ADR-0034) and caches the full env for every spawned subprocess — exec, terminal, claude_delegate, MCP stdio children (ADR-0072)")
      ContainerDb(db, "SQLite", "@langchain/langgraph-checkpoint-sqlite + native sqlite", "Checkpoints, memory, settings, schedules, proposals, bridges — at ~/.jarela")
      ContainerDb(filestore, "File Store", "lib/files + ~/.jarela/files/", "Content-addressed image/file refs plus generated binary artifacts; served by /api/v1/files/[name]")
      ContainerDb(extdir, "Extension dirs", "filesystem (~/.jarela/{providers,tools}/)", "Drop-in .cjs files for external providers + tools, hot-loaded per request (ADR-0013)")
    }

    System_Ext(anthropic, "Anthropic", "Claude")
    System_Ext(openai, "OpenAI", "GPT")
    System_Ext(google, "Google GenAI", "Gemini (LLM + STT/TTS)")
    System_Ext(deepseek, "DeepSeek", "OpenAI-compatible")
    System_Ext(cohere, "Cohere", "Embeddings")
    System_Ext(mcps, "MCP Servers", "External tool providers (stdio / SSE)")
    System_Ext(mcpreg, "MCP Registry", "registry.modelcontextprotocol.io — discovery only (ADR-0014)")
    System_Ext(github, "GitHub API", "Issues / PRs / Repos (native github_* tools, ADR-0015) + Copilot OAuth (model provider)")
    System_Ext(atlassian, "Atlassian Cloud", "Jira REST + Confluence REST (tools + document-RAG ingest, ADR-0026)")
    System_Ext(whatsapp, "WhatsApp Web", "Baileys-paired endpoint")
    System_Ext(usershell, "User shell rc / Windows User env", "Source for credential env vars (ADR-0016) and, since ADR-0072, the full env every spawned subprocess inherits")
    System_Ext(browserext, "Jarela Browser Extension", "Chrome MV3 — element picker, posts captures to localhost (ADR-0018)")

    Rel(user, ui, "HTTPS")
    Rel(ui, guard, "fetch + EventSource")
    Rel(guard, routes, "allow same-origin")
    Rel(routes, agents, "invoke")
    Rel(routes, voice, "STT/TTS + generate_voice")
    Rel(agents, mcp, "tool calls")
    Rel(agents, providers, "stream completion")
    Rel(agents, registry, "broadcast chunks")
    Rel(routes, registry, "subscribe (GET SSE) / submit (POST 202)")
    Rel(routes, filestore, "POST /attachments/{images,files} + GET /files/[name]")
    Rel(routes, sched, "register / trigger")
    Rel(sched, agents, "run scheduled job")
    Rel(wa_user, whatsapp, "message")
    Rel(whatsapp, bridges, "stream events")
    Rel(bridges, agents, "deliver as user turn")
    Rel(agents, db, "checkpoint / memory (via crypto)")
    Rel(agents, crypto, "encrypt sensitive at rest")
    Rel(agents, embed, "embed memory writes")
    Rel(agents, filestore, "materialize attachment refs for provider calls")
    Rel(routes, filestore, "GET /api/v1/files/[name]")
    Rel(crypto, db, "store ciphertext")
    Rel(providers, anthropic, "HTTPS")
    Rel(providers, openai, "HTTPS")
    Rel(providers, google, "HTTPS")
    Rel(providers, deepseek, "HTTPS")
    Rel(voice, google, "HTTPS (Gemini STT/TTS)")
    Rel(embed, cohere, "HTTPS (embed)")
    Rel(mcp, mcps, "stdio / SSE")
    Rel(routes, mcpreg, "HTTPS (picker search)")
    Rel(routes, github, "HTTPS")
    Rel(routes, atlassian, "HTTPS (tools)")
    Rel(sched, docs, "sweep ~10 min")
    Rel(docs, atlassian, "HTTPS (remote indexer)")
    Rel(docs, embed, "embed new chunks")
    Rel(docs, db, "document_sources / documents / document_chunks")
    Rel(agents, docs, "documents_* tools")
    Rel(agents, extdir, "scan per request (cache-busted require)")
    Rel(envsync, usershell, "read on boot")
    Rel(envsync, db, "write encrypted via crypto")
    Rel(proxy, db, "read proxy_config (via crypto)")
    Rel(providers, proxy, "outbound via GlobalDispatcher")
    Rel(routes, proxy, "outbound via GlobalDispatcher")
    Rel(browserext, routes, "POST /api/v1/page-capture (loopback)")

Shared utilities

Two cross-cutting helper layers sit underneath every container above. They are not their own boundary on the diagram — every container reaches into them — but they are load-bearing:

  • lib/utils/ — getOrCreateGlobal (singleton pinning across HMR), parseJsonSafe, stripHtml, truncateBytes, createOAuthFlowStore. Replaces ~13 duplication patterns surfaced by the audit.
  • lib/api/ — errorResponse / notFoundResponse / createdResponse / validateBody and the per-resource *ToResponse row→JSON serializers shared between list and [id] route handlers.

Both layers are pure-logic and 100% line-covered by Vitest.

Architecture audit snapshot

The 2026-08 whole-repo audit found four areas that should shape future refactors and dependency upgrades:

  • State ownership: UI-only preferences still have direct localStorage writers. Any new persistent state should go through lib/stores or an explicit documented browser-cache exception.
  • Route validation: API routes mix direct zod safeParse() calls with the shared validateBody() helper. New route handlers should use the helper so error bodies stay consistent.
  • Tool/plugin capability metadata: built-in tools declare read/write/execute capability tiers, while external .cjs tools still default to execute. Extension-contract changes here should be handled with an ADR and deprecation window.
  • Secrets and external process boundaries: credentials, MCP env, proxy config, and hot-loaded plugins remain the highest-risk security surfaces. Upgrades must preserve encryption/redaction and the single Next.js process invariant.

C4 — Component (Agent Runtime)

flowchart LR
    A[API Route /api/v1/*] --> G0[Origin Guard<br/>lib/auth]
    G0 --> B[Agent Factory<br/>lib/agents]
    B --> C[Provider Adapter<br/>lib/providers]
    B --> D[Tool Registry<br/>lib/tools<br/>category × capability]
    B --> E[MCP Client<br/>lib/mcp]
    C --> F[(LLM Provider)]
    C --> XP[External providers<br/>~/.jarela/providers/*.cjs<br/>hot-loaded]
    D --> G[Built-in tools]
    D --> XT[External tools<br/>~/.jarela/tools/*.cjs<br/>hot-loaded]
    D --> EM[Embeddings<br/>lib/embeddings]
    D --> FS[File Store<br/>lib/files<br/>~/.jarela/files/]
    A --> V[Voice<br/>lib/voice<br/>STT + TTS]
    V --> F
    EM --> F
    E --> H[(External MCP servers)]
    B --> I[Checkpoint Store<br/>lib/db]
    I --> J[(SQLite ~/.jarela)]
    B --> K[Memory Store<br/>lib/stores]
    B --> HR[Harness Resolver<br/>lib/agents/harness]
    HR --> K
    B --> PR[Prepare<br/>lib/agents/prepare<br/>system prompt + history window]
    PR --> B
    B --> OV[Output Validator<br/>lib/agents/output-validator]
    OV -.post-turn check.-> B
    K --> CR[Crypto Envelope<br/>lib/crypto]
    CR --> J
    K --> J
    B --> L[Notifications<br/>lib/notifications]
    BR[Bridges<br/>lib/bridges] --> B
    SC[Scheduler<br/>lib/scheduler] --> B
    EN[Env Sync<br/>lib/env<br/>boot probe + Sync button] --> K
    EN -."full shell env<br/>(ADR-0072)".-> D
    EN -."full shell env<br/>(ADR-0072)".-> E
    RR[Run Registry<br/>lib/agents/run-registry] -.pub/sub + replay buffer.-> A

Key Flow — User sends a chat turn

sequenceDiagram
    actor U as User
    participant UI as Web UI
    participant G as Origin Guard
    participant ATT as /api/v1/attachments
    participant API as /api/v1/threads/:id/run
    participant PIN as /api/v1/threads/:id/context-pin
    participant AG as Agent Runtime
    participant DB as SQLite
    participant LLM as LLM Provider

    U->>UI: drags context boundary line
    UI->>PIN: PATCH { hot_since } (ADR-0042)
    PIN->>DB: UPDATE threads.hot_since
    PIN-->>UI: { hot_since, warm_summary, ... }
    Note over UI: Card shows placeholder<br/>until next run recomputes summary

    U->>UI: types message + adds attachments
    opt image / binary file attachment
      UI->>ATT: POST multipart file
      ATT->>DB: persist metadata in message refs later
      ATT-->>UI: image_ref / file_ref
    end
    UI->>G: POST /threads/:id/run (submit, hot_since)
    G->>G: check Origin / Sec-Fetch-Site
    G->>API: forward if same-origin
    API->>AG: startRun + invoke(threadId, msg, hot_since)
    AG->>DB: load checkpoint + thread.hot_since
    AG->>AG: buildHistoryWindow honours hot_since
    AG->>ATT: materialize refs when provider needs bytes
    opt warm_summary_before ≠ hot_since
        AG->>LLM: summarise older messages
        LLM-->>AG: summary
        AG->>DB: setThreadWarmSummary
    end
    API-->>UI: 202 Accepted
    UI->>API: GET /threads/:id/run (EventSource subscribe)
    AG->>LLM: stream completion
    LLM-->>AG: tokens
    AG-->>API: broadcast chunks (run-registry)
    API-->>UI: SSE: text_delta / tool_call / done
    UI-->>U: render
    AG->>DB: save checkpoint
    UI->>API: GET /threads/:id (refetch incl. warm_summary)
    UI-->>U: warm summary card hydrates

Key Flow — Inbound bridge message (WhatsApp)

sequenceDiagram
    actor P as WhatsApp peer
    participant WA as WhatsApp Web
    participant BR as Bridges (Baileys)
    participant DSP as bridges/dispatcher
    participant AG as Agent Runtime
    participant DB as SQLite
    participant N as Notifications

    P->>WA: send message
    WA-->>BR: stream event
    BR->>DSP: normalize → InboundMessage
    DSP->>DB: route JID → agent_id + thread_id
    DSP->>AG: deliver as user turn
    AG->>DB: append + run graph
    AG-->>DSP: reply text
    DSP->>BR: send outbound
    BR-->>WA: deliver to peer
    AG->>N: SSE push to any open UI

Key Flow — Agent-led integration setup (ADR-0010)

sequenceDiagram
    actor U as User
    participant UI as Web UI
    participant AG as Agent Runtime
    participant REG as Manifest Registry
    participant PA as pending_actions
    participant API as /api/v1
    participant DB as SQLite

    U->>AG: "connect my gmail"
    AG->>REG: list_integrations / get_integration_setup("gmail")
    REG-->>AG: manifest (steps + proposes kinds)
    AG->>PA: propose_config_change(enable_integration, payload)
    PA-->>UI: ApprovalsBanner shows pending row
    U->>UI: Approve → secret-collection modal
    UI->>API: POST /pending-actions/:id/approve { extras: { client_id, client_secret } }
    API->>DB: applyEnableIntegration(payload, extras)
    AG->>PA: propose_config_change(start_oauth, { integration_id })
    U->>UI: Approve
    UI->>API: POST /pending-actions/:id/approve
    UI->>API: POST /integrations/gmail/oauth/start
    API-->>UI: { authorize_url }
    UI->>U: window.open(authorize_url)
    Note over AG,DB: Agent never sees secrets — collected by approval UI directly

The same propose_config_change → ApprovalsBanner → applyAction loop also carries agent-driven harness edits (ADR-0036): the agent proposes upsert_harness to create or modify a custom preset, and update_agent (with harness_id) to point an agent at it. Built-in harnesses remain read-only and the global default pointer stays UI-only — both invariants enforced inside applyAction.

Tool registry — category × capability

Every built-in tool registers with the tool registry on two orthogonal axes (ADR-0038):

  • ToolCategory — topical group (Memory, Files, Mail, Atlassian, …). Drives the Agent editor sidebar layout. Says nothing about safety.
  • Capability — safety class (read | write | execute).
  • read: pure observation, no mutations anywhere (memory_read, web_fetch, jira_search).
  • write: mutates local Jarela-owned state — SQLite tables, the file store, files in user-controlled directories (memory_write, file_write, schedule_task, documents_add_local_source).
  • execute: invokes external systems with side effects users see outside Jarela, OR runs arbitrary code (local_exec, generate_image, delegate_to_agent, claude_delegate, codex_delegate, jira_create_issue, gmail_create_draft).

Files with mixed capabilities (memory, files, schedule, atlassian, github, gmail, outlook, calendar) declare every capability bucket in a single registerLangChainPackage({ category, integrationId?, tools: { read?, write?, execute? } }) call. integrationId names the INTEGRATIONS key backing the tools, so the permission layer can deny them when that integration is unconfigured (ADR-0078). External (JARELA_TOOLS_DIR) and MCP tools default to execute until a manifest field overrides it. Consumers — a planned per-capability approval gate, UI badges, the ADR-0037 validator's citation rules — switch on the three values exhaustively. The capability-coverage.test.ts runtime check asserts every registered tool has a capability so a new tool cannot land uncategorised.

Output validator (anti-fabrication)

lib/agents/output-validator post-checks every assistant turn before the terminal done chunk leaves stallRetryStream (ADR-0037). It cross-references the assistant text against the tool_call chunks issued in the same turn and flags four fabrication shapes — claim-without-tool, citation-of-an-unregistered-tool, citation-of-an-uncalled-tool, and summary-without-action. A flagged turn triggers the same retry path the stall detector uses (↻ separator + a reason-aware synthetic-user nudge); if the retry budget is exhausted, persistAssistantMessage appends a visible *⚠️ Output validator flagged: ...* footer to the persisted message. The validator runs entirely on regex — no extra LLM call per turn — and is exercised by both unit tests and a named-scenario regression set (npm run test:eval) seeded with real observed hallucinations.

Key Flow — Scheduled background task

sequenceDiagram
    participant S as Scheduler
    participant AG as Agent Runtime
    participant DB as SQLite
    participant N as Notifications

    S->>DB: poll due jobs (cron)
    S->>AG: run(jobId)
    AG->>DB: load state
    AG->>AG: execute graph
    AG->>DB: persist result
    AG->>N: notify (if configured)

Key Flow — Delegate a coding task to Claude Code (ADR-0071, ADR-0073)

sequenceDiagram
    participant AG as Agent Runtime
    participant CD as claude_delegate tool
    participant MB as claude-memory-bridge
    participant DB as SQLite (memory_store, claude_delegate_sessions)
    participant CLI as claude CLI (child process)
    participant FS as ~/.claude/projects/.../memory
    participant UI as Chat UI (ToolList card)

    AG->>CD: claude_delegate({ task, cwd, ... })
    CD->>CD: resolveSafetyGate() — JARELA_TOOL_SAFETY tier
    CD->>MB: syncIn(cwd, claude-sync:<hash>)
    MB->>DB: listMemory(namespace)
    MB->>FS: write *.md + MEMORY.md index
    CD->>DB: getSession(project_key) / rememberSession
    CD->>CLI: spawn -p --output-format stream-json --permission-mode <gated>
    loop each stream-json turn
      CLI-->>CD: stream-json event (text / tool_use)
      CD->>CD: armTimer() — resets the subprocess idle timer
      CD->>UI: config.writer() -> "custom" stream -> tool_progress chunk (live step, also resets the outer wallclock)
    end
    CLI-->>CD: stream-json result event
    CD->>MB: syncOut(cwd, namespace, manifest)
    MB->>FS: read *.md
    MB->>DB: putMemory / deleteMemory
    CD->>CD: gitDiffSummary(cwd) — verify loop
    CD-->>AG: { result, changes, permission_denials, sync, steps }

Key Flow — Supervise a Codex coding task (ADR-0086)

sequenceDiagram
    participant AG as Agent Runtime
    participant CD as codex_delegate tool
    participant DB as SQLite (codex_delegate_sessions)
    participant CLI as codex CLI (child process)
    participant UI as Chat UI (ToolList card)

    AG->>CD: codex_delegate({ task, cwd, feature?, background? })
    CD->>DB: getSession(project_key)
    CD->>CLI: spawn exec or exec resume with sandbox policy
    loop each JSONL event
      CLI-->>CD: command or agent-message event
      CD->>UI: tool_progress chunk (live transcript)
    end
    CLI-->>CD: thread id and final result
    CD->>DB: rememberSession(project_key, thread_id)
    CD->>CD: gitDiffSummary(cwd) — verify loop
    CD-->>AG: { result, changes, transcript, resumed }
    opt background run
      AG->>CD: codex_delegate_status(job_id, poll or cancel)
      CD-->>AG: steps, status, result
    end

Key Flow — Browser-extension page capture (ADR-0018)

sequenceDiagram
    participant U as User
    participant BX as Browser Extension (MV3)
    participant SW as Extension Service Worker
    participant CS as Content Script (picker)
    participant PR as proxy.ts
    participant API as /api/v1/page-capture
    participant DB as SQLite
    participant BUS as Notifications Bus
    participant UI as Web UI (open tab)

    Note over SW,API: Heartbeat — every 15s
    SW->>API: GET /api/v1/health
    API-->>SW: 200 OK ⇒ icon enabled

    U->>BX: Click toolbar icon
    BX->>SW: action.onClicked
    SW->>CS: scripting.executeScript (idempotent)
    CS-->>U: Overlay banner + element-tracking outline
    U->>CS: Click target element (or ESC to cancel)
    CS->>SW: runtime.sendMessage(jarela-capture-visible-tab)
    SW->>SW: chrome.tabs.captureVisibleTab → PNG dataURL
    SW-->>CS: dataURL
    CS->>CS: crop to element bounding rect (OffscreenCanvas, devicePixelRatio)
    CS->>SW: runtime.sendMessage(jarela-capture, {text, selector, screenshot})
    SW->>PR: POST /api/v1/page-capture (Origin: chrome-extension://…)
    PR->>PR: Loopback Host check ✓; carve-out skips Origin check
    PR->>API: forward
    API->>API: Truncate text to 100KB UTF-8; validate screenshot ≤ 4MB base64
    API->>DB: addMessage(thread, "user", [text, image] when screenshot present)
    API->>BUS: publish(thread_message_added)
    API-->>SW: 200 {thread_id, msg_id, truncated, originalBytes}
    SW-->>CS: ack
    CS-->>U: Flash + "✈ Sent" pill animation + success banner
    BUS-->>UI: SSE: thread_message_added
    UI->>UI: dispatch jarela:thread-updated → re-fetch messages (image renders inline)

Non-Functional Requirements

NFR Target Notes
Cold start (dev) < 5 s npm run dev
First token latency < 1.5 s p95 Network-bound on provider
Local-only operation required No telemetry, no required cloud backend
Persistence survive process restart All state in ~/.jarela/*.sqlite
API key handling never leave the host Stored in DB or env, not synced
Same-origin enforcement required CSRF / DNS-rebinding guard in lib/auth/access.ts
Secrets at rest required for sensitive namespaces AES-GCM envelope, master key in OS keychain or .secret-key fallback
Outbound proxy support required on corporate networks env vars or in-app proxy_config row, applied via undici setGlobalDispatcher (ADR-0009); env wins over DB
Stream resilience reattach within seconds of network change EventSource auto-reconnect + 4000-event replay buffer in lib/agents/run-registry.ts (ADR-0008)
CI on every push required .github/workflows/ci.yml: lint + tsc + build + live integration suite

External Dependencies

Dependency Purpose Failure mode
Anthropic / OpenAI / Google / Cohere LLM inference Surface provider error to UI; allow model switch
Google GenAI (Gemini) — STT + TTS endpoints Push-to-talk voice input + generate_voice tool (ADR-0017) Voice surface degrades to text-only; chat continues
MCP servers External tools Tool call returns error; agent can recover or skip
External provider/tool .cjs files (~/.jarela/{providers,tools}/) User-authored extensions, hot-loaded Validation errors surfaced in GET /api/v1/extensions and the Extensions tab; loader skips invalid files (ADR-0013)
User shell rc (~/.zshrc/~/.bashrc) on macOS/Linux, User-scope env on Windows Source for credential env vars (ADR-0016); probed at boot + on demand Probe failure surfaces a warning, app falls back to whatever is already in process.env; tools surface "not configured" via the existing env-then-DB resolver
GitHub API (api.github.com) Native github_* tools — issues, PRs, repos (ADR-0015); Copilot OAuth for the model provider Tool call returns the API error; agent can recover or skip
SQLite (local) Persistence Fatal — startup fails fast with clear error

Decisions

See docs/adr/. Significant choices on persistence, agent runtime, and provider strategy will be recorded as ADRs.