Files
oikos/CONTRIBUTING.md
dtoro 64f7d54011
Some checks failed
ci / build-test (push) Has been cancelled
ci / docker-build (push) Has been cancelled
feat: Phase 2 — ports package, secrets port move, postgres adapter move
Problem: the hexagon's Phase 2 (plans/2026-08-15-hexagonal-architecture.md)
must give the use-cases-to-be their contract surface: driven-port
interfaces, test fakes, the secrets interface moved into core, and the
postgres package inside the adapters tree — before the first vertical
slice (Phase 3) can wire a composition root.

Change:
- internal/core/ports: full driven-port catalog per plan §3.3 —
  repositories as transaction-scoped aggregates whose inputs carry
  derived checks, audit, and events (§3.6), plus CommandExecutor,
  TargetResolver, Checker, Secrets, EventPublisher, Provisioner.
  Port-local payload types (Event, AuditEntry, CheckDef, KnowledgeEntry,
  ExecResult) keep signatures off infrastructure; TypeTree aliases
  internal/ontology (pure over domain) until checkdefaults is absorbed.
  ReadModels intentionally not declared yet — it materializes with the
  Phase 3 slice and grows as report handlers rewire.
- secrets.Backend is now an alias of ports.Secrets; implementations
  (Infisical, SOPS, Manager) unchanged. mcp's local secretBackend
  subset is deleted; tool constructors take ports.Secrets.
- internal/db → internal/adapters/postgres (mechanical import rewrite;
  package identifier stays db until the Phase 3 repository split).
  sqlc.yaml, Makefile, golangci exclusions, and docs follow the move;
  make generate-check verified.
- internal/adapters/ssh: Executor implements ports.CommandExecutor over
  the actuator dial pool + RunStreaming (10-min default timeout carried
  over from the httpapi path).
- internal/adapters/remote: Resolver implements ports.TargetResolver
  delegating to internal/remote (still pool-based; drops onto
  ports.EntityRepository when repositories land in Phase 3 — documented
  transitional import).
- internal/core/ports/portstest: importable fakes — in-memory
  EntityRepo (with check-then-act SetState, side-effect recording),
  RecordingExecutor, FakeChecker, SpyPublisher; port-satisfaction
  guards; tests.

Risk: ports are declared ahead of implementations — signatures firm up
per phase as slices land (documented in the package doc); the
remote→postgres transitional import is explicit and dissolves in
Phase 3.

Verification: go vet, make test (race, 19 packages), generate-check,
golangci on core+adapters — 0 issues; full-repo baseline down
365→344.
2026-08-15 22:56:56 +02:00

7.2 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.mod for pinned version)
  • PostgreSQL with TimescaleDB — the compose stack includes timescale/timescaledb:2.17.2-pg16
  • Docker for the full dev stack
  • The control-room SPA and desktop app live in their own repo — dtoro/oikos-web (Node 22+ there); this repo is backend-only since the hexagonal refactor Phase 1
# 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 (own repo — dtoro/oikos-web)
cd ~/Projects/oikos-web/web && OIKOS_API_TOKEN=dev-token npm run dev

# Desktop app (macOS — also in the oikos-web repo)
cd ~/Projects/oikos-web && make install

Desktop app

The desktop app's auth (Authentik login → keychain-persisted token) and auto-update (Gitea releases every 6 hours) moved with it to dtoro/oikos-web — see that repo's README. Auto-update now tracks oikos-web releases; builds installed before the split need one manual reinstall.

Project structure

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
  core/             Hexagon core ([ADR 0016](docs/adr/0016-hexagonal-ports-adapters.md)):
                    domain/ (pure model, stdlib only), ports/ (driven-port
                    interfaces), app/ (use-case services) — populated by the
                    phased refactor (plans/2026-08-15-hexagonal-architecture.md);
                    core may not import adapters, enforced by depguard
  adapters/         Ports' implementations (postgres, ssh, probes, remote,
                    secrets, events) + driving adapters (httpapi, mcpserver,
                    scheduler, execworker, cli) — scaffolded; packages move
                    here phase by phase
  httpapi/          REST + MCP server (OpenAPI-generated)
  mcp/              MCP tool implementations
  db/               (moved) → internal/adapters/postgres: pool, migrations,
                     seeds, sqlc queries — package still named `db` until
                     the Phase 3 repository split
  scheduler/        Observe loop, probes, signals
  actuator/         SSH execution
  learning/         Pattern recognition, anomaly detection
  policy/           Risk classifier
  secrets/          Infisical + SOPS backend
  ontology/         Type hierarchy, relationship validation
  knowledge/        Knowledge YAML seed ingestion
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 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:

  1. Add the path + schema to api/openapi.yaml
  2. Run make generate
  3. Implement the handler in internal/httpapi/impl.go
  4. 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/adapters/postgres/queries/*.sql. Go code is generated with sqlc into internal/adapters/postgres/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:

  1. Create migrations/NNN_name.up.sql with the next sequence number
  2. Write the DDL
  3. Run make migrate to 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):

  1. go vet + golangci-lint + govulncheck
  2. Generated code drift check (make generate-check)
  3. Build (go build ./...)
  4. Test with race detector + coverage
  5. Docker build verification (no push)

Coverage gates: policy + learning packages ≥ 80%, others ≥ 60%.

PR workflow

  1. Create a branch from main
  2. Make changes, write tests
  3. Run make lint test generate-check
  4. Commit with a message following: problem → change → risk → verification
  5. 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.

  • OIKOS.md — operating model, OODA loop, ontology
  • CLIENTS.md — for homelab clients consuming Oikos
  • docs/adr/ — architecture decision records