Skip to content

Independent Elixir Consumer

Use published Hex praxo_core 0.8.0 in your own application with Elixir 1.14 or newer. No PRAXO-CORE source checkout, API key, model service, or AxisVela product is required. Installation needs Hex access or a cached package.

This is an extended-language starter. It demonstrates the allow/deny path but is not one of the four pilot languages that carry the governed negative matrix.

Choose the Boundary

This is a fit for an embedded model/tool loop with explicit tool scope and policy. Your application still owns model access, authentic identity, persistent approvals, external effects, and isolation. It is not a hosted agent platform, durable graph engine, or sandbox service. See when not to use Core.

Create the Application

Declare the exact dependency in mix.exs:

defp deps do
  [
    {:praxo_core, "~> 0.9.0"}
  ]
end

Then run mix deps.get. Do not use a path: dependency or a checkout of the language source directory for acceptance.

Application Source

Use the following as starter.exs, rendered from the tested source.

defmodule ConsumerStarter do
  alias Praxo.{Agent, Models}

  def run_scenario(target) do
    counter = :counters.new(1, [:atomics])

    model = fn request ->
      last = List.last(request.messages)

      if last.role == "tool" do
        {:ok, Models.response(Models.message_assistant("recorded"))}
      else
        call = Models.tool_call("record-1", "record", %{"target" => target})
        {:ok, Models.response(Models.message_assistant("", [call]))}
      end
    end

    tool =
      Agent.new_tool("record", "Record a target in an in-memory counter.", fn _arguments ->
        :counters.add(counter, 1, 1)
        "recorded"
      end)

    policy = fn action ->
      case action.kind do
        "model.generate" ->
          Models.decision(true, "model generation")

        "tool.invoke" ->
          Models.decision(
            action.tool_name == "record" and action.arguments["target"] == "public",
            "only the public target is permitted"
          )

        _ ->
          Models.decision(false, "unsupported action")
      end
    end

    options = %{Agent.new_agent_options(model) |
      tools: [tool],
      allowed_tools: ["record"],
      policy: policy}

    case Agent.run_agent(options, Models.session("consumer-#{target}"), "Record the requested target.") do
      {:ok, result, _} -> {result.output, :counters.get(counter, 1), nil}
      {:error, code, _} -> {"", :counters.get(counter, 1), code}
    end
  end

  def run do
    {"recorded", 1, nil} = run_scenario("public")
    IO.puts("allow: recorded; effects=1")

    {"", 0, code} = run_scenario("private")
    if code != "AGENT_POLICY_DENIED" do
      raise "deny verification failed: code=#{inspect(code)}"
    end

    IO.puts("deny: policy rejected; effects=0")
  end
end

Acceptance Tests

Use the following as test_starter.exs, rendered from the tested source.

Code.require_file("starter.exs", __DIR__)

ConsumerStarter.run()
IO.puts("elixir consumer tests passed")

Verify

mix deps.get
mix run test_starter.exs

Expected program output:

allow: recorded; effects=1
deny: policy rejected; effects=0

The model is a deterministic fixture, not an LLM. allowed_tools limits the tool name; Policy also checks the target argument. A denied private target returns AGENT_POLICY_DENIED and must not increment the in-memory counter. Keep these tests when replacing the fixture with a real adapter.

For a source checkout without Hex, compile the beams and run with elixir -pa language/elixir/build/ebin test_starter.exs; that path is not independent consumer evidence.

Scope

This starter establishes only the stated Hex package and fixture behavior. It does not add the governed negative matrix, prove authentic identity, provide durable approvals, reconcile effects, or establish isolation. Continue with the Elixir implementation guide and the security model.