Gromozeka is an experimental Kotlin Multiplatform AI assistant.
It is both an agent UI and a dogfooding environment for building a more durable agent runtime: multi-tab conversations, tool execution, MCP integration, voice input, and typed long-term memory.
The project is currently a local research/development application, not a polished packaged product.
- Main dogfooding runtime:
OPEN_AI_SUBSCRIPTION. - Runtime composition roots:
:serverfor the control/API plane and:workerfor execution. - UI clients:
:presentationJVM Desktop and Wasm web/PWA. - Business workflows:
:application. - Domain contracts and memory models:
:domain. - AI integrations and tool implementations:
:infrastructure-ai. - Persistence and search implementations:
:infrastructure-db. - Active long-term memory backend: PostgreSQL through
MemoryStore.
The current development shape is split:
:serveraccepts client commands, persists runtime state, publishes durable work, streams events, and exposes the Ktor remote endpoint.:workerclaims exact Worker-targeted durable tasks and executes configured tools and finite AI request-response operations.:presentationowns UI code and can run either as a JVM desktop client or as a Wasm web client.- The server listens on
/wsfor remote UI traffic and serves already-built Wasm static files frompresentation/build/dist/wasmJs/developmentExecutableby default.
One Server deployment is one isolated Runtime. A Runtime can have several users, projects, and workers, but it is not a shared database for unrelated customer organizations. A future managed service should provision separate Runtime instances behind a management control plane instead of turning the Runtime itself into a pooled multi-tenant service.
Legacy and auxiliary integrations still exist in the codebase, but the current default development path is the OpenAI subscription runtime plus PostgreSQL-backed typed memory.
Gromozeka intentionally does not implement per-command approvals, command denylists, filesystem sandboxes, or a second application-level permission system. Isolation, when required, must be provided by the operating system, a dedicated account, container, virtual machine, credential scope, network policy, and backups.
A Gromozeka Worker is a trusted, unsandboxed executor. Enrolling a Worker authorizes the Gromozeka control plane and its selected models to invoke configured tools with the effective permissions of the Worker process. The Worker is not an autonomous agent and does not choose goals or policy. It executes exact Worker-targeted durable tasks, which can include configured tools and finite AI request-response operations. Conversation turns and memory pipelines remain on the Server.
Readonly and Writable are behavioral instructions for supported models, not security boundaries. A model that cannot reliably follow these instructions is unsupported.
The Server is a control plane whose authority is explicitly delegated by its enrolled Workers. It does not elevate a Worker beyond the effective permissions of its process or enlarge the underlying permissions of its machines; it provides another path for exercising authority that already exists. Authentication, transport security, private network access, narrowly scoped infrastructure credentials, and auditability protect that path; they are infrastructure requirements, not model-facing per-command permissions.
This trust model does not make external content trustworthy. Indirect prompt injection is a social-engineering and confused-deputy attack: attacker-controlled text in a file, repository, web page, email, or tool result tries to make an already-authorized model use its existing authority against the user's intent. It is closer to phishing than to an operating-system privilege escalation.
The important difference is how the target receives information. A human usually sees the channel, sender, and content as distinct signals. An LLM reasons over instructions and data in the same context, even when the API labels their roles. Modern models are trained with an explicit instruction hierarchy to prioritize higher-authority instructions and ignore malicious tool content, but this remains learned model behavior rather than a kernel-enforced boundary.
Available measurements show that neither humans nor models are reliably immune, but the numbers describe different populations and must not be treated as one comparable prevalence rate:
- The FBI 2025 IC3 report recorded 191,561 phishing/spoofing complaints and $215.8 million in reported losses. These are real-world reports, not a click or attack-success rate.
- A controlled spear-phishing study with 101 participants measured a 12% click-through rate for arbitrary phishing emails and 54% for both human-expert and fully AI-automated personalized emails.
- The 2026 LivePI preprint measured 10.7% to 29.6% total attack success across five frontier agent systems in a live but test-controlled environment.
- A NAACL 2025 adaptive-attack study bypassed all eight evaluated indirect-prompt-injection defenses with attack success rates above 50%, demonstrating that results against static attacks do not establish a hard boundary.
Gromozeka accepts this residual risk explicitly instead of presenting command filters or approval dialogs as a complete solution. It relies on supported models' instruction-hierarchy behavior, explicit source and role context, observable execution, and infrastructure-level isolation chosen by the operator. Logs and backups support detection and recovery; they do not prevent prompt injection.
- Runs Compose chat UI with tabs and project-aware sessions.
- Supports remote UI clients over WebSocket: JVM desktop locally, Wasm web/PWA in a browser.
- Calls LLM runtimes through domain-level
AiRuntimeabstractions. - Exposes internal tools for filesystem, shell, web search, code navigation, planning, and inter-agent workflows.
- Stores external MCP definitions and accepted tool snapshots in the Server database, while the assigned Worker owns the live connection and execution.
- Provides voice-oriented UI pieces such as push-to-talk and TTS services.
- Writes and recalls structured project memory automatically during chat.
The active memory system is not a plain vector database. It stores typed, provenance-aware memory objects:
Source: immutable evidence, usually a chat turn or tool output.Entity: canonical project/user/product/concept anchor.Claim: atomic factual memory with predicate, scope, evidence, lifecycle, and temporal fields.Note: rationale, design note, or reusable reasoning chunk.Task: durable follow-up or commitment memory with lifecycle state.Profile: projection built from profile-sync claims.Episode: reusable experience or lesson pattern.MemoryRun: debug/audit trace for memory pipeline execution.
Runtime write path:
- Capture the current message as a source.
- Route the message with
LlmMemoryWriteRouter. - Retrieve relevant existing memory when needed.
- Canonicalize entities.
- Build/reconcile notes, claims, and tasks.
- Materialize changes into
MemoryStore. - Update projections such as profiles.
Runtime read path:
- Plan whether memory is needed with
LlmMemoryReadPlanner. - Verify no-memory decisions with a model-based verifier when needed.
- Search typed memory in PostgreSQL.
- Select/rerank final memory context.
- Inject the retrieved memory into the main LLM call as runtime-only context.
Maintenance flows are explicit/manual durable operations handled asynchronously by a Worker: note consolidation, repair, entity maintenance, retention, and embedding rebuilds.
Useful memory docs:
- MEMORY_IMPLEMENTATION.md
- LLM_CASSETTE_NOTE.md
- agent_memory_handoff/README.md
- domain memory architecture
- macOS development machine.
- JDK 21.
- Docker, for local PostgreSQL with pgvector and RabbitMQ runtime work queues.
- OpenAI subscription auth file in Gromozeka home for the current dogfooding runtime.
- Microphone permissions if you want to use voice input.
PostgreSQL and RabbitMQ are intentionally explicit. The app should fail fast if either runtime dependency is not available.
The server, Worker, and UI clients are separate processes. Start local infrastructure first, then the server, a Worker, and one of the UI clients.
Start local infrastructure:
GROMOZEKA_HOME="$PWD/dev-data/client/.gromozeka" \
docker compose -f "$PWD/server/src/main/resources/docker-compose.yml" up -d postgres rabbitmqRun the server:
GROMOZEKA_HOME="$PWD/dev-data/client/.gromozeka" \
GROMOZEKA_MODE=dev \
./gradlew :server:runThe server defaults to 127.0.0.1:8765. It prints a line like:
==== Gromozeka server started: ws://127.0.0.1:8765/ws ====The Server only accepts commands, persists runtime state, publishes work, and streams events. Start the local all-capabilities Worker:
SPRING_CONFIG_ADDITIONAL_LOCATION="file:$PWD/worker/config/dev-worker.yaml" \
GROMOZEKA_HOME="$PWD/dev-data/client/.gromozeka" \
GROMOZEKA_MODE=dev \
./gradlew :worker:runSee worker/README.md for cloud/local Worker configuration and the trusted executor contract.
The Gradle development client defaults to the local Server:
ws://127.0.0.1:8765/ws
Override it with GROMOZEKA_REMOTE_URL when connecting through LAN, VPN, or
Tailscale. Packaged native clients have no hardcoded Server: they ask for its
address on first launch and persist the selection locally.
Open a running Server's Downloads settings to download a Client, standalone Server, or Worker from the matching Gromozeka release. Published releases currently provide:
- an unsigned macOS ARM64 Client DMG;
- a portable Windows x64 Client ZIP;
- macOS ARM64, Linux x64, and Windows x64 Server archives with private Java 21 runtimes and the Web client;
- macOS ARM64, Linux x64, and Windows x64 Worker archives with private Java 21 runtimes;
ghcr.io/lewik/gromozeka-serverandghcr.io/lewik/gromozeka-workerimages;- a self-hosted Compose file, environment template, and SHA-256 checksums.
Pushing a v<major>.<minor>.<patch> tag runs the release workflow, publishes
those assets and immutable images, then installs that exact release on the
primary AWS deployment. Prerelease tags such as v1.5.0-test.1 remain GitHub
prereleases. The workflow can also be dispatched without publishing or
deploying to verify all release jobs.
Desktop packages are unsigned for now. Android store/direct-release signing and iOS TestFlight/App Store distribution are intentionally deferred; iOS development builds are installed locally as described in iosApp/README.md.
Run the desktop UI client against the local server:
GROMOZEKA_REMOTE_URL="ws://127.0.0.1:8765/ws" \
GROMOZEKA_CLIENT_HOME="$PWD/dev-data/client/.gromozeka-remote-client" \
./gradlew :presentation:runBuild the Wasm web client:
./gradlew :presentation:wasmJsBrowserDevelopmentExecutableDistributionThen run the server and open locally:
http://127.0.0.1:8765/
The web client resolves its WebSocket endpoint from the browser URL, so http://127.0.0.1:8765/ uses ws://127.0.0.1:8765/ws.
For raw HTTP testing through LAN/VPN without Tailscale Serve, bind the server to all interfaces:
GROMOZEKA_HOME="$PWD/dev-data/client/.gromozeka" \
GROMOZEKA_MODE=dev \
GROMOZEKA_REMOTE_HOST=0.0.0.0 \
./gradlew :server:runThen open:
http://<machine-tailscale-or-lan-ip>:8765/
For private phone/laptop access inside a tailnet, use Tailscale Serve instead of exposing Gromozeka publicly. The Wasm client does not need a separate HTTPS build. It resolves /ws from the page URL, so an https:// page uses wss:// automatically.
When TLS terminates in Tailscale Serve or another trusted reverse proxy, set
GROMOZEKA_TRUST_FORWARDED_HTTPS=true. This trusts an exact
X-Forwarded-Proto: https header, so only enable it when untrusted clients
cannot reach the Server directly. The AWS deployment enables it because the
container port is published only on the instance loopback interface.
Build the Wasm web client:
./gradlew :presentation:wasmJsBrowserDevelopmentExecutableDistributionRun the server:
GROMOZEKA_HOME="$PWD/dev-data/client/.gromozeka" \
GROMOZEKA_MODE=dev \
./gradlew :server:runExpose the local server through Tailscale Serve:
GROMOZEKA_REMOTE_PORT="${GROMOZEKA_REMOTE_PORT:-8765}"
tailscale serve --bg "http://127.0.0.1:${GROMOZEKA_REMOTE_PORT}"Then open:
https://<machine>.<tailnet>.ts.net/
If you run the server on a non-default port, rerun the tailscale serve --bg ... command with the same GROMOZEKA_REMOTE_PORT.
Check the current Serve configuration:
tailscale serve statusStop serving Gromozeka through Tailscale:
tailscale serve resetIf static files need to be served from a custom directory, set:
GROMOZEKA_WEB_STATIC_DIR="/absolute/path/to/web/dist"Stop local infrastructure:
docker compose -f "$PWD/server/src/main/resources/docker-compose.yml" stop postgres rabbitmqBuild:
./gradlew :presentation:build -qServer build:
./gradlew :server:build -qWeb client distribution only:
./gradlew :presentation:wasmJsBrowserDevelopmentExecutableDistribution -qApplication context smoke test:
./gradlew :server:test -qMemory maintenance unit tests:
./gradlew :application:jvmTest --tests 'com.gromozeka.application.service.memory.MemoryMaintenancePipelineTest' -qMemory real-model E2E in deterministic replay mode:
./gradlew :server:test \
--tests 'com.gromozeka.server.MemoryRealModelE2eTest' \
-Dgromozeka.memory.e2e=true \
-Dgromozeka.llm.cassette.mode=replay-only \
-qRecord missing LLM cassettes intentionally:
./gradlew :server:test \
--tests 'com.gromozeka.server.MemoryRealModelE2eTest' \
-Dgromozeka.memory.e2e=true \
-Dgromozeka.llm.cassette.mode=record-missing \
-qFor gromozeka.memory.e2e=true, cassette mode defaults to replay-only. This prevents accidental live LLM calls during normal verification.
domain/ Domain models, service contracts, tool contracts
application/ Use cases and orchestration pipelines
infrastructure-ai/ LLM runtimes, tools, MCP-related integrations
infrastructure-db/ PostgreSQL persistence, search, repository implementations
server/ Spring/server runtime composition, Ktor remote endpoint, server-owned resources
presentation/ Compose JVM Desktop and Wasm web UI clients
shared/ Shared utilities
agent_memory_handoff/ Memory design handoff and reference architecture
docs/ Architecture notes, diagrams, research, guides
- Domain contracts should explain what the system must be able to do without forcing a storage or UI implementation.
- Memory debug quality is judged by traces and E2E artifacts, not only by compilation.
- The current E2E suite intentionally uses real LLM calls when recording cassettes and deterministic replay afterward.
- Logs are verbose by design while the memory MVP is still being hardened.
- Postgres data lives under
GROMOZEKA_HOME/postgreswhen started through the provided compose file.
Gromozeka is an attempt to move beyond "just chat" toward an assistant that can work with code, tools, projects, and its own accumulated context.
The goal is human-AI collaboration where the system remembers useful project knowledge, exposes its traces, and stays debuggable enough that a developer can understand why it did what it did.
Custom License - free for non-commercial use, commercial use requires permission. See LICENSE for details.
