Problem: the Oikos control room was browser-only — no native desktop
experience (system tray, notifications, keychain-persisted auth).
Change: add a Wails v3 thin-shell desktop app at cmd/desktop/ that embeds
the existing SPA in a webview. The Go side is ~380 lines — no bundled
server, no Postgres connection. It reads auth from the OS keychain,
injects it into the SPA on load, and the SPA talks HTTPS to the homelab
same as a browser.
Phase 1.0 — Scaffold + window:
- Embed web/dist/ into the Wails binary
- Inject window.__OIKOS_CONFIG__ with keychain-stored apiUrl + token
- 1400×900 window, min 1024×700
- System tray: Open/Quit, click toggles window
Phase 1.1 — Native shell:
- Poll /api/v1/dashboard/summary every 30s; osascript notification
when approvals or critical signals increase
- Save/restore window position to ~/.config/oikos/window.json
- EnableAutoStart/DisableAutoStart — macOS LaunchAgent plist
Phase 1.2 — Token management:
- Config.svelte calls window.wails.Call.ByName('SaveConfig') after
successful connection — persists to OS keychain
- ConfigService binds SaveConfig, ClearConfig, EnableAutoStart,
DisableAutoStart to the Wails runtime
Phase 1.3 — Auto-update:
- Poll Gitea releases API every 6h, compare semver, show dialog
- 'Check for Updates' tray menu item triggers immediate poll
Phase 1.4 — Distribution:
- macOS entitlements.plist: network client + keychain access
- .gitea/workflows/desktop.yml: CI builds macOS arm64 + Linux amd64
on 'desktop-*' / 'v*' tags, attaches artifacts to release
- Makefile: desktop (build), desktop-package (build + zip/tar.gz)
- CONTRIBUTING.md: documented desktop app + commands
Risk: low. Wails v3 alpha API may shift; the Go glue is ~380 lines and
trivially portable. The desktop app is additive — zero changes to the
existing server or SPA logic. No config mutation, no infrastructure
impact.
Verification: go build, go vet, go mod tidy all pass.
6.6 KiB
Contributing to Oikos
Developer guide for the Oikos codebase. If you are a homelab client consuming Oikos, see CLIENTS.md. If you are an AI agent working on the repo, see .agents/dev/CONTRIBUTING.md.
Dev setup
- Go 1.26+ (see
go.modfor pinned version) - PostgreSQL with TimescaleDB — the compose stack includes
timescale/timescaledb:2.17.2-pg16 - Docker for the full dev stack
- Node 22+ for
web/(the control-room SPA — standalone, not part of the compose stack or theoikosbinary)
# Start dependencies (Postgres + Redis). api/nomos require a shared bearer
# token — no dev-open bypass — so set one even for local dev.
OIKOS_MCP_BEARER_TOKEN=dev-token docker compose --profile dev up -d
# Run all tests
make test
# Run integration tests (needs compose Postgres)
make test-db
# Build the binary
make build
# SPA dev server (proxies to api/nomos, injecting the same token)
cd web && OIKOS_API_TOKEN=dev-token npm run dev
Project structure
cmd/desktop/ Wails v3 desktop app (macOS + Linux)
main.go Thin shell: webview, system tray, notifications, auto-update
wails.json Wails project config
entitlements.plist macOS code-signing entitlements
cmd/oikos/ Single-binary entry point
cmd/nomos/ Nomos MCP client gateway
cmd/webhook/ Gitea deploy-webhook receiver (push-to-deploy on mac-mini)
internal/ All Go packages
httpapi/ REST + MCP server (OpenAPI-generated)
mcp/ MCP tool implementations
db/ Connection pool, migrations, seeds, sqlc queries
scheduler/ Observe loop, probes, signals
actuator/ SSH execution
learning/ Pattern recognition, anomaly detection
notifier/ Matrix notifications, approval tokens
policy/ Risk classifier
secrets/ Infisical + SOPS backend
domain/ Core types: entities, approvals, signals, patterns
ontology/ Type hierarchy, relationship validation
knowledge/ Knowledge YAML seed ingestion
web/ Control-room SPA (Svelte 5) — standalone, not embedded
in the oikos binary; see plans/2026-07-12-wails-desktop-app.md
api/openapi.yaml API contract — the source of truth for endpoints
migrations/ Forward-only SQL migrations (TimescaleDB)
seeds/ Bootstrap YAML: ontology, inventory, policy, knowledge
compose/ Dockerfiles + Caddy config
scripts/ Deploy, watchdog, rollback
checks/ Host health-check scripts run over SSH by the scheduler
tools/ Client auto-setup scripts (checks)
nomos/ Nomos config, persona, skills
.agents/ Agent instruction files + skills
plans/ Design documents
docs/adr/ Architecture decision records
docs/operations/ Runbooks (rollback, etc.)
Commands
| Command | Purpose |
|---|---|
make build |
Build oikos binary |
make test |
Run all tests with race detection |
make test-db |
Run integration tests against compose Postgres |
make lint |
go vet + golangci-lint |
make generate |
Regenerate OpenAPI + sqlc code |
make generate-check |
CI drift guard — fail if generated code is stale |
make migrate |
Apply DB migrations |
make seed |
Ingest seeds into DB |
make export |
Export DB state to YAML seeds |
make dev |
Start compose dev stack |
make clean |
Remove binary + test cache |
make ui |
Build the SPA (web/dist/) |
make deploy-ui |
Build + deploy the SPA to the Caddy host |
make desktop |
Build the Wails desktop app for the current platform |
make desktop-package |
Build + package (zip on macOS, tar.gz on Linux) |
make webhook |
Build cmd/webhook (deploy-webhook receiver) |
make tidy |
go mod tidy |
Conventions
APIs are OpenAPI-first
The REST API is defined in api/openapi.yaml. Server code is generated with
oapi-codegen into internal/httpapi/gen/. To add an endpoint:
- Add the path + schema to
api/openapi.yaml - Run
make generate - Implement the handler in
internal/httpapi/impl.go - Add tests in
internal/httpapi/api_test.go
Never hand-edit internal/httpapi/gen/api.gen.go.
Database access is sqlc-first
SQL queries live in internal/db/queries/*.sql. Go code is generated with
sqlc into internal/db/sqlcgen/. Config in sqlc.yaml.
- Queries target pgx/v5 with UUID + timestamptz overrides
- Never hand-edit generated sqlc code
Migrations are forward-only
SQL migrations live in migrations/ as NNN_name.up.sql. There are no down
migrations (see ADR 0008).
Migrations are idempotent where possible (IF NOT EXISTS, DO $$ blocks).
To add a migration:
- Create
migrations/NNN_name.up.sqlwith the next sequence number - Write the DDL
- Run
make migrateto apply
Seeds are DB-generated
seeds/*.yaml are the bootstrap files used by oikos seed. After making
changes via the API, run make export to regenerate the seed files. These
files are version-controlled and serve as DR fallback.
Writing style
Follow .agents/shared/writing-style.md. Documentation is reference prose, not marketing. Banned vocabulary includes "robust", "seamless", "leverage", "utilize", "delve", "cutting-edge".
Risk classification
Every mutation is classified against seeds/policy.yaml before execution.
Four risk classes: read_only, reversible_low, config_mutation,
destructive. The classifier can only lower autonomy relative to policy,
never raise it. When in doubt, escalate.
CI
Gitea Actions runs on push to main and pull requests (ci.yml):
go vet+golangci-lint+govulncheck- Generated code drift check (
make generate-check) - Build (
go build ./...) - Test with race detector + coverage
- Docker build verification (no push)
Coverage gates: policy + learning packages ≥ 80%, others ≥ 60%.
PR workflow
- Create a branch from
main - Make changes, write tests
- Run
make lint test generate-check - Commit with a message following: problem → change → risk → verification
- Push to Gitea; CI gates PRs on green
Secrets
Secrets are managed by Infisical (primary) with SOPS as DR fallback. Never
hardcode secrets. Use environment variables from .env for local dev.
The .env and .infisical-credentials files are gitignored.
Related
- OIKOS.md — operating model, OODA loop, ontology
- CLIENTS.md — for homelab clients consuming Oikos
- docs/adr/ — architecture decision records