# AGENTS.md — orientation for any agent on a homelab client

You are running on a machine that is part of the **hubris** homelab. The full
context is in this checkout at `/opt/homelab/`. This file is the entry
point. Read it once at start, then keep working.

- **New client?** Read [CLIENTS.md](CLIENTS.md) first.
- **Developing on this repo?** Also read [.agents/dev/CONTRIBUTING.md](.agents/dev/CONTRIBUTING.md).

The operating model — OODA loop, risk classes, approval rules, the ontology,
and node lifecycle — is defined in [OIKOS.md](.agents/OIKOS.md). Before any mutation,
classify the action against `seeds/policy.yaml`; when the class requires
approval, stop and ask the operator.

Agent-facing instruction lives under `.agents/`:
`.agents/shared/` holds the conventions every agent applies
([writing-style](.agents/shared/writing-style.md), [caveman](.agents/shared/caveman.md),
[page-templates](.agents/shared/page-templates.md), [llm-wiki](.agents/shared/llm-wiki.md)), and
`.agents/domains/` holds the per-domain schemas
([knowledge](.agents/domains/knowledge/schema.md), [operations](.agents/domains/operations/schema.md)).

**Source of truth:** The Postgres database is the single source of truth for all
structured data and knowledge. It is bootstrapped from `seeds/` at deploy time:
`seeds/ontology.yaml` (entity types, relationships, lifecycles), `seeds/inventory.yaml`
(hosts, services, entities), `seeds/policy.yaml` (risk classes, approval rules), and
`seeds/knowledge.yaml` (documents, investigations, runbooks). The old narrative wiki
is archived at `archive/knowledge/` for historical reference.

## 1. Who you are

Run `hostname` (Linux) or `scutil --get LocalHostName` (macOS), then read:

    /opt/homelab/inventory.yaml

That file tells you your role, your peers, what's mounted, and what services
you host. If it does not exist, this client was not enrolled — stop and tell
the operator; see [CLIENTS.md](CLIENTS.md#enrollment) for the enrollment flow
(the entity needs to exist in `planned`/`provisioning` state first).

## 2. The topology

- `/opt/homelab/inventory.yaml` — every host, LXC, VM, and workstation
  with their mesh addresses, roles, and service mappings. This is the seed file;
  at runtime the DB is authoritative (query via MCP `get_entity` or the REST API).
- `/opt/homelab/seeds/knowledge.yaml` — full narrative knowledge
  (documents, investigations, runbooks). Counts are not hardcoded here; count
  them from the seed or query the DB. Ingested into the DB on deploy.
- `/opt/homelab/.agents/operations/commands.md` — the operator's cheatsheet
  for pct, caddy, DNS, and the Oikos command surface.

## 3. The MCP server

The homelab exposes a Model Context Protocol server with structured tools.
Endpoint: `https://mcp.hubris.network/mcp`. Every call needs
`Authorization: Bearer <token>` — the API has no unauthenticated path except
enrollment and `/healthz` (see "Authentication" below for where the token
comes from).

Available tools (67 total — the authoritative list; do not hardcode the count
elsewhere; regenerate from `internal/mcp/` when tools change):

  💡 Slug param alias: all entity-lookup tools now accept `slug` in addition to
     their declared param name (e.g. `get_entity(slug="lxc:seanime")` works).

  Entity Tools — knowledge graph, discovery, and lifecycle:
    ping — lightweight connectivity check
    get_entity(slug_or_id) — get an entity by slug or UUID
    list_entities(type, state, q, limit) — entities filtered by type, state, or search
    get_relations(entity_id, types) — list inbound/outbound edges for one entity
    get_blast_radius(entity_id, depth=3) — entities affected if this one goes down
    create_entity(type, name, slug, attributes, state) — create a new entity in the graph
    update_entity_attributes(slug, attributes) — merge discovered facts into an entity
    set_entity_state(slug, state) — transition entity to a new lifecycle state
    create_relationship(source, target, type) — record a discovered edge
    end_relationship(source, target, type) — soft-delete an active edge
    whoami(hostname) — entity record, peers, and health for a host
    explain(service_slug) — compact context card (type, state, health, relations)
    preflight(service_slug, action) — risk class + approval requirement
    get_change_history(entity_slug, limit=20) — last audit-log entries per entity
    get_state_snapshot() — last scheduler Observe-pass: fleet health, disk, drift
    audit_knowledge_graph() — read-only drift report over the graph and checks
    discover_infra_drift() — running guests vs DB: missing/ghost entities
    find_entities_by(key, value, limit=25) — search entities by attribute values

  Ops Tools — live state, signals, checks, and execution:
    run(target, command, purpose, declared_risk) — general execution primitive; read-only auto-acts, mutations queue for approval, destructive always needs explicit confirmation
    inspect_path(path, targets) — bulk mount/df/ls/stat across multiple hosts/LXCs
    get_execution_status(execution_id) — poll execution progress
    tail_log(service_slug, lines=50) — journalctl for a service
    get_service_status(service_slug) — systemctl is-active/is-enabled
    get_lxc_state(lxc_slug) — pct status from Proxmox host
    ping_service(service_slug) — HTTP reachability + scheduler health state
    list_lxcs(state) — all LXC containers with ID, host, IP, last-audited hint
    restart_service(target, service) — restart a systemd service (config_mutation, requires approval)
    docker_exec(lxc_slug, container, command, purpose) — run a command inside a Docker container on an LXC; handles escaping, resolves target from entity graph; read-only auto-runs, mutations require approval
    push_file(target, source_path, dest_path, backup=true) — push a file into an LXC from the Proxmox host (config_mutation, requires approval)
    ack_signal(signal_id) — acknowledge an open signal
    resolve_signal(signal_id, resolution) — resolve a signal with optional note
    mute_signal(signal_id, duration_s=3600) — temporarily mute a signal
    cancel_execution(execution_id, reason) — cancel a queued/running execution
    update_check(check_id, enabled) — enable or disable a health check
    list_checks(entity_slug, enabled) — list health checks with verdict, probe kind
    list_executions(entity_slug, status, limit=25) — cursor-paginated execution history
    list_entity_sessions(entity_slug) — active Nomos sessions linked to an entity
    get_dashboard_summary() — fleet overview: counts, health, signals, approvals
    list_approvals(status, entity_slug, limit) — list pending/recent approvals; filter by status (pending, approved, denied) or entity
    decide_approval(approval_id, decision) — approve or deny a pending execution; calls the same API endpoint as the Approve button in the UI
    get_secret(key, path, environment) — retrieve a secret from the Infisical vault
    list_secrets(path_prefix) — list secret keys in the Infisical vault
    set_secret(key, value, path, environment) — store/update a secret (requires approval)

  Knowledge Tools — search, read, and maintain the knowledge base:
    search_knowledge(query) — full-text search across docs (snippets, not full body)
    get_entity_knowledge(entity_slug) — all docs/investigations/runbooks linked to a slug
    get_knowledge_content(slug) — full markdown body of one knowledge entry
    upsert_knowledge(title, content, about, tags, kind) — write what you learned
    upsert_session_summary(session_id, summary, entities_touched, discoveries) — batch-write session findings into the graph; creates knowledge entries, links entities, records a session-audit entry
    delete_knowledge(knowledge_slug) — soft-delete a knowledge entry
    restore_knowledge(knowledge_slug) — restore a soft-deleted entry
    merge_knowledge(target_slug, source_slugs) — fold entries into a target
    rename_knowledge_tag(from, to) — bulk-rename tags across all entries
    get_knowledge_revisions(knowledge_slug) — version history for a knowledge entry
    get_knowledge_duplicates(threshold=0.6) — near-duplicate detection via trigram similarity
    get_knowledge_orphans(stale_days=90) — unlinked, untagged, or stale entries
    list_knowledge_tags() — all tags with usage counts and casing variants
    list_my_secrets(caller_pubkey) — secrets accessible to a client by age public key

  Analysis Tools — fleet health, metrics, and introspection:
    get_health_summary(health) — fleet health per entity, optionally filtered
    get_audit_trail(entity_id) — query the audit log
    query_metrics(hours=24) — time-series with bucketed avg/min/max
    get_signal_history(entity_slug, state, limit=50) — open and recent signals
    get_patterns(status, entity_type, action) — learned action patterns
    get_skills(status) — available automation skills
    get_trend(entity_id, days=7) — metric slope, variance, and averages
    get_event_timeline(severity, entity_slug, limit=50) — recent events
    get_agent_activity(limit=50) — agent self-inspection log
    classify_command(command, declared_risk) — pre-flight risk classification before `run`
    get_ontology() — entity types, relationship types, and lifecycle definitions
    http_get(url) — fetch a public web page/raw file; ~16KB cap

**When to prefer MCP over grepping the clone:** always for knowledge queries.
`search_knowledge("jellyfin hardware acceleration")` returns ranked results from
the DB with entity links. `get_entity_knowledge("lxc:jellyfin")` returns documents,
runbooks, and investigations in one call. Grep the clone only when MCP is
unreachable.

## 4. Authentication

Every API/MCP route requires `Authorization: Bearer <token>` except
`POST /api/v1/clients/enroll` and `/healthz`. Enrollment (see
[CLIENTS.md](CLIENTS.md#enrollment)) does not currently issue a per-client
API/MCP bearer token — there is one shared
secret (`OIKOS_MCP_BEARER_TOKEN`, validated in `internal/httpapi/server.go`'s
`combinedAuth`); get it from the operator until per-client token issuance
exists. The SPA has its own flow instead: a first-launch Config screen that
stores a token in `localStorage` (the SPA lives in the `dtoro/oikos-web`
repo — `web/src/pages/Config.svelte` there — since the Phase 1 client
extraction).

## 5. Knowledge conventions

All narrative knowledge (documents, investigations, runbooks) lives in the DB
(`knowledge_entities` table) and is seeded from `seeds/knowledge.yaml`. Agents
can register new knowledge via the API:

```
POST /api/v1/knowledge/{entity_slug}
{"title": "...", "content": "...", "tags": ["..."]}
```

The DB is the truth. The old wiki files are archived at `archive/knowledge/`
(historical reference only — use MCP `search_knowledge` for live queries).

- **Runbook procedures** live as `runbook` entities in the DB and as SKILL.md
  files under `.agents/skills/<name>/`. They carry `risk_class`, `procedure`
  (JSON-schema-validated), and are linked to entity types via `applies_to_type`.
- **Investigations** are `investigation` entities linked to affected entities
  via `about` edges.
- **Documents** are `document` entities linked to entities via `documents` edges.
  They carry `at_glance` (structured attributes) and `changelog` (parsed entries).
- **Live state precedence.** If you observe a discrepancy between the docs and
  running state, update the DB *in the same session* via the API. The `oikos export`
  command regenerates `seeds/knowledge.yaml` for version control.

## 6. Acting on the homelab

- **Read state**: use MCP tools. Nomos (the AI agent) is the primary
  operator interface — it routes to the MCP tool list in §3 for
  observe/orient/decide/act.
- **Actions** (restart, logs, apt, pct exec, or anything else): Nomos calls
  `run` (the general execution primitive) via MCP. `reversible_low`/read-only actions execute
  immediately; `config_mutation` and `destructive` actions are queued for
  operator approval via the App button in the control-room UI or via
  `list_approvals`/`decide_approval` MCP tools from any agent.
- **Secrets**: managed by Infisical (`oikos secret` subcommand for
  migration). Never hardcode secrets — use env vars from `.env`.
- **Mutations** (restart, edit configs, etc.): classified against
  `seeds/policy.yaml`. `reversible_low` actions auto-execute;
  `config_mutation`/`destructive` actions require approval — granted by
  the operator via the App button or `decide_approval` MCP tool call.
  See OIKOS.md.

## 7. Communication mode

Read and apply `/opt/homelab/.agents/shared/caveman.md` (if present). It defines the lab's
terse-communication standard — drop filler, keep substance, use fragments.

## 8. Auto-setup mechanism

The homelab-context repo ships tooling that gets automatically installed
on every client after `git pull`. This is handled by `tools/post-pull.sh`
(replaces the raw git pull in the sync timer) which runs any script matching
`tools/setup-*.sh` after pull.

Currently auto-setup:
- **Host checks** (`tools/setup-checks.sh`): Deploys `checks/install.sh`'s
  health-check scripts to `/opt/oikos/checks` on each host. The scheduler's
  `ssh-script` check kind depends on these actually being there (count is
  whatever is currently seeded in the DB — do not hardcode it here).

To add a new auto-setup, create `tools/setup-<name>.sh` in the repo,
commit and push. All enrolled clients pick it up within 5 minutes.

To trigger sync manually: run `/opt/homelab/tools/context-poller.sh`, or
wait for the 5-min timer. (The server-side `tools_changed` detection only
correctly recognizes `setup-*.sh` scripts — earlier it silently matched
nothing, so nothing auto-ran on any client via this path.)

## 9. Versioning

Every commit to `main` **MUST bump the version** in the `VERSION` file at the
repo root. The format is semver-ish: `major.minor.patch` (e.g. `0.2.3`).

Rules:
- **patch** (`0.2.2` → `0.2.3`): bugfixes, small tweaks, docs-only changes
- **minor** (`0.2.3` → `0.3.0`): new features, new tools, visible functionality
- **major** (`0.3.0` → `1.0.0`): breaking changes (API removal, tool retirement)

The version is shown in the UI sidebar. The `v` prefix is added at build time.

## 10. When in doubt

Use MCP tools: `search_knowledge <query>` for narrative context,
`get_entity <slug>` for structured data, `get_entity_knowledge <slug>` for
everything linked to an entity. The clone is the fallback; MCP is the index.