Incident management system
  • Rust 88.4%
  • Just 4.1%
  • Nix 3.4%
  • Shell 3.3%
  • Go Template 0.8%
Find a file
Simon Shine d4a42d6d2c
Some checks failed
Build and push the API image and the chart / build-and-push (push) Failing after 6m33s
Build and push the API image and the chart / chart (push) Successful in 1m20s
chore(hooks): replace lefthook with hk-nix
### hooks

- lefthook's generated config ran a bare `treefmt`, which the nix flake check sandbox has
  no PATH for, so checks.lefthook-check failed on every run and said nothing useful.
- hk references every tool by absolute store path, so the hook that runs locally is the
  one that runs in CI, and `nix flake check` now passes.
- Installed through git config rather than .git/hooks, so an editor or GUI client started
  outside direnv runs the same hooks.
- pre-commit stays treefmt alone. deadnix, clippy and the lockfile check moved to pre-push,
  where reading the whole workspace is affordable; commit-msg checks conventional commits.

### nixpkgs

- Bumped to current unstable: hk reached nixpkgs after the old pin, and hk-nix's overlay
  wraps an existing pkgs.hk rather than supplying one.
- Drops x86_64-darwin, which nixpkgs 26.11 no longer supports.
2026-10-12 00:10:27 +02:00
.forgejo/workflows feat(chart): publish the helm chart from CI 2026-10-11 21:13:40 +02:00
charts/incidental incidental: temporal incident store (CLI + API) for operator agents 2026-07-07 13:54:53 +02:00
crates incidental: temporal incident store (CLI + API) for operator agents 2026-07-07 13:54:53 +02:00
docs incidental: temporal incident store (CLI + API) for operator agents 2026-07-07 13:54:53 +02:00
nix chore(hooks): replace lefthook with hk-nix 2026-10-12 00:10:27 +02:00
scripts incidental: temporal incident store (CLI + API) for operator agents 2026-07-07 13:54:53 +02:00
.envrc chore(hooks): replace lefthook with hk-nix 2026-10-12 00:10:27 +02:00
.gitignore chore(hooks): replace lefthook with hk-nix 2026-10-12 00:10:27 +02:00
Cargo.lock incidental: temporal incident store (CLI + API) for operator agents 2026-07-07 13:54:53 +02:00
Cargo.toml incidental: temporal incident store (CLI + API) for operator agents 2026-07-07 13:54:53 +02:00
flake.lock chore(hooks): replace lefthook with hk-nix 2026-10-12 00:10:27 +02:00
flake.nix chore(hooks): replace lefthook with hk-nix 2026-10-12 00:10:27 +02:00
justfile feat(chart): publish the helm chart from CI 2026-10-11 21:13:40 +02:00
README.md incidental: temporal incident store (CLI + API) for operator agents 2026-07-07 13:54:53 +02:00
rust-toolchain.toml incidental: temporal incident store (CLI + API) for operator agents 2026-07-07 13:54:53 +02:00

incidental

A temporal incident-management system (CLI + API) built as long-term memory for a Kubernetes operator agent (KAOS). Not primarily a human ticketing tool: the primary reader is a machine that re-derives its beliefs on every invocation, so provenance, confidence, and "what did we believe at time T" are first-class.

The full specification lives in docs/01-design-spec.md.

Design in one paragraph

Incidents are event-sourced: an incident is an append-only stream of immutable events (observations, transitions, hypothesis revisions, evidence, actions); current state is a projection. Every event is bitemporal (occurred_at valid time, recorded_at transaction time), so as-of queries reconstruct an incident exactly as the store knew it at any past moment. Identity is a signature (resource kind / namespace / error class / alert name): re-observing a signature appends to the open incident (a pod crashlooping 400 times is one incident), and re-observing a closed lineage opens a numbered recurrence linked with a typed recurrence_of edge — recurrence count per signature is the escalation signal. Hypotheses carry an earned confidence level: probable/confirmed are rejected unless ruled-out alternatives are recorded. Mutations mirror the agent's authority split: executed actions are logged facts, proposals go through proposed → approved → executed → rolled_back. Every write is attributed to an actor (human, or agent+version) resolved from its API token.

Workspace

Crate What
incidental-core Pure domain: event model, lifecycle FSM, signatures, confidence rules, projections, wire types. No IO.
incidental-server incidental-api binary: axum HTTP API over Postgres (sqlx, embedded migrations).
incidental-client Typed HTTP client over the wire types (use this from the agent).
incidental-cli incidentctl binary: thin shell over the client.

CLI/API parity is by construction: every subcommand is one API call and --output json emits the API's wire types verbatim.

Quickstart (dev)

$ nix develop            # rust, postgres, jq, just, helm, treefmt
$ just db-start          # local postgres on 127.0.0.1:5433 (state in .dev/)
$ INCIDENTAL_BOOTSTRAP_TOKEN=dev-bootstrap just run-api &
$ export INCIDENTAL_TOKEN=dev-bootstrap
$ INCIDENTAL_TOKEN=dev-bootstrap just smoke   # end-to-end exercise

Typical agent session:

# mint a token bound to the agent identity (writes are attributed to it)
$ incidentctl token create --name kaos-prod --actor-kind agent \
    --actor-name kaos --actor-version 0.1.0

# a tick begins
$ RUN=$(incidentctl -o json run start --trigger periodic | jq -r .id)

# observe a condition; same signature never duplicates
$ incidentctl observe --resource-kind Pod --namespace media \
    --error-class CrashLoopBackOff --alert-name KubePodCrashLooping \
    --severity sev3 --title "jellyfin crashlooping" \
    --source kubectl --idempotency-key tick42-obs1

# investigate: hypotheses are a revision history, confidence is earned
$ incidentctl hypothesis "$INC" --kind proximate \
    --statement "bad ConfigMap rollout" --confidence probable \
    --ruled-out "memory pressure: RSS flat under load"

$ incidentctl evidence "$INC" --kind log-excerpt --source kubectl \
    --body-file - < excerpt.log

# act (after human approval per policy), resolve, close the loop
$ incidentctl action "$INC" --command "kubectl rollout undo deploy/jellyfin" \
    --effect "pods healthy" --run "$RUN"
$ incidentctl transition "$INC" --to resolved --cause fixed
$ incidentctl proposal submit --incident "$INC" --surface alerting \
    --title "alert on config-rollout crashloops" --body-file proposal.md

$ incidentctl run finish "$RUN" --report "1 incident resolved" --touched "$INC"

# later: how did we see it mid-incident? has it recurred?
$ incidentctl show "$INC" --as-of 2026-07-07T12:00:00Z
$ incidentctl signature "$FP"

API ≡ CLI surface

incidentctl HTTP
observe POST /v1/observations
list GET /v1/incidents
show [--as-of] GET /v1/incidents/{id}[?as_of=]
events GET /v1/incidents/{id}/events
transition POST /v1/incidents/{id}/transition
severity POST /v1/incidents/{id}/severity
hypothesis POST /v1/incidents/{id}/hypotheses
evidence POST /v1/incidents/{id}/evidence
link POST /v1/incidents/{id}/links
action POST /v1/incidents/{id}/actions
comment POST /v1/incidents/{id}/comments
signature GET /v1/signatures/{fingerprint}
proposal submit/advance/list/show POST /v1/proposals, POST /v1/proposals/{id}/advance, GET …
run start/finish/show/list POST /v1/runs, POST /v1/runs/{id}/finish, GET …
token create/list/revoke POST/GET /v1/tokens, DELETE /v1/tokens/{id}
health GET /healthz (also /readyz, /metrics)

Wire contract: the serde types in incidental_core::wire (version v1). Writes accept expected_seq for optimistic concurrency (HTTP 409 on conflict) and idempotency keys where re-delivery is expected. CLI exit codes: 0 ok, 1 error, 2 usage, 3 not found, 4 conflict.

Lifecycle (incidentctl transition --to …):

detected → triaged → investigating → mitigating → resolved → closed
         ↘ suppressed        reopened ↗ (investigating)
         ↘ false-positive

Resolving requires --cause fixed|self-healed; the store keeps self-healed-then-recurring visible because it is intermittent-fault territory.

Auth

Static bearer tokens, SHA-256-hashed at rest, each bound to an actor identity. Bootstrap: start the API with INCIDENTAL_BOOTSTRAP_TOKEN=…, mint real tokens via incidentctl token create, then remove the variable.

Nix

$ nix build .#incidentctl      # CLI (also the default package)
$ nix build .#incidental-api   # API server
$ nix build .#oci-image        # docker-archive tarball running the API
$ nix run . -- --help          # run the CLI
$ nix flake check              # builds all crates + lefthook/treefmt checks

Push the image:

$ nix build .#oci-image -o result-oci
$ skopeo copy docker-archive:result-oci docker://registry.example.com/incidental-api:0.1.0

Helm

$ helm install incidental charts/incidental \
    --set image.registry=registry.example.com \
    --set database.existingSecret=incidental-db-app \
    --set bootstrap.existingSecret=incidental-bootstrap

The chart consumes a Postgres URI from an existing secret (CloudNativePG <cluster>-app convention, key uri), or templates its own CNPG Cluster with --set database.cnpg.enabled=true. Optional metrics.serviceMonitor.enabled=true wires /metrics into the Prometheus operator. Migrations run at API startup.

Spec §9 caveat: run the store (and its database) off the cluster it observes — it has to be up precisely when that cluster is on fire. The chart exists for deploying to a different cluster, or accepting the risk consciously.

Deferred (v2)

Suppression windows, per-agent-version confidence calibration, evidence retention tiers, and redaction on ingest (spec §§3–5) are deliberately not in v1; the schema leaves room for each.