ash_graphlaw

Hex.pm CI Docs License: MIT REUSE compliant

ash_graphlaw is the Ash/BEAM membrane for GraphLaw. It hosts the pinned graphlaw.wasm WASI module through Wasmex, preserves GraphLaw's JSON ABI, and exposes admissions to Ash resources as a Spark extension with a Change, a Validation and a Preparation. Semantic standards (RDF, SHACL, N3, RDFS, OWL-RL, hooks, plan admission, receipts) stay inside GraphLaw; they are not reimplemented in Elixir.

Ash / Reactor / SA2A
|
v
ash_graphlaw Change / Validation / Preparation
|
v
Pool / Host (Wasmex + WASI)
|
v
graphlaw.wasm
|
v
PurRDF + Eyeron + GraphLaw

Core invariant

A caller proposes a request. GraphLaw derives and validates: it decides whether the projected semantic state and the requested transition are admitted. ash_graphlaw projects the outcome as a typed success (AshGraphLaw.Admitted, wrapped in AshGraphLaw.Evidence) or a typed refusal (AshGraphLaw.Refusal). Nothing in this library grants authority; admission evidence is an observation bound to an exact input digest, and AshGraphLaw.Standing never reports :ALIVE for an admission alone.

request --> [ Change / Validation / Preparation ] --> Pool --> graphlaw.wasm
|
{:ok, %Admitted{}} <-- admitted, digest-bound evidence ---+
{:error, %Refusal{}} <-- closed code + class + broken_term-+

Typed capabilities

Every public GraphLaw operation has a typed, generated Elixir entry point, so no supported operation needs AshGraphLaw.call/2. The surface is generated from the GraphLaw capability registry (schema graphlaw.capability-registry/1, vendored in priv/graphlaw/) through the graphlaw-ash-capability-pack; AshGraphLaw.Capability.Registry.names/0 lists the ops in ABI order (14 at ABI 1), and every op has a request builder, a decoder and a lossless AshGraphLaw.Result.* struct.

{:ok, %AshGraphLaw.Result.Shacl{conforms: true}} =
AshGraphLaw.shacl(data: turtle_text, shapes: shapes_text)
{:error, %AshGraphLaw.Refusal{code: code}} = AshGraphLaw.parse(text: "not rdf at all")
AshGraphLaw.Capability.API.sparql!(data: turtle_text, query: "SELECT * WHERE { ?s ?p ?o }")

Per-op pages: capabilities reference.

Installation

def deps do
[
{:ash_graphlaw, "~> 26.10.1"}
]
end

The package version is 26.10.1 (calendar versioning). The graphlaw.wasm binary is not shipped in the Hex package; fetch the pinned release asset with the vendor task (see Vendoring the engine). With Igniter available, mix igniter.install ash_graphlaw runs mix ash_graphlaw.install.

Configuration

# config/config.exs
config :ash_graphlaw,
start_pool: true

With start_pool: true the application supervises AshGraphLaw.Pool. The wasm path resolves in this order: the :wasm_path option, the GRAPHLAW_WASM_PATH environment variable, the :wasm_path application environment, then priv/graphlaw/graphlaw.wasm. The digest pin applies to the vendored path and to environment or config supplied paths.

DSL

defmodule MyApp.Ticket do
use Ash.Resource,
domain: MyApp.Domain,
extensions: [AshGraphLaw.Resource]
graphlaw do
runtime do
timeout_ms 5_000
max_skew_secs 60
trusted_keys ["<64 hex characters: the public key of a lease signer you trust>"]
end
admission :ticket_close do
step :plan
ceiling :select
law MyApp.PlanLaw
end
end
actions do
update :close do
change {AshGraphLaw.Change.Admit, admission: :ticket_close}
end
end
end

MyApp.PlanLaw implements the AshGraphLaw.Law behaviour and returns the GraphLaw law steps for the subject. The admission runs before the action, and a refusal becomes an Ash error.

Authority: signed leases

The ceiling is met only by a signed lease in the changeset context; a bare :select atom grants only :observe. The engine verifies the signature against trusted_keys and judges expiry on its own clock, and the resulting AshGraphLaw.Evidence records the lease identity and the engine digest.

signed = MyApp.Leases.mint_signed_lease(subject: "ticket-42", ceiling: :select)
MyApp.Ticket
|> Ash.Changeset.for_update(ticket, :close, %{},
context: %{graphlaw_lease: %{signed_lease: signed}}
)
|> Ash.update()

MyApp.Leases.mint_signed_lease/1 stands for your own signer; ash_graphlaw verifies leases and never mints authority. See authority boundary.

Handling refusals

case MyApp.Ticket |> Ash.Changeset.for_update(ticket, :close) |> Ash.update() do
{:ok, closed} ->
closed
{:error, error} ->
case Enum.find(Ash.Error.to_error_class(error).errors, &match?(%AshGraphLaw.Error.Refused{}, &1)) do
%AshGraphLaw.Error.Refused{refusal: refusal} ->
{refusal.code, refusal.class, refusal.broken_term}
nil ->
{:error, error}
end
end

Direct calls return {:ok, %AshGraphLaw.Admitted{}} or {:error, %AshGraphLaw.Refusal{}} and AshGraphLaw.Standing.of/1 derives the standing from the result. The closed refusal vocabulary is in typed refusals.

Introspect

AshGraphLaw.Info reads the declared graphlaw section of any resource without running the engine:

AshGraphLaw.Info.admissions(MyApp.Ticket)
AshGraphLaw.Info.admission(MyApp.Ticket, :ticket_close)

See the DSL cheat sheet for every option.

Vendoring the engine

The engine binary is pinned by release tag v26.9.29 (asset graphlaw.wasm, ABI

  1. and by SHA-256. Fetch and check it:
mix ash_graphlaw.vendor # downloads graphlaw.wasm into priv/graphlaw/, checks the SHA-256
mix ash_graphlaw.verify # re-checks the digest and the WASI import allowlist

Source: https://github.com/seanchatmangpt/graphlaw/releases/download/v26.9.29/graphlaw.wasm

Expected SHA-256: 7bb2a7e5ebcef7584b0b960451272d56fa75414d76a12138d41e8973e126eee0. A mismatch is refused; the binary is never used unverified. See vendor the wasm.

Verification ladder

Cheapest, highest-information check first:

mix format --check-formatted
mix compile --warnings-as-errors
mix credo --strict
mix test # wasm-free tests
mix test --include wasm --include slow # with the vendored engine
mix ash_graphlaw.verify
mix ash_graphlaw.parity # capability parity court
mix ash_graphlaw.mutate --require-killed # mutation catalog
mix docs
mix hex.build

A green ladder is an observation on one exact commit. It is not standing: :ALIVE requires an exact-SHA receipt. See claims and evidence.

Documentation map

Documentation follows the Diataxis layout under documentation/ (the hub). Hosted docs: https://hexdocs.pm/ash_graphlaw.

Kind Start here
Tutorial getting started, first admitted action
How-to vendor the wasm, declare an admission, write a law module, run the pool, handle refusals
Reference DSL, ABI, claims and evidence, support matrix, typed refusals, typed capabilities, DSL cheat sheet
Explanation architecture, authority boundary, generation and residue, wasm host design

Agent-facing guidance is in usage-rules.md and AGENTS.md.

Citing

See CITATION.cff for the citation metadata of this release.

Reproduce

REPRODUCE.md lists the exact commands and pins to rebuild and re-verify this release from a clean checkout.

Security

Report vulnerabilities as described in SECURITY.md. The engine is pinned by SHA-256 and the WASI import surface is allowlisted; the host never widens either.

Non-goals

Manufactured provenance

Regeneration

This file is a projection. Edit ontology.ttl, queries/project.rq or templates/README.md.tmpl, then run scripts/ggen_sync.sh (scripts/vendor_marketplace.sh first when vendor/ is absent). Do not edit README.md by hand.

License

MIT. See LICENSES/MIT.md.