Monitoring coverage was 3 of 89 active entities. Three bugs, each hidden by discarded errors in checkdefaults: - writeCheck generated a fresh uuid, inserted the check entity ON CONFLICT (slug) DO NOTHING, then wrote a check_defs row referencing it. On any re-seed the slug already existed, the entity insert no-oped, and the FK violated — aborting the ingest transaction and surfacing as an unrelated failure several entities later. Re-seeding has been broken since; prod's coverage was frozen at its first successful seed. This is what TestSeedIngestIdempotentAndNoDuplicateEdges had been reporting. - shortSlug truncated to the last 8 chars, so all 21 ingress routes collapsed to ".network" and overwrote each other; service:jellyfin collided with lxc:jellyfin. - The ssh-script checker never read the `args` config checkdefaults wrote, so process_check.sh always ran without its unit name and returned "unknown". Coverage is now 75/89. Monitoring is declared per entity type in seeds/ontology.yaml and resolved through the is-a hierarchy, so a type can say it warrants nothing (site, lan, mesh, cluster) and never be reported as a gap. coverageSweep raises an `unmonitored` signal only where a type declares monitoring it lacks — 8 real gaps, no false positives. Also: - entity_types.attribute_schema was never ingested: the seed loader read "attribute_schema" but the YAML says "attributes", so all 60 types stored JSON null. - ListExecutions ignored its declared target/action/correlation_id filters and paginated on a non-unique target slug, dropping and repeating rows. - started_at was captured but only written at terminal state, so a running execution reported NULL for its whole life. The three MCP auto-run copies wrote no timing at all; they are now one autoRun helper. - SSH output was buffered to completion and discarded entirely on timeout. Both sshExec copies now stream through a shared execlog sink into execution_logs, and keep partial output when a command is cancelled. - executions.correlation_id was a random per-execution uuid that correlated nothing; it is now the chat session id, which is what lets the chat tail live output. - reversible_low had no auto-run branch despite policy declaring it unattended. Since computeCommandRisk never returns it, the class only arises when an agent declares it over a read_only command — so gating it penalised candor without adding safety. - backup-target gains a backup-freshness checker (portable find -mmin, since the first target is on macOS), resolving its host by walking backs-up-to backwards. The pre-deploy pg_dump is now a tracked backup target. UI: an Executions section on entity detail with live output tailing, and streamed output under a running `run` call in the chat timeline. Migrations 022-024. Ops.svelte and context.ts exclude execution.output from their refetch triggers, which would otherwise fire once a second per command. Co-Authored-By: Claude <noreply@anthropic.com>
121 lines
7.0 KiB
YAML
121 lines
7.0 KiB
YAML
# Oikos policy seed — risk classes, approval rules, autonomy settings.
|
|
#
|
|
# Bootstraps risk_classes / approval_rules / autonomy_settings (migration
|
|
# 005). After ingest the DB is authoritative; runtime policy changes go
|
|
# through the dual-control meta-approval flow (plan S3) and are exported
|
|
# back here via `GET /api/v1/export`.
|
|
#
|
|
# Adapted from legacy oikos/policy.yaml (2026-07-07):
|
|
# - `commands:`/`mcp_tools:` maps are gone — read endpoints and MCP read
|
|
# tools are read_only by construction and never classified; mutating
|
|
# API calls classify via approval_rules below.
|
|
# - `actions:` map became approval_rules keyed on (entity_type, action);
|
|
# entity types may be abstract (rule inherits down the hierarchy,
|
|
# most-specific match wins: scope_entity > concrete type > ancestor).
|
|
# - `service_overrides:` became scope_entity rules.
|
|
# - `lifecycle_overrides:` became autonomy_settings keys read by the
|
|
# classifier.
|
|
|
|
version: 1
|
|
|
|
risk_classes:
|
|
read_only:
|
|
description: Observes state; cannot change anything.
|
|
approval_required: none
|
|
autonomy_allowed: true
|
|
reversible_low:
|
|
description: >-
|
|
Changes runtime state in a way a single follow-up command undoes
|
|
(restart, cache clear, sync pull). No config or data changes.
|
|
approval_required: none # still subject to global.auto_act + rules below
|
|
autonomy_allowed: true
|
|
config_mutation:
|
|
description: >-
|
|
Changes tracked configuration or deployed software: repo edit + push,
|
|
deploy trigger, Caddy/Gitea/app config, package upgrades. Reversible
|
|
via git, but affects other consumers.
|
|
approval_required: operator # Matrix ✅/❌ (single-use HMAC token)
|
|
autonomy_allowed: false
|
|
destructive:
|
|
description: >-
|
|
Destroys or irreversibly alters data/entities: container destroy,
|
|
disk format, DB wipe, secret rotation, client revocation.
|
|
approval_required: operator_confirmed
|
|
autonomy_allowed: false
|
|
|
|
approval_rules:
|
|
# Signal kinds are looked up as `action` by internal/policy/classify.go, and
|
|
# an unmatched kind silently falls back to reversible_low/operator. Declare
|
|
# `unmonitored` so its routing is intentional: it reports a coverage gap and
|
|
# there is nothing to remediate automatically — closing it means an operator
|
|
# adding a check_def, which is its own deliberate change.
|
|
- {entity_type: entity, action: unmonitored, risk_class: read_only, autonomy_level: auto}
|
|
|
|
# Backup freshness signals. Reporting-only for the same reason: the fix for a
|
|
# stale or missing backup is a deliberate human change (re-run the job, fix
|
|
# the mount, correct the path), never something to auto-remediate. Declared
|
|
# so the routing is intentional rather than the reversible_low/operator
|
|
# fallback an unmatched kind would otherwise get.
|
|
- {entity_type: backup-target, action: backup-stale, risk_class: read_only, autonomy_level: auto}
|
|
- {entity_type: backup-target, action: backup-missing, risk_class: read_only, autonomy_level: auto}
|
|
- {entity_type: backup-target, action: backup-misconfigured, risk_class: read_only, autonomy_level: auto}
|
|
- {entity_type: backup-target, action: backup-unreachable, risk_class: read_only, autonomy_level: auto}
|
|
|
|
# ── Generic rules on (possibly abstract) types ──
|
|
- {entity_type: service, action: restart, risk_class: reversible_low, autonomy_level: auto}
|
|
- {entity_type: service, action: cache-clear, risk_class: reversible_low, autonomy_level: auto}
|
|
- {entity_type: service, action: sync-pull, risk_class: reversible_low, autonomy_level: auto}
|
|
- {entity_type: service, action: db-wipe, risk_class: destructive, autonomy_level: never}
|
|
- {entity_type: compose-stack, action: restart, risk_class: reversible_low, autonomy_level: auto}
|
|
- {entity_type: docker-container, action: restart, risk_class: reversible_low, autonomy_level: auto}
|
|
- {entity_type: machine, action: apt-upgrade, risk_class: config_mutation, autonomy_level: escalate}
|
|
- {entity_type: machine, action: reboot, risk_class: config_mutation, autonomy_level: escalate}
|
|
- {entity_type: machine, action: format-disk, risk_class: destructive, autonomy_level: never}
|
|
- {entity_type: config-repo, action: edit, risk_class: config_mutation, autonomy_level: escalate}
|
|
- {entity_type: deploy-pipeline, action: trigger, risk_class: config_mutation, autonomy_level: escalate}
|
|
- {entity_type: lxc, action: create, risk_class: config_mutation, autonomy_level: escalate}
|
|
- {entity_type: lxc, action: migrate, risk_class: config_mutation, autonomy_level: escalate}
|
|
- {entity_type: lxc, action: restart, risk_class: reversible_low, autonomy_level: auto}
|
|
- {entity_type: lxc, action: destroy, risk_class: destructive, autonomy_level: never}
|
|
- {entity_type: vm, action: restart, risk_class: reversible_low, autonomy_level: auto}
|
|
- {entity_type: vm, action: destroy, risk_class: destructive, autonomy_level: never}
|
|
- {entity_type: dns-record, action: change, risk_class: config_mutation, autonomy_level: escalate}
|
|
- {entity_type: ingress-route, action: change, risk_class: config_mutation, autonomy_level: escalate}
|
|
- {entity_type: storage-pool, action: change, risk_class: destructive, autonomy_level: never}
|
|
- {entity_type: secret, action: rotate, risk_class: destructive, autonomy_level: never}
|
|
- {entity_type: key, action: revoke, risk_class: destructive, autonomy_level: never}
|
|
|
|
# ── Governance objects (the OS's own levers — always operator-gated) ──
|
|
- {entity_type: pattern, action: activate, risk_class: config_mutation, autonomy_level: never}
|
|
- {entity_type: skill, action: activate, risk_class: config_mutation, autonomy_level: never}
|
|
|
|
# ── Per-entity overrides (wide blast radius) ──
|
|
- {entity_type: service, action: restart, scope_entity: "service:caddy",
|
|
risk_class: config_mutation, autonomy_level: escalate}
|
|
# everything *.hubris.network rides on caddy
|
|
- {entity_type: service, action: restart, scope_entity: "service:dns",
|
|
risk_class: config_mutation, autonomy_level: escalate}
|
|
# LAN-wide resolver
|
|
- {entity_type: service, action: restart, scope_entity: "service:authentik",
|
|
risk_class: config_mutation, autonomy_level: escalate}
|
|
# SSO provider — restart locks logins fleet-wide
|
|
|
|
autonomy_settings:
|
|
# Global kill-switch. Cold start = off: the agent escalates everything
|
|
# until patterns validate and the operator raises this (plan: trust is earned).
|
|
global.auto_act: "off" # off | reversible_low
|
|
|
|
# Per-entity hard blocks (checked even when global.auto_act is on)
|
|
never_auto_act.service:caddy: "true"
|
|
never_auto_act.service:dns: "true"
|
|
never_auto_act.service:authentik: "true"
|
|
never_auto_act.host:hubris: "true"
|
|
never_auto_act.host:strong: "true"
|
|
|
|
# Lifecycle-state classifier overrides (from legacy lifecycle_overrides)
|
|
lifecycle_override.provisioning.config_mutation: reversible_low
|
|
# no dependents yet — config changes are cheap
|
|
lifecycle_override.deprecated.refuse: new-inbound-edges
|
|
lifecycle_override.destroyed.refuse: all
|
|
# any action targeting a destroyed entity raises a drift signal instead
|