decern decern

Authorization kernel · Pure Rust


decern

Decide what an agent, a person or a service is allowed to do. Keep proof of every answer.

Three things set it apart. A solver checks the safety rules against every possible input, instead of the handful a test file covers. Every answer goes into a signed, linked log, so anyone can check later that nothing was changed or removed — including people who don't trust whoever ran the server. And nothing gets decided for an unverified caller: the server refuses to start until it can name who is asking.

Contribute on GitHub Where to start Install and try it

The problem


Everyone agrees how to write permissions down. Nobody guarantees they hold.

Tokens and delegation formats settle how permission is described. Whether it holds — that passed-on authority can only get narrower, that nothing vanished from the log, that an answer stayed inside its mandate — is left to whoever builds the system.

decern fills that gap. People, AI agents and services are one kind of principal here, answered by the same proven function.

What makes it different


Prove the rules. Record the answer. Know who asked.

01 · Proven, not tested

A solver checks every case, not a few

An answer depends on four things only: who is asking, what authority exists, the policy, and the time. Because it is that contained, cvc5 can search every possible input for one that breaks a rule. Nine rules are checked this way. Green means it found nothing.


decern-proof · runs in decern prove and CI only, never on the request path

02 · Recorded, not trusted

A log that shows if it was touched

Each answer is added to the end of the log, signed, and linked to the one before it. The signature shows a record is genuine; the links show none went missing. A log torn by a crash repairs itself; history cut short is caught. You check the file yourself, offline.


decern-ledger · Merkle · JCS canonicalisation · anchors

03 · Known, not assumed

Refuses to answer an unverified caller

Before evaluating anything, decern-serve establishes who is asking — a valid bearer token, or a declared, authenticated front taking responsibility for what it asserts. There is no default identity, and no way to start the server without naming one. The verified caller is written onto the decision record itself, not inferred afterwards.


decern-server · RFC 9068 bearer validation · asserted_by on every record

Open call


The core stays small. Everything around it is open work.

A proven core is only useful once real systems can reach it — clients, gateways, log backends, policy packs. Those sit outside the kernel on purpose, and they are open.

People and agents are both welcome, on the same terms. Every change goes through one gate: ./scripts/verify.sh.

Area 01

A new client SDK

Mirror the Go, Python or TypeScript client against the AuthZEN surface in decern-server. Any language.

Area 02

Harden the enforcement adapter

The HTTP shim shipped in examples/ext_authz_adapter — extend it, or bring the gRPC ext_authz variant — a thin client over the server.

Area 03

Authority-graph tooling

Authority-graph export — blast-radius shipped; DOT and Mermaid renderings remain over the kernel's directory.

Area 04

A ledger backend

Implement the head-store trait in decern-store for your substrate.

Area 05

A new proven property

Add the invariant to decern-proof — and the negative control that proves it load-bearing.

Area 06

Model and policy packs

Cedar policy, schema and entities for a domain, contributed to crates/decern-kernel/model/.

Getting a change in

Open an issue first for anything big or design-changing — a short conversation saves a wasted PR. For a small, obvious fix, just send the PR. Agent-written PRs are welcome on the same terms; agents start at AGENTS.md, with the working method and standards in .agent/.

The one gate

build · test · cvc5 proofs
clippy · fmt · cargo-deny
standards guard


Run it before you open a PR and review holds no surprises.

Rules


Nine rules, checked against every possible input.

Each line says exactly what the solver checked — never more.

01 money-gate No privileged money action without explicit approval.
02 isolation No decision ever crosses a tenant boundary.
03 decay No decision once authority has expired.
04 attenuation-edge No access without ownership, a delegation ancestor, or an explicit grant.
05 scope-gate Bounded actions require their named scope.
06 revocation-gate A revoked principal is allowed nothing.
07 residency-gate Residency-labelled resources need matching clearance.
08 role-gate Role-required resources need the role.
09 consent-gate Non-owned PII needs explicit consent.

Negative controls

Every rule also ships with its opposite: a test that takes the guarantee out of the policy and insists cvc5 does find a counterexample. If that test ever passed quietly, the proof was proving nothing.

What it does not prove

cvc5 proves the kernel's rules. It cannot tell whether your policy says what you meant, and it is never a claim that a deployment is secure — the deciding endpoints validate RFC 9068 bearer tokens when configured, or run behind a declared authenticating front; the server refuses to start with neither stated. Where a rule leans on a derived value, that part is ordinary tested Rust and is written up as such. The full boundary lives in the repository, under known limitations ↗.

One request, start to finish


Decide. Record. Or refuse.

The answer is handed back only after its record is safely written. If it cannot be recorded you get 503 — never a bare allow.

01

Ask

A request arrives at decern-serve, and the caller is established first — a verified token, or the declared front.

02

Evaluate

The proven kernel answers, using the authority graph. The clock comes from the server, never the request.

03

Record

The answer, plus who is accountable for it, is appended and signed.

04

Serve or 503

Sent back only if that record was written. Otherwise refused.

05

Verify

Anyone can re-check the links and signatures later, offline.

decern decision flow: the caller is established, the proven kernel evaluates, the decision is recorded to the tamper-evident ledger, and it is served with 200 only when the record was written, else 503; an unestablished caller is refused before evaluation

Who is answerable


A record names an accountable owner: the top of the asker's delegation chain, worked out by the server from its own directory — never taken from the request. A stand-in's record names whoever is finally answerable for it; a top-level principal vouches for itself; a caller the directory does not know gets none.

When the caller was established from a verified token, the record also names who asserted the request — the subject, client and issuer the token claimed, exactly as verified. That is a separate field from the accountable owner above: one says who is answerable, the other says who spoke.

Both are there for accountability, not as a gate. Neither ever changes the answer.

The ledger


Change one byte and the log says so.

Alter record 02 below to see what decern verify reports.

/tmp/decern.jsonl
Seq Decision Recorded Prev Chain
0001 allow corp · Read · claim1 genesis intact
0002 deny agent-7 · MoveMoney · account9 9f3c…a1 intact
0002 allow agent-7 · MoveMoney · account9 9f3c…a1 mismatch
0003 allow corp · Read · claim2 41ba…7d intact orphaned

$ decern verify --ledger /tmp/decern.jsonl
chain intact · 3 records · 3/3 signatures valid · exit 0

$ decern verify --ledger /tmp/decern.jsonl
chain broken at seq 0002 · hash mismatch · later records orphaned · exit 1

Illustration rendered in the browser — hashes abbreviated, no signing key involved. examples/quickstart.sh runs the real loop: prove, serve, decide, verify, and tamper-is-rejected.

An actual record seq 0 · Ed25519-signed · hash-chained · from a live run
{
  "entry": {
    "seq": 0, "ts_ms": 1786245929000,
    "subject_type": "Principal", "subject_id": "corp",
    "action": "Read",
    "resource_type": "Resource", "resource_id": "claim1",
    "context": { "now": 1786245929 },
    "decision": true, "reasons": ["P-read"],
    "digests": {"authority": "cb03c58cb1f6…", "parameters": "f2eaa74d032e…"},
    "sponsor": { "kind": "Principal", "id": "corp" }
  },
  "prev":    "0000000000000000000000000000000000000000000000000000000000000000",
  "hash":    "78d312d1b88a75d8441e19920408e3a8438ec2d547e7b5496636ba37a4bed8a6",
  "sig_b64": "oDpSX6ucuBT6kxM4jo1poXzoKfVEobU4dlW3YXZHvBfdNL1JmZh1F0GEvA8//lmAOA1xXvybmqxl/Vaw9itlAw==",
  "kid":     "46a941e7d9536df4922254a6a3cf983bd90ea3d2264c44390257adca00468fff"
}

$ decern verify --ledger /tmp/decern.jsonl --pubkey 46a941e7…
OK · 2 entries · root 7bd41405…5205 · signatures verified

Subject-side


The subject named in a record does not have to take the operator's word for what was decided about them. GET /audit/v1/subject projects every record naming a given subject straight from the ledger; GET /.well-known/decern-subject-side-disclosure states, in one document, what a subject can ask for and how. Neither route changes an answer — they make the record legible to the one it was about.

Missions


These tools, until this time — and never wider than the approver.

Someone with authority hands an agent a narrow slice of it: named tools, a deadline. Ask for a tool the approver does not hold, or a deadline past their own, and the Mission is refused — with nothing recorded.

Every accepted step is written to the same log before it is reported as done, and a Mission that has ended never comes back. Its reference is derived from the approval itself, so re-approving an identical dead grant is refused — a new one has to differ in at least one field.

Registry is durable and local — --missions <PATH>, default decern-missions.json beside the ledger. Consulted in-perimeter, no phone-home.

Mission lifecycle

POST /mission/v1/approve
{approver, agent, description, approved_tools, capabilities?, expiry}
→ {approver, s256, reference}
GET /mission/v1/{s256}
→ {reference, state: active | terminated, expiry}
POST /mission/v1/{s256}/terminate
→ {reference, state: terminated}

What it is made of


Two binaries over seven small libraries.

The default build pulls no TLS stack, OpenSSL or cmake — though cedar's stacker/psm compiles one small assembly routine via cc. The Postgres head store adds a TLS provider, so it sits behind --features postgres, and is off by default.

decern architecture: the decern and decern-serve binaries over the kernel, identity, proof, ledger, store, and crypto crates
Crate topology, as shipped in the repository.
decern-kernel lib The deterministic decision function — Cedar authority graph, the directory, the check itself. The security core.
decern-proof lib The SMT harness — compiles the Cedar model symbolically and discharges the nine invariants with cvc5, each with a negative control.
decern-ledger lib The tamper-evident record — append-only, Ed25519-signed, hash-chained; Merkle, JCS canonicalisation, anchors, single-file and sharded.
decern-store lib Persistence traits and reference implementations — the single-host file head store and the durable Mission registry.
decern-store-postgres lib · opt Multi-host head store over Postgres advisory locks — the same trait, behind a feature flag.
decern-identity lib The Mission core — approval-backed, fail-closed-attenuated authority.
decern-crypto lib Ed25519 and SHA-256 primitives shared by the ledger and identity.
decern-cli → decern bin prove · decide · verify.
decern-server → decern-serve bin The fail-closed PDP — evaluate, record, serve — and the Mission lifecycle over decern-identity.

HTTP surface

POST /access/v1/evaluation
POST /decide (alias)
POST /mission/v1/approve · GET · terminate
GET  /anchor/v1/tree-head
GET  /audit/v1/subject
GET  /directory/v1/…/descendants
GET  /.well-known/decern-subject-side-disclosure
GET  /pubkey · /healthz

Request {subject, action:{name}, resource, context}; response {decision}, with reasons or errors under context.

Clients

Go, Python and TypeScript SDKs ship in the repository, written against the AuthZEN surface. New SDKs and enforcement adapters are the easiest way in: mirror an existing client, or put a thin gateway in front of the server.


crates/decern-kernel/model/ holds the Cedar policy, schema and entities the kernel loads.

Start


Prove it, run it, verify it.

Two binaries: decern to prove and verify, decern-serve to answer requests. You only need cvc5 on PATH for the proofs — not to serve answers.

Prebuilt binaries

Prebuilt for Linux (x64/arm64), macOS (Apple Silicon) and Windows (x64) — grab the latest archive on GitHub, unpack, and put both binaries on your PATH.

Get it on GitHub ↗

From source

cargo install decern-cli decern-server

Or clone and build with the pinned toolchain from rust-toolchain.toml.

# 1 · prove all invariants hold over every input (cvc5)
decern prove
# 2 · run the PDP (writes a tamper-evident ledger)
decern-serve --ledger /tmp/decern.jsonl --trust-proxy &
# 3 · decide over HTTP (AuthZEN-shaped) — corp reads a claim it owns
curl -s localhost:8080/access/v1/evaluation -H 'content-type: application/json' -d '{
  "subject":  {"type":"Principal","id":"corp"},
  "action":   {"name":"Read"},
  "resource": {"type":"Resource","id":"claim1"}
}'
# 4 · verify the ledger (hash chain + every signature)
decern verify --ledger /tmp/decern.jsonl \
  --pubkey "$(curl -s localhost:8080/pubkey | jq -r .kid)"

Sovereign · one process

--ledger <PATH>

One ledger file. Binds loopback by default; starting requires the caller posture to be named — bearer validation, or a declared front.

Hosted · one host

--sharded <DIR>

Several server processes share one ledger — one hash chain per tenant — coordinated by a file lock. Unix only.

Hosted · many hosts

--sharded postgres://…

Advisory-lock head store for replicas across hosts. Build with --features postgres; the URL is never logged.

--ledger and --sharded are mutually exclusive. Audit a sharded deployment with decern verify --sharded <dir> --pubkey <kid> — it checks every shard and exits non-zero if any fails.

Integrations & clients

The MCP server (examples/mcp) has been run end to end against a real MCP client, not curl, with allow, deny and step-up all read back correctly by the model. Alongside it, a forward-auth shim for NGINX, Traefik and Envoy (examples/ext_authz_adapter) puts the same decision point in front of a gateway. Call a running decern-serve directly from an application with a thin AuthZEN client — uv add decern (Python), npm install decern (TypeScript) or go get (Go).

Who runs this


Maintained in the open, with the roadmap in the open too.

What is next

Record who asserted a mission transition, not only a decision. Derive approval at the decision point by default, not from the request. Publish a signed revocation feed a gateway can hold open. Fetch and store anchors from the CLI, for single and sharded logs alike. OAuth, vendor credentials and approval UX stay outside the decision plane.

ROADMAP.md

Maintained by

AA

Anivar A. Aravind

anivar.net ↗

Review and merge rules are written down in GOVERNANCE.md; disclosure in SECURITY.md.