Kern documentation
Everything about how Kern works: the agent loop and its tools, the permission model, the terminal stack, the workspace model, and how the project is put together.
Installation
Kern ships as a GitHub release rather than through the Play Store, so installation is a direct APK download. Pick the build for your architecture:
| Build | Device | Ubuntu |
|---|---|---|
app-arm64-v8a-release.apk | Most modern phones | Yes |
app-x86_64-release.apk | x86_64 emulators | Yes |
app-armeabi-v7a-release.apk | Older 32-bit phones | No |
app-x86-release.apk | x86 emulators | No |
app-universal-release.apk | Any of the above | Yes |
adb install -r app-arm64-v8a-release.apk
Every release is signed with the same key, so a later version installs straight over an existing copy and keeps your workspaces, sessions and API keys.
Requirements
- Android 9, API 28, or newer.
- arm64-v8a, armeabi-v7a, x86 or x86_64.
- About 45 MB for the app, plus roughly 375 MB for the Ubuntu environment.
- An OpenAI-compatible endpoint and a key, or a local model.
The agent loop
A turn is a sequence of steps. The agent builds a context from the conversation, the workspace and its own memory, asks the model what to do next, and executes the tool calls that come back. It repeats until the model stops asking for tools.
Each step follows the same shape:
- The context is assembled under a byte budget, with older turns folded into a digest.
- The request is streamed from the provider, and deltas are shown as they arrive.
- Tool calls are split into two groups by whether they are read-only.
- Read-only calls run concurrently. Mutating calls run one at a time, behind approval.
- Results are reordered to match the order the model asked for them in.
- The loop repeats, until the model returns without requesting a tool.
If a provider call fails and produced nothing, the turn is retried with a backoff. If the model returns an empty reply, it is nudged once. If it stops because it hit a token limit, it is asked to continue, up to twice.
A failing tool never ends the turn. It comes back as an error result the model can read and act on, which is what lets it recover on its own.
The tool set
Twenty-nine tools, in eight groups.
| Tool | What it does | Read only |
|---|---|---|
list_files | List a directory | Yes |
read_file | Read a file, with a byte cap | Yes |
search_files | Search file contents | Yes |
glob | Match paths by pattern | Yes |
grep | Search with a regular expression | Yes |
fetch_url | Fetch a URL, with secrets redacted | Yes |
propose_edit | Propose a snippet replacement | No |
edit_file | Replace an exact snippet | No |
write_file | Write a whole file | No |
delete_file | Delete a file | No |
propose_patch | Propose a multi-file patch | No |
run_build | Run the project build | No |
run_tests | Run the test suite | No |
run_lint | Run static analysis | No |
run_command | Run a shell command | No |
terminal_status | Report the live session state | Yes |
git_status | Working tree status | Yes |
git_diff | Show the current diff | Yes |
git_commit | Create a commit | No |
update_todos | Maintain the task list | No |
propose_plan | Present a plan for review | No |
remember | Write a durable memory note | No |
server_start | Start a dev server | No |
server_stop | Stop a dev server | No |
preview_open | Open the live preview | No |
preview_status | Report preview state | No |
preview_console | Read the page console | No |
preview_eval | Evaluate JavaScript in the page | No |
preview_screenshot | Capture the page | No |
A read-only tool may run alongside the other read-only tools in the same step. Anything that writes is sequenced and gated by the permission mode.
Arguments are repaired centrally.
If a model sends file_path where the schema says path, or
wraps everything in an input object, or sends a number as a string, the
repair layer fixes it in one place instead of in twenty-nine tools.
Permission modes
| Mode | Reads | Edits and commands | Destructive |
|---|---|---|---|
| Ask changes | Automatic | Always asks | Always asks |
| Auto edit | Automatic | Automatic | Always asks |
| Full access | Automatic | Automatic | Always asks |
The mode is chosen from the chip above the composer and applies to the whole session. Destructive commands are excluded from every mode, without exception.
Command classification
Every shell command is classified before it runs, by a conservative pattern matcher:
| Impact | Examples | Behaviour |
|---|---|---|
| Read-only | ls grep cat | Runs freely |
| Mutating | npm install git add | Gated by the mode |
| Network | curl npm publish | Gated by the mode |
| Destructive | rm -rf mkfs git reset --hard | Always asks |
This is a safety signal, not a security boundary. The classifier never executes anything, and it errs toward a higher impact when it is unsure. It exists to catch accidents, not to stop a determined attacker.
Approvals and plans
When a step needs a decision, the turn pauses and the UI shows a sheet:
- Command approval shows the exact command, its working directory and the reason the agent wants it.
- Edit approval shows the diff before anything is written.
- Plan review shows a multi-step plan, which can be approved, rejected or sent back for changes.
A patch that touches several files can be approved file by file, so you can take the half you agree with.
Context and memory
The agent works from a context budget rather than the whole history. When the replayed conversation grows past the budget, the oldest tool results are dropped and the older turns are folded into a digest, while the most recent tool results are kept intact.
Three sources feed the context:
- The system prompt, which describes every tool and the behaviour rules.
- Project rules, read from a
KERN.mdfile at the workspace root, if one exists. - Workspace memory, a per-workspace file the agent can write to, holding plans, notes and an archive.
The terminal stack
Kern's terminal is four layers, each replaceable on its own.
| Layer | Module | What it does |
|---|---|---|
| Contract | core:terminal:api | The TerminalRuntime interface every implementation satisfies |
| Emulator | core:terminal:engine | Pure-Kotlin VT and ANSI emulation, a plain JVM module |
| PTY | core:terminal:pty | The native pseudo-terminal, in C with CMake and the NDK |
| Runtime | core:terminal:implementation | Device shell and proot backends, sessions, the distro installer |
The Linux environment
The Ubuntu environment is installed on demand from a catalog that is data rather than code. The catalog pins a rootfs URL, its size and its SHA-256 per ABI.
The install runs in five stages:
- Download, with the progress reported against the compressed size.
- Verify the SHA-256 against the catalog.
- Extract into a staging folder, reporting unpacked bytes rather than a flat zero.
- Probe that proot can actually start in this environment.
- Promote the staged folder into place, atomically.
Because the promotion is the last step and atomic, an interrupted or failed install never damages a working environment.
The binaries are pinned and verified. The vendored proot runtime is committed to the repository, recorded in a lock file with its size and SHA-256, and checked on every build. A tampered binary fails the build rather than the device.
The native PTY
Android's bionic has no reliable combination of forkpty and
login_tty, so the pseudo-terminal is opened by hand:
posix_openpt() -> grantpt() -> unlockpt() -> ptsname()
fork()
child: setsid()
ioctl(slave, TIOCSCTTY)
dup2() onto 0, 1, 2
execve()
parent: keeps the master descriptor
Because the child gets a controlling terminal, job control, interactive prompts and
full-screen programs all behave the way they should. Reads use poll with a
short timeout, so a cancelled coroutine is noticed promptly rather than hanging on a
blocking read.
The VT emulator
The emulator is a plain kotlin-jvm module, deliberately not an Android
library, so it can be tested on the JVM.
- A byte-to-action state machine covering CSI, OSC and DCS sequences, tolerant of a sequence split across reads.
- Primary and alternate screen buffers, with a bounded scrollback.
- Scroll regions, saved cursor, wrap handling and DEC private modes.
- 256-colour and truecolour SGR, and correct widths for East-Asian characters.
- Response bytes for DSR, DA and OSC colour queries, which makes the terminal bidirectional rather than a renderer.
The package manager
The package screen drives apt inside the guest over the shared command
executor. The catalogue comes from apt-cache pkgnames and the installed list
from dpkg --get-selections.
Installing is not a blocking call. The agent streams the output and the app shows it live in a log screen, so a large install is visible rather than silent.
Workspace types
| Type | Where it lives | Shared with the terminal |
|---|---|---|
| App-managed | Private app storage | Yes, directly |
| SAF, with All files access | Its real /sdcard path | Yes, directly |
| SAF, without All files access | A private execution mirror | Through the mirror, with a manual sync back |
The two concepts are separate on purpose. A logical workspace describes the folder you chose. An execution workspace describes where the commands actually run. They differ precisely when a SAF folder cannot be reached at its real path.
Storage and permissions
All files access arrives in Android 11, API 30. On Android 9 and 10 the permission does not exist, and Kern does not offer a grant prompt that could not work. The SAF adapter and the execution mirror cover those versions instead.
Paths are contained, never absolute:
WorkspacePaths.normalize("/etc/passwd") -> null
WorkspacePaths.normalize("../../secrets.txt") -> null
Files and the editor
The file explorer browses the workspace through the same adapter the agent uses, so there is exactly one view of the tree. The editor is a plain Compose text surface with a line-numbered gutter, a dirty flag and a 16 MB read cap.
Providers
The provider layer is split in two. core:providers:api holds the contracts
and stays provider-neutral, so no provider's own data shapes leak out of it.
core:providers:openai holds an adapter for anything speaking the OpenAI chat
completions API.
Two details worth knowing:
- Streaming tool calls arrive out of order, so they are accumulated by index and reassembled before the agent sees them.
- The connection test probes the chat endpoint with a one-token request, not the models endpoint, because some gateways answer the models list without checking the credential.
Live preview
The preview is a WebView pointed at the local dev server. Navigation is locked to the loopback interface, and anything else is handed to the system browser.
The JavaScript bridge is deliberately tiny.
The only function exposed to the page is a console reporter, so a remote page can never
reach it. Screenshots use PixelCopy, because the older canvas path re-runs
the software draw and comes back empty from a hardware-rendered WebView.
Appearance
Eleven palettes, light and dark, plus Material 3 dynamic colour on Android 12 and newer. The terminal has seven themes of its own, including a 24-bit truecolour palette, and can follow the app theme or stay fixed.
Notifications and background work
A live terminal session is held by a foreground service that mirrors the session count, and an agent turn is held by a second one, because Android freezes and eventually reclaims a backgrounded process. The turn itself lives in an application-scoped coroutine, so navigating away does not cancel it.
Architecture
Twenty-two Gradle modules with a strict core and feature layering.
core:model is pure Kotlin with no Android dependency, and
core:terminal:engine is a plain JVM module, so both are unit-testable on the
JVM.
| Module | Responsibility |
|---|---|
:app | Navigation shell, setup wizard, foreground services |
:core:model | Domain models and enums, pure Kotlin |
:core:common | Dispatchers, logging, secret redaction |
:core:designsystem | Theme tokens, palettes, shared components |
:core:data | Room, DataStore, Keystore credentials |
:core:workspace | Workspace lifecycle, adapters, execution mirror |
:core:providers:api | Provider-neutral contracts |
:core:providers:openai | OpenAI-compatible streaming adapter |
:core:agent | The agent loop, context builder, workspace memory |
:core:tools | Tools, registry, argument repair |
:core:auth | API-key auth coordinator |
:core:terminal:api | The TerminalRuntime contract |
:core:terminal:engine | VT and ANSI emulator, plain JVM |
:core:terminal:pty | Native PTY, C with CMake and the NDK |
:core:terminal:binaries | Vendored proot runtime, SHA-256 pinned |
:core:terminal:implementation | Backends, sessions, installer |
:feature:home | Dashboard |
:feature:files | File explorer and code editor |
:feature:agent | Agent chat, approvals, plan review |
:feature:settings | Appearance, provider profiles, model catalogue |
:feature:terminal | Terminal UI, environment setup, package manager |
:feature:preview | Live WebView preview and console drawer |
Data and storage
Room is at version 4, with nine entities and three explicit migrations. The exported schemas are committed and are the source of truth for those migrations.
The data access objects use @Upsert, never
@Insert(REPLACE).
Replace deletes and re-inserts, which fires the cascading delete and silently wipes
sessions and credentials. A regression test locks this in.
API keys live in a separate table and are encrypted with AES-256-GCM through the Android Keystore. Only ciphertext is stored, and the key never leaves the keystore.
Security
- Credentials are encrypted with AES-256-GCM and bound to the Keystore.
- Secrets are stripped from URLs, headers, tool arguments and throwable messages before anything is logged.
- Android backup and data extraction are disabled, so the credential database cannot leave the device.
- The preview's JavaScript bridge exposes one console reporter and nothing else.
- Workspace paths are contained, so a tool cannot escape the workspace with an absolute path or a parent reference.
Testing
466 unit tests across 66 suites, all passing. The stack is JUnit 4, Truth, Robolectric, Turbine, MockK, MockWebServer, Room testing and Compose UI test.
./gradlew testDebugUnitTest
The areas with the deepest coverage are the agent loop, the tool set, the terminal engine and the installer, because those are where a regression is hardest to spot by hand.
Building from source
git clone https://github.com/MurShidM01/Kern.git
cd Kern
./gradlew assembleDebug
./gradlew assembleRelease
The release build produces one APK per ABI plus a universal one, signed with the standard Android debug keystore so that every future version can be installed over the last.
Contributing guidelines, including the conventions for commits and the parts of the codebase that need extra care, are in CONTRIBUTING.md.
License
Kern's own source is MIT licensed. The bundled runtime keeps its own license, and the full texts ship inside the app:
| Component | Version | License |
|---|---|---|
| proot | 5.1.107.96 | GPL-2.0 |
| libtalloc | 2.5.0 | LGPL-3.0 |
| libandroid-shmem | 0.7 | MIT |
Because proot is redistributed under GPL-2.0, the project as a whole is MIT, GPL-2.0 and LGPL-3.0.