No project description provided
Project description
Koherent
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 (assignation) 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:
- Recording — a
ProvenanceFieldon your model captures every create/update/delete as a history row (via django-simple-history). - Attributing — a signal stamps each history row with the acting
user, theclient(app), and aTaskresolved from the request's verified provenance token. - 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 assignation, 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 assignation 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 assignation from the provenance token's claims
(assignation 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 an assignation?
An assignation 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 assignation 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 assignation."""
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 assignation. 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 assignation 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 assignation X changed."
Long-running assignations span many separate HTTP requests over time; because the
Taskis keyed by the token's assignation 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 { assignationId 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: { assignationId: "task-a", kind: CREATE } }) { id }
}
Available predicates: assignationId, agentClientId, issuer, changedBy (user sub),
kind, changedSince, and changedBefore.
Interaction with authentikate
authentikate owns identity; Koherent consumes it.
- Models —
AUTH_USER_MODELisauthentikate.User, and Koherent'sTask.assigner/ProvenanceEntry.clientpoint at authentikate'sUserandClient. Tasks are scoped to an authentikateOrganization. - The verified provenance token —
AuthentikateExtensionvalidates the incoming Rekuest provenance token (authentikate.provenance.ProvenanceToken, a separate EdDSA trust domain configured underAUTHENTIKATE["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(), andget_token()to stamp each row and to build theTaskfrom 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 localUserwhen 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 types —
KoherentExtensionswitches on kante'sHttpContextandWsContext. 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'sInfotype; theeffective_changesDataLoader 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 assignation 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
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file koherent-3.0.2.tar.gz.
File metadata
- Download URL: koherent-3.0.2.tar.gz
- Upload date:
- Size: 15.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: uv/0.11.22 {"installer":{"name":"uv","version":"0.11.22","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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
3c628e218a0203e98296bb7b3766e4504dc94594dde200a1f574fe301e4cf8af
|
|
| MD5 |
4b7aabbfb122982f300496f5f6eed6c8
|
|
| BLAKE2b-256 |
d32467ceb115ed76ee0ae20e41617b0cd831107cf17ed1283bd5a937a8f8a97c
|
File details
Details for the file koherent-3.0.2-py3-none-any.whl.
File metadata
- Download URL: koherent-3.0.2-py3-none-any.whl
- Upload date:
- Size: 19.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: uv/0.11.22 {"installer":{"name":"uv","version":"0.11.22","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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
1d7127b076250c10d532f008c8f97c350a122e058cd926d2ce5d37fd8881f5f0
|
|
| MD5 |
9244cc18c27d6b1dfca7516c7b397db9
|
|
| BLAKE2b-256 |
fd945f18fe5b4f6ffbf806869104423682bbe4bbabf20ba64d72c14b8eb83c57
|