Koala is a local-first AI home-state service for Bare Systems.
- Go orchestrator: camera discovery, RTSP ingest, zone state, REST APIs, and MCP on
6705. - Python worker: private HTTP inference service on
6704(YOLO baseline with deterministic fallback).
Koala does not passively intercept camera traffic. The orchestrator actively
connects to configured RTSP streams, samples frames with ffmpeg, forwards
those frames to the private worker, filters detections into zone state, and
then exposes AI-friendly APIs on 6705.
RTSP ingest -> frame sampling -> worker inference -> detection filtering -> temporal aggregation -> agent/API response
docker compose up --buildOr locally:
go run ./cmd/koala-orchestrator -config configs/koala.yaml
python -m koala_worker.serverCode deploys are automated via blink:
blink deploy koala-worker
blink deploy koala-orchestratorOne-time GPU setup β must be done manually on the Jetson before first deploy. Blink does not handle this because the install takes several minutes and is a system-level operation that only needs to happen once per device.
# SSH into the Jetson, then:
# 1. System packages
sudo apt-get install -y python3-pip libopenblas-dev
# 2. cuSPARSELt β required for PyTorch 24.06+ builds
wget https://developer.download.nvidia.com/compute/cuda/repos/ubuntu2204/arm64/cuda-keyring_1.1-1_all.deb
sudo dpkg -i cuda-keyring_1.1-1_all.deb
sudo apt-get update
sudo apt-get install -y libcusparselt0 libcusparselt-dev
# 3. numpy pinned version
pip3 install 'numpy==1.26.1'
# 4. PyTorch 2.5.0 β official NVIDIA wheel for JetPack 6.1 / R36.4 / Python 3.10
# Source: https://docs.nvidia.com/deeplearning/frameworks/install-pytorch-jetson-platform/
pip3 install --no-cache \
https://developer.download.nvidia.com/compute/redist/jp/v61/pytorch/torch-2.5.0a0+872d972e41.nv24.08.17622132-cp310-cp310-linux_aarch64.whl
# 5. Install ultralytics (YOLO) β do this BEFORE verifying torch
# ultralytics will pull in a CPU-only torch from PyPI; we overwrite it next
pip3 install ultralytics
# 6. Re-pin the NVIDIA CUDA torch β ultralytics upgrades it to a CPU build from PyPI
pip3 install --no-cache --force-reinstall \
https://developer.download.nvidia.com/compute/redist/jp/v61/pytorch/torch-2.5.0a0+872d972e41.nv24.08.17622132-cp310-cp310-linux_aarch64.whl
# 7. Verify CUDA is available (must print True)
python3 -c 'import torch; print(torch.__version__); print("cuda:", torch.cuda.is_available())'After the one-time setup, restart the worker and confirm GPU inference is live:
blink restart koala-worker
blink test koala-worker --tags gpu # passes when cuda=TrueOn first start after GPU setup, the worker downloads yolov8n.pt (~6 MB) into
$KOALA_MODEL_DIR (default: ~/baresystems/runtime/koala/worker/models/).
Subsequent starts load the cached model. YOLO detects person, package, and
animal labels; without GPU setup the worker falls back to a deterministic stub.
See BLINK.md for the supported Jetson deployment contract and operator notes for the current managed deployment shape.
The stable blink deployment shape is:
blinkhost:192.168.86.53- DVR:
192.168.86.46 - BearClaw and Koala Live call Koala at
http://192.168.86.53:6705 - The camera-facing orchestrator runs on host networking
- The worker stays private on
127.0.0.1:6704
That host-network orchestrator requirement is not optional on blink. During
the March 20, 2026 outage, the host could reach the DVR while Docker bridge
containers could not. If the host can reach cameras and containers cannot, do
not change camera code first. Preserve that network pattern as part of the
supported deployment contract described in BLINK.md.
All agent-facing interfaces on 6705 require Authorization: Bearer <mcp_token>.
- Legacy JSON tool routes:
POST /mcp/tools/koala.get_system_healthPOST /mcp/tools/koala.list_camerasPOST /mcp/tools/koala.get_zone_statePOST /mcp/tools/koala.check_package_at_door
- MCP JSON-RPC endpoint:
POST /mcp
The /mcp endpoint is a stateless MCP HTTP entrypoint for BearClaw-class
agents. It supports initialize, ping, tools/list, and tools/call while
the legacy /mcp/tools/... routes remain available for direct HTTP clients.
When wiring Koala into BearClaw, use BearClaw's HTTP MCP transport in
mcp_servers:
bareclaw config set mcp_servers "koala=http_mcp http://192.168.86.53:6705/mcp <mcp_token>"BearClaw will discover the koala.* MCP tools from /mcp and expose them in
its local tool catalog as koala__get_system_health, koala__list_cameras,
koala__get_zone_state, and koala__check_package_at_door.
Example direct tool route:
POST /mcp/tools/koala.get_zone_statewith{ "input": { "zone_id": "front_door" } }
All update APIs require Authorization: Bearer <mcp_token>.
GET /admin/updates/statusGET /admin/updates/securityGET /admin/updates/rollouts/listGET /admin/updates/historyGET /admin/ingest/statusPOST /admin/updates/checkPOST /admin/updates/stagePOST /admin/updates/applyPOST /admin/updates/rollbackPOST /admin/updates/rollouts/startPOST /admin/updates/rollouts/get
Device agent endpoints (called by orchestrator update executor):
GET /agent/updates/healthPOST /agent/updates/stagePOST /agent/updates/applyPOST /agent/updates/rollback
Manifest payload shape:
{
"input": {
"manifest": {
"key_id": "key-2026-03",
"version": "0.2.1",
"artifact_url": "http://updates.local/koala-0.2.1.bundle.json",
"sha256": "<64-char-hex>",
"signature": "<base64-ed25519-signature>",
"min_orchestrator_version": "0.1.0-dev",
"min_worker_version": "0.1.0-dev"
},
"device_ids": ["koala-local"]
}
}Signature payload is the newline-joined fields:
key_id, version, artifact_url, sha256, created_at, min_orchestrator_version, min_worker_version
When update.enabled=true, update.encryption_key_base64 is required, plus one signing mode:
- Legacy mode:
update.public_key_base64(deprecated) - Rotation mode:
update.active_key_id,update.previous_keys, andupdate.public_keys
Set update.rotation_only_mode: true to enforce rotation-only mode and reject legacy public_key_base64.
The agent now expects artifact_url to point to an encrypted/signed bundle JSON, not a raw binary artifact.
Unknown key_id signature attempts are tracked in agent health under unknown_key_attempts and emit security alert logs.
GET /admin/updates/security returns those counters plus recent unknown-key alert events.
GET /admin/updates/history returns persisted rollout/security events from SQLite (update.audit_db_path).
Rollout start input additionally supports:
mode:all,batch,canarybatch_size: required forbatch; optional forcanarymax_failures: stop rollout if failures exceed this thresholdpause_between_batches_ms: optional delay between batchesrollback_scope:failedorbatch
Pull mode configuration (client-initiated updates):
update.poll_enabled: enable periodic manifest pollingupdate.poll_interval_seconds: base polling intervalupdate.poll_jitter_seconds: random jitter added per cycleupdate.manifest_url: manifest URL to poll
GET /admin/updates/status includes data.poller with latest poll result, last/next poll time, and failure counters.
Poll events are persisted in SQLite and available via GET /admin/updates/history with category=\"poll\".
The orchestrator can continuously pull frames from RTSP cameras and feed inference automatically.
Runtime controls:
runtime.enable_stream_workersruntime.stream_sample_fpsruntime.stream_capture_timeout_seconds
Current implementation captures snapshots via ffmpeg, so ffmpeg must be installed on the device.
Ingest workers include watchdog + auto-reconnect behavior, and /admin/ingest/status now returns recent incident events (stream_failure, stream_stalled, stream_recovered) for MCP-visible operations monitoring.
Camera probing supports RTSP-first checks with ONVIF fallback probing (camera.onvif_url); ONVIF reachability is surfaced in list_cameras capability data.
For the blink homelab deployment shape and the Docker-to-LAN camera networking
workaround, see BLINK.md.
Staging/apply storage:
- staged artifact:
<update.staging_dir>/<version>/artifact.bin - staged manifest:
<update.staging_dir>/<version>/manifest.json - active version marker:
<update.active_dir>/current_version
When an alert fires (an absentβpresent rising edge for a tracked entity in a zone β see "AI Agent Interface"), Koala captures the exact camera frame that triggered the detection, draws the detection bounding box(es) onto it, and retains it so operators can visually confirm what was detected. The BearClawWeb dashboard renders these as thumbnails on each "Recent Alerts" card.
ingest manager service.processTask state.Aggregator snapshot.Store MCP server BearClawWeb
(captures frame, -> (holds the exact -> (Ingest returns the -> (annotated JPEG -> GET /admin/alerts/ -> Home::AlertsController
submits FrameTask triggering frame in alerts that fired keyed by alert ID, {id}/snapshot proxies; alert-snapshot
with FrameB64) task.FrameB64) on this call) bounded ring) (bearer or ?token=) Stimulus controller shows)
Key point: the frame is the locally captured one already present in
FrameTask.FrameB64 at internal/service/service.go. It is not re-fetched
and is never forwarded to the inference worker, so detection snapshots are
independent of the privacy.frame_buffer_enabled gate (which only governs the
worker call). Annotation matches detections to the alert by label + zone_id
and draws every matching box.
| Concern | Location |
|---|---|
| Config flag | internal/config/config.go β PrivacyConfig.DetectionSnapshotsEnabled (privacy.detection_snapshots_enabled) |
| Fired-alert signal | internal/state/aggregator.go β Ingest / detectTransitionsLocked return []Alert |
| Capture + annotate | internal/service/service.go β captureSnapshots; internal/snapshot/annotate.go |
| Storage | internal/snapshot/store.go β Store (bounded in-memory ring) |
| Wiring | cmd/koala-orchestrator/main.go (creates the store when the flag is on) |
| Serve | internal/mcp/server.go β alertSnapshot, route /admin/alerts/{id}/snapshot |
- In-memory only.
snapshot.Storeis a bounded map keyed by alert ID, capacitysnapshot.DefaultMax(200). Oldest entries are evicted first. - Lifecycle mirrors the alert ring. The aggregator keeps the newest 200
alerts (
state.defaultMaxAlerts); the snapshot store keeps the newest 200 frames. They are inserted together, so a snapshot exists for roughly every retained alert. Both are lost on restart β this is deliberate and keeps the two structures consistent. - Missing snapshots are graceful. Alerts older than the deploy, or evicted
alerts, return
404from the serve endpoint; the dashboard'salert-snapshotStimulus controller hides the figure instead of showing a broken image. - Serving is cache-friendly. An alert ID maps to a fixed frame, so the
endpoint sends
Cache-Control: immutable.
privacy:
detection_snapshots_enabled: true # default false in code; on in the deployed configDefault is off in code (privacy-by-default stance). It is enabled in
configs/koala.yaml and in the Blink provision heredoc (blink.toml) so the
homelab deploy ships with it on. Disabling the flag stops capture entirely and
the serve endpoint returns 404 for every alert.
The storage boundary is deliberately narrow so these are low-risk changes:
- Change storage location (disk / object store / DB).
snapshot.Storeis the only thing that holds bytes. Extract itsPut/Getinto aStoreinterface and provide an alternate implementation (e.g. write JPEGs under a configurableprivacy.snapshot_dirwith a retention sweeper, or push to S3).service.captureSnapshotsandmcp.alertSnapshotalready depend only onPut/Get(id), so they need no changes. Add the path/retention knobs toPrivacyConfigand wire them inmain.go. - Persist across restarts. Requires persisting the alert ring too (today both are in-memory), or decoupling snapshot retention from alert retention and keying lookups purely by alert ID. Decide whether evicted-alert snapshots should remain reachable.
- Store short video clips, not single frames. This is the larger change: the
ingest manager currently buffers only the latest frame per camera
(
latestFramesininternal/ingest/manager.go). A clip needs a rolling pre-/post-roll frame buffer per camera (a ring of recent frames + a few seconds of capture after the trigger), assembled into an MP4. The store would hold clip bytes/paths instead of a JPEG, and the serve endpoint would setContent-Type: video/mp4. The alertβmedia keying and the dashboard proxy stay the same; the new cost is buffering memory/CPU on the Jetson and encode time. - Retention tuning.
snapshot.NewStore(max)takes the cap; todaymain.gopasses0(βDefaultMax). Surface it asprivacy.snapshot_retentionif the fixed 200 becomes a constraint.
The BearClawWeb side is storage-agnostic: it proxies bytes through
Home::AlertsController#snapshot β KoalaClient#alert_snapshot and never sees
the Koala bearer token. Switching Koala to disk/clips requires no Rails changes
beyond possibly the <img> β <video> element if clips land.
Generate encrypted/signed bundle + signed manifest:
go run ./cmd/koala-packager \
--artifact /path/to/koala-update.tar.gz \
--bundle-url http://updates.local/koala-update.bundle.json \
--key-id key-2026-03 \
--version 0.2.1 \
--private-key-base64 "$KOALA_UPDATE_PRIVATE_KEY_B64" \
--encryption-key-base64 "$KOALA_UPDATE_ENCRYPTION_KEY_B64" \
--min-orchestrator-version 0.1.0-dev \
--min-worker-version 0.1.0-dev \
--bundle-out /tmp/koala-update.bundle.json \
--manifest-out /tmp/koala-manifest.jsonThe generated bundle includes encrypted artifact bytes + bundle signature.
The generated manifest includes plaintext artifact sha256, created_at, and manifest signature.
go test ./...
cd worker && python -m pytestReplay gate harness (latency + accuracy for check_package_at_door):
go test ./tests -run TestReplayHarnessPackageDoorLatencyAndAccuracyGates -v