Mirrored from https://github.com/SNAPKITTYWEST/kotlin-command-center at commit
6849123. Part of the SnapKitty October 2026 main drop.
Kotlin Command Center
Talk to your Ollama agents and administer your server from your phone β without opening a single inbound firewall port.
ββββββββββββ Bearer APP_SECRET ββββββββββββββββββββ Access svc token ββββββββββββββββββ
β Kotlin β ββββββββββββββββββββΆ β Cloudflare β βββββββββββββββββββΆ β cloudflared ββββΆ Ollama :11434
β app β https://api. β Worker gateway β https://ollama- β (outbound β
β (:core / β yourdomain.com β worker/ β internal. β only, no β
β :android β β β yourdomain.com β inbound ββββΆ sshd :22
β :desktop)β ββββββββββββββββββββΆ β β βββββββββββββββββββΆ β ports) β
ββββββββββββ wss://ssh. ββββββββββββββββββββ Access svc token ββββββββββββββββββ
yourdomain.com (WS handshake) on both hostnames
+ CF-Access headers
How the pieces fit:
- Server (
server/):cloudflaredopens outbound-only tunnels for Ollama (ollama-internal.yourdomain.com β localhost:11434) and SSH (ssh.yourdomain.com β ssh://localhost:22). No inbound ports, ever. - Worker gateway (
worker/): the only thing the phone calls for inference. VerifiesAuthorization: Bearer <APP_SECRET>, rate-limits per IP, then forwards to the tunnel with the Cloudflare Access service token attached./healthis unauthenticated; unknown routes 404; non-POST API calls 405. - Kotlin
:core(app/core/): Ktor CIO client for the Worker (AgentClient), plus a real JSchSocketFactorythat bridges WebSocket frames to stream-oriented SSH over piped streams (WebSocketBridge.kt), withStrictHostKeyChecking=yesagainst a pinnedknown_hosts(SshClient). :desktop(app/desktop/): terminal CLI (health,prompt,chat,ssh) for testing the whole stack from a laptop.:android(app/android/): Compose UI (agent chat + server admin tabs), source-complete.
Setup order
- Server β on the box that runs Ollama:
sudo DOMAIN=yourdomain.com ./server/setup.sh(installs cloudflared, creates the tunnel, routes DNS, installs the systemd unit). Then followserver/ACCESS_SETUP.mdto put both hostnames behind Cloudflare Access with a service token (Allow policy). - Worker β
cd worker && wrangler secret put APP_SECRET(+CF_CLIENT_ID,CF_CLIENT_SECRET), setTUNNEL_BASEinwrangler.toml, thenwrangler deploy. - App β fill the
CC_*env vars (see:desktopusage), or the Android settings screen once it exists:CC_WORKER_URL,CC_APP_SECRET,CC_SSH_WS_URL,CC_CF_CLIENT_ID,CC_CF_CLIENT_SECRET,CC_SSH_USER,CC_SSH_HOST,CC_SSH_PORT,CC_SSH_KEY,CC_KNOWN_HOSTS(generate withssh-keyscan -t rsa ssh.yourdomain.com, verify the fingerprint out-of-band first). - Build:
./gradlew :core:test :desktop:build(needs the Android SDK only for:android; it is excluded from the build until then).
Security model
- The phone never reaches Ollama directly; the Worker is the single choke point and it demands the app secret on every API call.
- Both tunnel hostnames require the Cloudflare Access service token at the edge β the open internet gets a 403 before touching your server.
- SSH host keys are pinned:
StrictHostKeyChecking=yeswith aknown_hostsyou generated. Unknown or changed keys abort the connection. (The sketch this replaced usednoβ that was fixed, not inherited.) - Secrets live in environment / Keystore-backed storage, never in code, flags, shell history, or the repo. Nothing in this tree is a real secret: domains, UUIDs, and tokens here are placeholders.
Rotation
- Service token (phone lost / token leaked): Access β Service Tokens β
delete
kotlin-command-center, create a new one,wrangler secret putthe newCF_CLIENT_ID/CF_CLIENT_SECRET, update the app. The old token dies immediately. - APP_SECRET:
openssl rand -hex 32, thenwrangler secret put APP_SECRET+ update the app config. - SSH key: rotate like any SSH key; re-run
ssh-keyscanonly if the host key legitimately changed (and verify the new fingerprint).
Verified here vs. needs your hardware
Verified on this build machine (2026-09-30):
:coremain + test sources compile clean (kotlinc 2.0.21).- 8/8 unit tests pass: Worker health/prompt/chat/auth-failure via
Ktor MockEngine; 256 KiB corruption-free round trip through the
WebSocketβstream bridge; handshake URL/headers; a real JSch instance
emitting its
SSH-2.0-banner through the bridge. :desktopCLI compiles and runs (usage/help path exercised).node --check worker/worker.jspasses.
Not verifiable here (no such infrastructure on this box):
- Real Cloudflare account, tunnel, DNS, and Access policies.
- Real Ollama behind the tunnel; real sshd behind the SSH hostname.
:androidcompilation β no Android SDK here. The module is excluded fromsettings.gradle.kts; addinclude(":android")on a machine with the SDK. Android secrets must move to EncryptedSharedPreferences before shipping (seeapp/android/README.md)../gradlewitself: this sandbox intercepts JVM TCP loopback (Gradle daemon handshake fails with the sandbox's policy banner), so the wrapper could not be generated here. The Gradle scripts are standard; run./gradlew :core:test :desktop:buildon your own machine.
License
AGPLv3 β see LICENSE. Every source file carries an SPDX header; the
gate is: grep -rL "SPDX-License-Identifier: AGPL-3.0-or-later" <sources>.
πΌ Commercial License
SnapKitty code is free and open under AGPL-3.0 for open-source use. Building a commercial product or service? A proprietary commercial license from Snapkitty Collective LLC lets you ship this code without the AGPL's source-sharing and network-use obligations.