AGPL-3.0-or-later · forever.

showcase · tier 3 · ops & support

Config drift sentinel

Builds the sanctioned baseline (RFC 7396 merge), diffs it against live prod (RFC 6902), and pages on-call only for the drift nobody approved, with a model explaining what changed.

Silence means prod matches exactly what was signed off. That is the whole alert policy. · conformance-gated in nika-spec ↗ · re-proven at every push

  • 10tasksthe plan falls out of the bindings
  • 6wavesparallel by construction
  • 2verbs exercisedinvoke · infer
  • T3the tierops & support

The whole file

10 tasks · 6 waves

The plan falls out of the bindings, nothing is scheduled by hand.

config-drift-sentinel.nika.yamlsource
nika: v1workflow:  id: config-drift-sentinel  description: "live config vs sanctioned baseline → typed drift → triaged, explained alert"model: ollama/qwen3.5:4b   # local · zero key · swap for anthropic/claude-haiku-4-5 (explain is cheap)inputs:  config_url:    type: string    default: "https://api.internal.example.com/v1/config"    description: "The live-config endpoint · the placeholder resolves nowhere, which is what makes the default run a rehearsal"const:  # The committed rehearsal baseline. Point this at your real intended state  # and change `permits.fs.read` in the same edit — a permit is a literal you  # can read, it cannot interpolate a const (`NIKA-AUTH-007`).  baseline_path: "./examples/fixtures/config-baseline.json"  # Sanctioned drift · an RFC 7396 merge patch applied to the baseline before  # the diff. `{}` means « nothing is pre-approved ».  #  # This is a BARE LITERAL, deliberately. A typed constant is `{ type, value }`  # and nothing else: an object missing either key is read as a literal object  # (`01-envelope.md:273`). Writing `{ type: object, default: {} }` here would  # declare nothing — `${{ const.approved_overrides }}` would resolve to that  # whole three-key map. Measured, with `{ replicas: 6 }` as the baseline:  #   {"default":{},"description":"…","replicas":6,"type":"object"}  # No error, no warning. The sentinel would just spend every morning diffing  # prod against a config nobody ever intended.  approved_overrides: {}  # An RFC 6902 patch path starting with one of these can take the service  # down. Everything else is recorded and read in the morning. The list lives  # here, in the file, instead of inside a prompt: paging policy is reviewable.  paging_prefixes: ["/replicas", "/feature_flags", "/upstreams", "/limits"]secrets:  oncall_webhook:    source: env    key: ONCALL_WEBHOOK_URL    egress:                       # sanction the one send · the secret IS the URL      - to: "nika:notify"        host_from_self: truepermits:  tools: ["nika:emit", "nika:fetch", "nika:hash", "nika:jq", "nika:json_diff", "nika:json_merge_patch", "nika:notify", "nika:read"]  net:    # TWO hosts, two different jobs.    #    # `host_from_self:` above sanctions the FLOW (this secret may be the URL)    # — it does not grant the capability. The webhook host stays unknown at    # check time, so it is judged at RUN against this list: name the    # escalation host here or the page is refused mid-run, after the drift    # has already been found. Measured, all three cases · with the host    # absent, or with some other host named, `check` is green and the run    # dies `NIKA-SEC-004 · <host> resolves outside the declared net.http    # boundary`; with it named, the permit clears and the send goes out.    http:      - "api.internal.example.com"   # the config endpoint this sentinel polls      - "hooks.slack.com"            # where the page goes · swap for your own  fs:    # Read-only by design: a drift sentinel OBSERVES prod, it never edits it.    # There is no `write:` key at all, so every path on this machine is denied    # for writes — the baseline is the one file this workflow may open.    read: ["./examples/fixtures/config-baseline.json"]tasks:  # ── what prod says it is · as BYTES ────────────────────────────────  # `mode: raw` keeps the response as the string the server sent, which is  # what `fingerprint` hashes below. The recovery value is that same string:  # a real HTTP body, one benign step off the baseline (log_level info →  # debug), so the triage downstream has something honest to triage. Point  # `--var config_url=` at a live endpoint and this branch never runs.  live_raw:    on_error:      recover: '{"service":"checkout-api","replicas":6,"log_level":"debug","feature_flags":{"beta_ui":false,"instant_refunds":true,"queue_v2":false},"limits":{"request_timeout_ms":2500,"max_body_bytes":1048576},"upstreams":{"ledger":"https://ledger.internal.example.com","risk":"https://risk.internal.example.com"}}'    retry:      max_attempts: 3      backoff_strategy: exponential    invoke:      tool: "nika:fetch"      args:        url: "${{ inputs.config_url }}"        mode: raw  # ── what prod is supposed to be · also bytes ───────────────────────  baseline_raw:    invoke:      tool: "nika:read"              # returns a STRING · a file is text until you parse it      args: { path: "${{ const.baseline_path }}" }  # Provenance over the exact response bytes, taken BEFORE the parse: two  # different JSON texts that mean the same thing hash differently, and that  # is the point — this attests the artifact, not the interpretation.  # `nika:hash` requires `content:` to be a string; handing it the parsed  # object fails `NIKA-BUILTIN-HASH-001` at run while `check` stays green.  fingerprint:    with:      raw: ${{ tasks.live_raw.output }}    invoke:      tool: "nika:hash"      args:        algo: blake3        content: "${{ with.raw }}"        encoding: hex  # ── text becomes data, in one place ────────────────────────────────  # Both sides are parsed together so the « where did this stop being a  # string » question has exactly one answer in this file. `json_merge_patch`  # and `json_diff` take OBJECTS: feeding either one the raw string fails  # `NIKA-BUILTIN-JSON_MERGE_PATCH-001` at run, and `check` cannot see it  # coming — the type only exists once the tool has run.  parsed:    with:      live: ${{ tasks.live_raw.output }}      baseline: ${{ tasks.baseline_raw.output }}    invoke:      tool: "nika:jq"      args:        input: { live: "${{ with.live }}", baseline: "${{ with.baseline }}" }        expression: "{ live: (.live | fromjson), baseline: (.baseline | fromjson) }"  expected:    with:      baseline: ${{ tasks.parsed.output.baseline }}    invoke:      tool: "nika:json_merge_patch"     # RFC 7396 · objects merge, null deletes      args:        target: "${{ with.baseline }}"        patch: "${{ const.approved_overrides }}"  # ── the difference, as data ────────────────────────────────────────  drift:    with:      expected: ${{ tasks.expected.output }}      live: ${{ tasks.parsed.output.live }}    invoke:      tool: "nika:json_diff"            # RFC 6902 · [{op, path, value}, …]      args:        before: "${{ with.expected }}"        after: "${{ with.live }}"  # ── triage · which of those operations is worth a phone call ───────  # `any($p[]; …)` is true when the operation's path starts with any prefix.  # A log_level change survives the diff and dies right here, which is the  # difference between a sentinel people keep and one they mute.  paging:    with:      patch: ${{ tasks.drift.output }}    invoke:      tool: "nika:jq"      args:        input: { patch: "${{ with.patch }}", prefixes: "${{ const.paging_prefixes }}" }        expression: >-          .prefixes as $p          | .patch          | map(select(.path as $path | any($p[]; . as $pre | $path | startswith($pre))))  explain:    with:      patch: ${{ tasks.drift.output }}    when: ${{ size(with.patch) > 0 }}   # a clean scan spends nothing    on_error:      recover: "(explanation unavailable · the model call failed · the raw patch is attached)"    infer:      max_tokens: 600                   # three bullets · a ceiling, not a hope      prompt: |        This RFC 6902 patch is UNSANCTIONED config drift in production ·        ${{ with.patch }}        Explain in 3 bullets · what changed · likely blast radius · first check.  # ── the page carries the FACT · never the payload ──────────────────  #  # All three trifecta legs are structural here: the baseline is a private  # read, the live config is untrusted ingress, and the webhook is external  # egress. Interpolating the diff — or the explanation derived from it —  # into this message would be a realized flow: someone who can move prod  # config could shape the diff until it echoes baseline values out through  # the webhook. So the page says THAT drift happened and where to look; the  # drift itself goes to the journal below, which never leaves the machine.  # `when:` may still read the patch — a gate decides, it does not transmit.  alert:    with:      paging: ${{ tasks.paging.output }}    when: ${{ size(with.paging) > 0 }}    invoke:      tool: "nika:notify"      args:        channel: webhook        target: "${{ secrets.oncall_webhook }}"        message: "Unsanctioned config drift on a paging path · details in the config.drift.scan journal event"        severity: critical  # `nika:emit` is a LOCAL event · it needs no `net:` grant because nothing  # leaves the machine. The full patch, the provenance fingerprint and the  # explanation land here, where whoever answers the page reads them. On a  # clean scan `explain` is gated off and resolves null — the scan is still  # recorded, which is how you prove the sentinel ran at all.  record:    with:      patch: ${{ tasks.drift.output }}      paging: ${{ tasks.paging.output }}      live_hash: ${{ tasks.fingerprint.output }}      explanation: ${{ tasks.explain.output }}    invoke:      tool: "nika:emit"      args:        event_type: "config.drift.scan"        payload:          patch: ${{ with.patch }}          paging: ${{ with.paging }}          live_hash: ${{ with.live_hash }}          explanation: ${{ with.explanation }}outputs:  drift:    value: ${{ tasks.drift.output }}    description: "RFC 6902 operations · empty when prod matches the sanctioned state"  paging:    value: ${{ tasks.paging.output }}    description: "The subset of those operations that is worth a phone call"  live_hash:    value: ${{ tasks.fingerprint.output }}    description: "blake3 of the exact response bytes · the provenance receipt"

nika inspect · engine 0.106.1 · vendored graph, never re-derived

13 declared edges · 6 waves · the plan falls out of the bindings