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.

A2A v1.0 capability cards, one per registry capability: capability cards.

Installation

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

The package version is 26.11.10 (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.11.10 (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.11.10/graphlaw.wasm

Expected SHA-256: 62cd95d4ef7294dcd5976a2f685b71580bb834816e9f4bba0d13d5555e35c8f8. 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.