Skip to main content

No project description provided

Project description

Koherent

codecov PyPI version Maintenance Maintainer PyPI pyversions PyPI status PyPI download month Code style: black Checked with mypy Ruff

What is Koherent?

Koherent adds provenance-aware audit logging to Django applications that expose a Strawberry GraphQL API. It is a thin layer over django-simple-history: every change to a tracked model is recorded as a history row, and each row is attributed to who changed it, which client/app they used, and — crucially — the verified task it happened under.

It answers questions like "which automated run touched this record, on whose authority, and what exactly did it change?" and exposes those answers directly in your GraphQL schema as a queryable, filterable provenance field.

Installation

pip install koherent   # pulls in django-simple-history, authentikate, kante

Koherent is a Django app. Add it to INSTALLED_APPS after authentikate (it relies on the authentikate user/client/organization context):

INSTALLED_APPS = [
    # ...
    "authentikate",
    "koherent",
    "your_app",
]

AUTH_USER_MODEL = "authentikate.User"

Scope

Koherent is responsible for three things and nothing else:

  1. Recording — a ProvenanceField on your model captures every create/update/delete as a history row (via django-simple-history).
  2. Attributing — a signal stamps each history row with the acting user, the client (app), and a Task resolved from the request's verified provenance token.
  3. Exposing — Strawberry types (ProvenanceEntry, Task) and a flat, semantic filter (ProvenanceFilterMixin) surface that history in your GraphQL API.

It does not authenticate requests, mint tokens, or define the transport — those belong to authentikate and kante respectively (see below). Koherent is the piece that sits between them and turns an authenticated, context-carrying request into a durable, queryable audit trail.

Note: Koherent is built for the Arkitekt / Rekuest ecosystem. The "task" it tracks is a Rekuest task, attested by a signed provenance token.

What gets tracked

Every history row (a ProvenanceEntry) carries:

Field Meaning
user The user who made the change (history_user).
client The OAuth client / app that made the change, if any.
task The verified task the change ran under, if any.
kind CREATE / UPDATE / DELETE.
date When the change happened.
effective_changes The per-field old→new diff for that row.

The task is a Task row built once per task from the provenance token's claims (task id and its parent/root, the root human causer, the executing agent, the issuer, the single-use token id, and an args hash).

What is a task?

A task id (a.k.a. correlation_id / context_id) groups together every change made during one logical run. In Arkitekt, when a user calls an app through a Rekuest, all of that app's mutations carry the same task id — so you can later find, audit, or revert every change a single run produced.

How it fits together

        Rekuest provenance token (signed, EdDSA)
                       │
        ┌──────────────▼───────────────┐
 kante  │  HttpContext / WsContext      │   ← GraphQL transport + context
        └──────────────┬───────────────┘
                       │  request
        ┌──────────────▼───────────────┐
 authn. │  AuthentikateExtension        │   ← verifies token, sets user/client/
        │  → user, client, organization │     organization + provenance on request
        │  → verified ProvenanceToken   │
        └──────────────┬───────────────┘
                       │
        ┌──────────────▼───────────────┐
 koher. │  KoherentExtension            │   ← reads provenance token into a contextvar
        └──────────────┬───────────────┘
                       │  model.save()
        ┌──────────────▼───────────────┐
        │  simple_history signal        │   ← stamps history row with user/client/Task
        │  → ProvenanceEntry rows        │
        └───────────────────────────────┘

On the agent: one token, many mutations

This is the heart of why provenance groups changes. The grouping is not done by Koherent — it falls out of how a rekuest-next agent runs a task.

When a user calls a registered function on an agent:

from rekuest_next import register
from service.api import create_model, update_model  # generated GraphQL clients


@register
def do_some_transactions(name: str) -> Model:
    """Every GraphQL call below is grouped under one task."""
    z = create_model(name=name)             # mutation #1
    f = update_model(id=z.id, name="renamed")  # mutation #2
    return f

the agent receives an Assign message carrying an opaque, server-signed provenance token for that single task. While the function runs, rekuest-next holds that token in an AssignmentHelper stored in a contextvar (current_assignation_helper), and a GraphQL link in its client chain — ContextLink — transparently stamps the token onto every outgoing operation:

So create_model and update_model above — and any other call the function makes — all leave the agent carrying the same Rekuest-Task header. The token is set once per task execution and reused for the lifetime of that execution (Python's contextvars propagate it to every coroutine the function spawns); the application code does nothing special.

On the server side, AuthentikateExtension verifies that Rekuest-Task token and Koherent's get_or_create_task() resolves it to a Task row, deduplicating by the task id (tsk): the first mutation creates the row, every later mutation with the same token reuses it (a warm Task.objects.filter(...).first() lookup, cached in a contextvar per request). The result: every history row produced anywhere in that one @register execution links back to a single Task, and you can later query "show me everything task X changed."

Long-running tasks span many separate HTTP requests over time; because the Task is keyed by the token's task id rather than by request, they all still collapse onto the same row.

Model setup

Add a ProvenanceField to any model you want to audit. Importing the field also registers the history signal that does the attribution.

from django.db import models
from koherent.fields import ProvenanceField


class MyModel(models.Model):
    your_field = models.CharField(max_length=1000, null=True, blank=True)
    provenance = ProvenanceField()  # records & attributes every change

ProvenanceField is a simple_history.HistoricalRecords whose generated history model mixes in koherent.models.ProvenanceEntryModel (the client / task columns). The history rows are reachable via the reverse relation provenance_entries (the default related_name).

Run python manage.py makemigrations && migrate to create the history table and the Task table.

Schema setup

Expose the provenance on your Strawberry types and wire up the extension. Order matters: AuthentikateExtension must run before KoherentExtension.

import strawberry
import strawberry_django
from strawberry_django.optimizer import DjangoOptimizerExtension
from authentikate.strawberry.extension import AuthentikateExtension
from koherent.strawberry.extension import KoherentExtension
from koherent.strawberry import ProvenanceEntry, ProvenanceFilterMixin
from your_app import models


@strawberry_django.filter_type(models.MyModel)
class MyModelFilter(ProvenanceFilterMixin):
    your_field: strawberry_django.filters.FilterLookup[str] | None


@strawberry_django.type(models.MyModel, filters=MyModelFilter)
class MyModel:
    id: strawberry.ID
    your_field: str
    # The reverse relation is `provenance_entries`; expose it as `provenance`.
    provenance: list[ProvenanceEntry] = strawberry_django.field(
        field_name="provenance_entries"
    )


# authentikate's types carry Apollo Federation @key directives, so use a federation schema.
schema = strawberry.federation.Schema(
    query=Query,
    mutation=Mutation,
    extensions=[AuthentikateExtension, KoherentExtension, DjangoOptimizerExtension],
)

Any mutation that calls model.save() now produces an attributed history row automatically — no explicit logging call needed.

Querying provenance

query {
  myModel(id: "1") {
    id
    provenance {
      kind
      date
      user { sub }
      task { taskId agentClientId }
      effectiveChanges { field oldValue newValue }
    }
  }
}

effectiveChanges is batched through a DataLoader, so selecting it across many instances stays a constant number of queries.

Filtering by provenance

ProvenanceFilterMixin adds a flat, exact-match provenance filter to a model's filter type — no nested traversal required:

query {
  myModels(filters: { provenance: { taskId: "task-a", kind: CREATE } }) { id }
}

Available predicates: taskId, agentClientId, issuer, changedBy (user sub), kind, changedSince, and changedBefore.

Interaction with authentikate

authentikate owns identity; Koherent consumes it.

  • ModelsAUTH_USER_MODEL is authentikate.User, and Koherent's Task.assigner / ProvenanceEntry.client point at authentikate's User and Client. Tasks are scoped to an authentikate Organization.
  • The verified provenance tokenAuthentikateExtension validates the incoming Rekuest provenance token (authentikate.provenance.ProvenanceToken, a separate EdDSA trust domain configured under AUTHENTIKATE["provenance"]) and attaches it to the request. There is no static-token bypass for provenance — the signature is always checked.
  • The auth context — Koherent's history signal reads authentikate.vars.get_user(), get_client(), get_organization(), and get_token() to stamp each row and to build the Task from the token's claims (tsk, ptk, rtk, rcb, sub, act.sub, act.cid, iss, jti, ahs, aha). The root human causer (rcb) is resolved to a local User when one exists; otherwise the task is recorded with a null assigner but the raw sub is kept.

Because of this dependency, AuthentikateExtension must precede KoherentExtension in the schema's extension list — Koherent reads what authentikate has already put on the request.

Interaction with kante

kante provides the GraphQL transport (ASGI app) and the request context that both extensions build on.

  • Context typesKoherentExtension switches on kante's HttpContext and WsContext. For HTTP requests it sets the request's provenance token into a context variable for the duration of the operation; for websockets it does not (the connection — and its headers — is shared across operations, so there is no reliable per-operation provenance).
  • Mutations over websockets are rejected. Since the task context can't be tracked per operation on a persistent socket, Koherent raises rather than record an unattributed change.
  • Info — resolvers use kante's Info type; the effective_changes DataLoader is cached on the kante request so it lives exactly one request.

Public API

from koherent.fields import ProvenanceField, HistoricForeignKey, GenericRelation
from koherent.models import Task, ProvenanceEntryModel
from koherent import get_current_provenance, get_current_task
from koherent.strawberry import (
    KoherentExtension,
    ProvenanceEntry,      # the history-row GraphQL type
    Task,                 # the task GraphQL type
    ProvenanceFilter,     # the flat provenance filter
    ProvenanceFilterMixin,  # drop-in mixin adding a `provenance` filter
)

Development

uv sync
uv run pytest        # test_project + testing_module exercise the full stack

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

koherent-4.0.0.tar.gz (16.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

koherent-4.0.0-py3-none-any.whl (20.6 kB view details)

Uploaded Python 3

File details

Details for the file koherent-4.0.0.tar.gz.

File metadata

  • Download URL: koherent-4.0.0.tar.gz
  • Upload date:
  • Size: 16.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.11.24 {"installer":{"name":"uv","version":"0.11.24","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for koherent-4.0.0.tar.gz
Algorithm Hash digest
SHA256 33c823c6b9de2e43795032f95c5e3ef813411f5b74b6d95a0c5c72c2116297c5
MD5 1474bbf40fe246ad82c85453c3ab7a99
BLAKE2b-256 8d989502dc1609f82289e502b992dddcf82e1ecf7ea5afbe9e662f8ea86fbe2f

See more details on using hashes here.

File details

Details for the file koherent-4.0.0-py3-none-any.whl.

File metadata

  • Download URL: koherent-4.0.0-py3-none-any.whl
  • Upload date:
  • Size: 20.6 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.11.24 {"installer":{"name":"uv","version":"0.11.24","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for koherent-4.0.0-py3-none-any.whl
Algorithm Hash digest
SHA256 7afa6676048eaa3e3c7bec13e8aeb654546fddf524ad4e5456f8e0cdc97b6c04
MD5 71ed9c14724d31960da766110007c0f4
BLAKE2b-256 8c117d6bae1066be55099c53c1b0f0cd09b1cb11b3b0280e4ef99c16f4b8f22c

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page