From 2f5aa7de0c82046dce70ae215ad531a2799f5039 Mon Sep 17 00:00:00 2001 From: Ayla Croft Date: Sun, 27 Sep 2026 17:53:31 -0400 Subject: [PATCH 1/5] feat: a regulated profile that refuses to boot on a configuration it cannot support Five refusals, gated on TRINITY_PROFILE=regulated, or :trinity, :profile set to :regulated. Under the default profile nothing here does anything: every check takes the profile as its first argument and answers :ok for :default without looking at the value. AC1 the MCP authorization profile must be :production AC2 the personal issuer must not be the one in force AC3 every model must name an endpoint on TRINITY_REGULATED_LLM_ENDPOINTS AC4 receipts that cannot append at boot stop the node AC5 the authority in force must not be Trinity.Authority.Local AC5 supplies no adapter and invents none. If TRINITY_AUTHORITY is unset or resolves to Local, the boot is refused and the operator has to supply one. The refusal is the point. AC3 refuses a model with no base_url rather than letting it through. Such a model goes to its provider's default endpoint, which the configuration does not state, and an endpoint that is not stated cannot be shown to be on the list. Trinity.Profile is pure. Nothing in it starts, stops or touches a process, and it reads the environment in two named functions only. The caller reads the world and hands the values in, so every refusal is unit testable and the Trinity boundary grows no dependency on Authority or Receipts to ask a question about a value. Two things deliberately not changed, because the owner's rules forbade it and the rules were right: - lib/trinity/receipts/supervisor.ex keeps its fail-open shape for every profile that is not :regulated. A missing signer still logs, sets the alarm, and lets the tree start, because every effect is denied until a signer returns. AC4 is a separate call that :default never makes. - lib/trinity/mcp/boot.ex keeps its rescue. AC2 is decided before any child starts, which is before MCP.Boot runs, rather than by narrowing a rescue so a warning could become a refusal. Neither file appears in this diff. One export added: Trinity exported Receipts.KeyCustody already but not Authority.Selection, and the application needs the selection RULE before the selection PROCESS exists. Adding both boundaries to the application's deps was the first attempt and the compiler refused it, correctly. The mistake worth recording: the first version of AC4's tests called KeyCustody.boot!/1 from an async file. That function writes its answer to persistent_term on both paths including the failure, so inducing a failure poisoned the signer for the whole run and 48 unrelated tests failed with {:signer_unavailable, {:key_file, :enotdir}}. Moved to its own async: false file, which ExUnit runs only after every async module has finished, restoring the selection in on_exit. Signed-off-by: Ayla Croft --- lib/trinity.ex | 8 + lib/trinity/application.ex | 74 +++++++- lib/trinity/profile.ex | 196 ++++++++++++++++++++++ test/trinity/profile_signer_down_test.exs | 59 +++++++ test/trinity/profile_test.exs | 174 +++++++++++++++++++ 5 files changed, 510 insertions(+), 1 deletion(-) create mode 100644 lib/trinity/profile.ex create mode 100644 test/trinity/profile_signer_down_test.exs create mode 100644 test/trinity/profile_test.exs diff --git a/lib/trinity.ex b/lib/trinity.ex index b713f5d..ff1fa58 100644 --- a/lib/trinity.ex +++ b/lib/trinity.ex @@ -35,6 +35,10 @@ defmodule Trinity do Telemetry.Redaction, Repo, Repo.Receipts, + # Slice regulated-boot: the application asks it what the profile refuses, before it + # starts anything. Pure, so the refusals are unit testable and so this boundary does not + # grow a dependency on Authority or Receipts to ask a question about a value. + Profile, UUID, Config, Sessions, @@ -110,6 +114,10 @@ defmodule Trinity do Receipts.Verifier, Receipts.KeyCustody, Authority, + # Slice regulated-boot: `Selection.select/1` is the pure rule the selection child uses. + # The application asks it before that child exists, so that `:regulated` can refuse a + # local authority having started nothing. Exported rather than duplicated. + Authority.Selection, Content.Part ] ++ if(Mix.env() == :test, do: [DataCase, NetworkGuard, Factory], else: []) diff --git a/lib/trinity/application.ex b/lib/trinity/application.ex index f5de26d..1a81afa 100644 --- a/lib/trinity/application.ex +++ b/lib/trinity/application.ex @@ -29,6 +29,11 @@ defmodule Trinity.Application do # project's rules about key material, and the filter is the last line for exactly that case. install_log_redaction() + # Slice regulated-boot. Before any child, because a refused boot should have started nothing: + # the same reasoning the DataDir lock is placed early for. Under `:default` this returns :ok + # without reading anything. + verify_regulated_configuration!() + children = desktop_children() ++ [ @@ -94,7 +99,74 @@ defmodule Trinity.Application do # See https://elixir.hexdocs.pm/Supervisor.html # for other strategies and supported options opts = [strategy: :one_for_one, name: Trinity.Supervisor] - Supervisor.start_link(children, opts) + + case Supervisor.start_link(children, opts) do + {:ok, pid} -> verify_regulated_receipts(pid) + other -> other + end + end + + ## Slice regulated-boot: the refusals `:regulated` adds, and nothing else changed by it + ## + ## Every decision below is `Trinity.Profile`'s, which is pure and unit tested. This module reads + ## the world and hands it the values. Under `:default` each one answers :ok without looking. + + # AC1, AC2, AC3 and AC5: configuration facts, knowable before a child exists. Raising here + # stops the boot with the reason, which is what `verify_log_redaction!/0` below already does and + # what the DataDir lock does by refusing to start. + defp verify_regulated_configuration! do + profile = Trinity.Profile.current() + + if profile == :regulated do + mcp_auth = Application.get_env(:trinity, :mcp_auth, []) |> Keyword.get(:profile, :local) + llm = Application.get_env(:trinity, :llm, []) + authority = authority_module() + + [ + Trinity.Profile.check_mcp_auth(profile, mcp_auth), + Trinity.Profile.check_embedded_as(profile, mcp_auth), + Trinity.Profile.check_authority(profile, authority), + Trinity.Profile.check_llm_endpoints( + profile, + Trinity.Profile.raw_endpoints(), + Keyword.get(llm, :models, []) + ) + ] + |> Enum.each(fn + :ok -> :ok + {:error, reason} -> raise "TRINITY_PROFILE=regulated refuses to boot: #{inspect(reason)}" + end) + end + + :ok + end + + # AC5's input. `Trinity.Authority.Selection.select/1` is the same pure rule the selection child + # uses, asked here before that child exists rather than duplicated. + defp authority_module do + case Trinity.Authority.Selection.select(System.get_env("TRINITY_AUTHORITY")) do + {:ok, module} -> module + _ -> nil + end + end + + # AC4. Only reached under `:regulated`: `:default` never calls it, so + # `Trinity.Receipts.Supervisor` keeps its fail-open shape untouched for every other profile. + # A signer that is unavailable there logs, sets the alarm and lets the tree start; here it stops + # the node, and the tree that did start is brought down rather than left running refused. + defp verify_regulated_receipts(pid) do + if Trinity.Profile.current() == :regulated do + case Trinity.Profile.check_receipts(:regulated, Trinity.Receipts.KeyCustody.boot!()) do + :ok -> + {:ok, pid} + + {:error, reason} -> + Supervisor.stop(pid, :shutdown) + {:error, {:regulated_boot_refused, reason}} + end + else + {:ok, pid} + end end # Tell Phoenix to update the endpoint configuration diff --git a/lib/trinity/profile.ex b/lib/trinity/profile.ex new file mode 100644 index 0000000..0801161 --- /dev/null +++ b/lib/trinity/profile.ex @@ -0,0 +1,196 @@ +# SPDX-FileCopyrightText: Sudo Apt Holdings LLC +# SPDX-License-Identifier: Apache-2.0 +defmodule Trinity.Profile do + @moduledoc """ + The deployment profile, and the refusals `:regulated` adds to boot. + + Two profiles. `:default` is everything this tree did before and behaves exactly as it did. + `:regulated` refuses to start in a configuration that cannot support a regulated deployment, + on the reasoning that an operator who has set the wrong thing should find out at boot and not + from an auditor. + + ## Every function here is pure, and that is deliberate + + Nothing in this module reads the environment except `current/0` and `raw_endpoints/0`, and + nothing starts, stops or touches a process. The caller reads the world and hands the values in. + Two reasons: a refusal that cannot be unit tested is a refusal nobody has checked, and this + module sits in the `Trinity` boundary, which does not depend on `Trinity.Authority` or + `Trinity.Receipts` and should not grow a dependency on them to ask a question about a value. + + ## `:default` is never changed by anything here + + Every check takes the profile as its first argument and answers `:ok` for `:default` without + looking at anything else. `Trinity.Receipts.Supervisor` keeps its fail-open shape for every + profile that is not `:regulated`: a missing signer logs, sets `Trinity.Receipts.Alarm`, and lets + the tree start, because every effect is denied until a signer returns. That is right for a + personal machine and wrong for a regulated one, and the difference is expressed here rather than + by changing the supervisor. + """ + + @type t :: :default | :regulated + + @env "TRINITY_PROFILE" + @endpoints_env "TRINITY_REGULATED_LLM_ENDPOINTS" + + @doc """ + The profile in force: `#{@env}`, else `config :trinity, :profile`, else `:default`. + + An unrecognised value raises rather than falling back. A typo in a profile name must not + silently produce the permissive profile, which is the one failure mode this whole module exists + to prevent. + """ + @spec current() :: t() + def current do + case System.get_env(@env) do + nil -> Application.get_env(:trinity, :profile, :default) + "" -> Application.get_env(:trinity, :profile, :default) + "regulated" -> :regulated + "default" -> :default + other -> raise ArgumentError, "#{@env} is not default or regulated: #{inspect(other)}" + end + end + + @doc "True when the profile in force is `:regulated`." + @spec regulated?() :: boolean() + def regulated?, do: current() == :regulated + + @doc "The raw value of `#{@endpoints_env}`, unparsed." + @spec raw_endpoints() :: String.t() | nil + def raw_endpoints, do: System.get_env(@endpoints_env) + + @doc "The name of the endpoint allow-list variable, so an error message and a test agree on it." + @spec endpoints_env() :: String.t() + def endpoints_env, do: @endpoints_env + + @doc """ + AC1. Under `:regulated` the MCP authorization profile must be `:production`. + + `:local` trusts a bearer token in a file on the host and `:personal` runs an authorization + server on the owner's machine. Neither is an identity provider a regulated boundary can point + at, so neither may be the one in force. + """ + @spec check_mcp_auth(t(), atom()) :: :ok | {:error, term()} + def check_mcp_auth(:default, _mcp_auth_profile), do: :ok + def check_mcp_auth(:regulated, :production), do: :ok + + def check_mcp_auth(:regulated, got), + do: {:error, {:regulated_requires_production_mcp_auth, got}} + + @doc """ + AC2. Under `:regulated` the embedded, personal authorization server must not be the one in force. + + This is decided here, before `Trinity.MCP.Boot` runs, because that module ends in a rescue that + turns any failure into a warning log. A warning is not a refusal, and the rescue is not narrowed + to make this work. + """ + @spec check_embedded_as(t(), atom()) :: :ok | {:error, term()} + def check_embedded_as(:default, _mcp_auth_profile), do: :ok + def check_embedded_as(:regulated, :personal), do: {:error, :regulated_refuses_personal_issuer} + def check_embedded_as(:regulated, _other), do: :ok + + @doc """ + AC5. Under `:regulated` the authority in force must not be `Trinity.Authority.Local`. + + **No external adapter is supplied by this tree and none is invented here.** If `TRINITY_AUTHORITY` + is unset, or names something that resolves to `Local`, the boot is refused. The refusal is the + point: a regulated boundary where the machine is judge, actor and scribe is the configuration + this profile exists to stop, and the operator has to supply an adapter for it to start. + """ + @spec check_authority(t(), module()) :: :ok | {:error, term()} + def check_authority(:default, _module), do: :ok + + def check_authority(:regulated, Trinity.Authority.Local), + do: {:error, :regulated_refuses_local_authority} + + def check_authority(:regulated, module) when is_atom(module) and not is_nil(module), do: :ok + + @doc """ + AC4. Under `:regulated`, receipts that cannot be appended at boot stop the node. + + `boot_result` is what `Trinity.Receipts.KeyCustody.boot!/1` answered. Under `:default` this + returns `:ok` whatever it was, which is the existing behaviour and is not changed: the supervisor + logs, sets the alarm, and the tree starts with every effect denied. + """ + @spec check_receipts(t(), {:ok, term()} | {:error, term()}) :: :ok | {:error, term()} + def check_receipts(:default, _boot_result), do: :ok + def check_receipts(:regulated, {:ok, _selection}), do: :ok + + def check_receipts(:regulated, {:error, reason}), + do: {:error, {:regulated_requires_receipts, reason}} + + @doc """ + AC3. Under `:regulated` every configured model must name an endpoint on the allow-list. + + `raw` is `#{@endpoints_env}`, a comma separated list of scheme and host, for example + `https://models.internal,https://gateway.lab.example`. `models` is `config :trinity, :llm`'s + `:models` list. + + **A model with no `base_url` is refused**, and that is not an oversight. Such a model goes to its + provider's default endpoint, which this configuration does not state, and an endpoint that is not + stated cannot be shown to be on the list. Under `:regulated` "we cannot tell where this goes" is + a refusal. + """ + @spec check_llm_endpoints(t(), String.t() | nil, [map()]) :: :ok | {:error, term()} + def check_llm_endpoints(:default, _raw, _models), do: :ok + + def check_llm_endpoints(:regulated, raw, _models) when raw in [nil, ""], + do: {:error, {:regulated_llm_endpoints_unset, @endpoints_env}} + + def check_llm_endpoints(:regulated, raw, models) do + case allowed_endpoints(raw) do + [] -> + {:error, {:regulated_llm_endpoints_unset, @endpoints_env}} + + allowed -> + Enum.reduce_while(models, :ok, &first_refusal(&1, &2, allowed)) + end + end + + defp first_refusal(model, :ok, allowed) do + case check_model(model, allowed) do + :ok -> {:cont, :ok} + {:error, _} = error -> {:halt, error} + end + end + + @doc """ + Parses the allow-list into `{scheme, host}` pairs, dropping anything that is not a scheme and a + host. A malformed entry is dropped rather than widening the list. + """ + @spec allowed_endpoints(String.t() | nil) :: [{String.t(), String.t()}] + def allowed_endpoints(nil), do: [] + + def allowed_endpoints(raw) when is_binary(raw) do + raw + |> String.split(",", trim: true) + |> Enum.map(&String.trim/1) + |> Enum.reject(&(&1 == "")) + |> Enum.flat_map(fn entry -> + case URI.parse(entry) do + %URI{scheme: s, host: h} when is_binary(s) and is_binary(h) and h != "" -> [{s, h}] + _ -> [] + end + end) + end + + defp check_model(model, allowed) do + id = Map.get(model, :id, "(no id)") + url = Map.get(model, :base_url) + + if url in [nil, ""], + do: {:error, {:regulated_llm_endpoint_unstated, id}}, + else: check_url(id, url, allowed) + end + + defp check_url(id, url, allowed) do + case URI.parse(url) do + %URI{scheme: s, host: h} when is_binary(s) and is_binary(h) and h != "" -> + if {s, h} in allowed, + do: :ok, + else: {:error, {:regulated_llm_endpoint_not_allowed, id, "#{s}://#{h}"}} + + _ -> + {:error, {:regulated_llm_endpoint_unparseable, id, url}} + end + end +end diff --git a/test/trinity/profile_signer_down_test.exs b/test/trinity/profile_signer_down_test.exs new file mode 100644 index 0000000..8284b52 --- /dev/null +++ b/test/trinity/profile_signer_down_test.exs @@ -0,0 +1,59 @@ +# SPDX-FileCopyrightText: Sudo Apt Holdings LLC +# SPDX-License-Identifier: Apache-2.0 +defmodule Trinity.ProfileSignerDownTest do + @moduledoc """ + AC4, and the one thing this work was told not to change: the default profile still boots with the + signer down, and `:regulated` does not. + + **`async: false`, and the reason is not politeness.** `Trinity.Receipts.KeyCustody.boot!/1` writes + its answer to `:persistent_term` on both paths, including `{:unavailable, reason}` on failure. An + earlier version of this ran inside an async file and poisoned the signer for the whole run: 48 + unrelated tests failed with `{:signer_unavailable, {:key_file, :enotdir}}`. ExUnit runs + synchronous modules only after every async one has finished, so this is the only safe place to + induce a real failure, and the selection is restored afterwards whatever happens. + """ + use ExUnit.Case, async: false + + alias Trinity.Profile + alias Trinity.Receipts.KeyCustody + + setup do + # A path whose parent is a file, so the key store cannot be created. This is a real failure + # from the real function rather than a hand-written error tuple. + path = Path.join(System.tmp_dir!(), "profile-signer-#{System.unique_integer([:positive])}") + File.write!(path, "a file where a directory should be") + + on_exit(fn -> + # Put the real signer back before anything else runs. boot!/0 re-selects from the configured + # keys directory and rewrites the persistent term. + {:ok, _} = KeyCustody.boot!() + File.rm(path) + end) + + down = KeyCustody.boot!(Path.join(path, "keys")) + + assert {:error, _} = down, + "the probe did not produce a failing boot!, so nothing below is testing what it claims" + + {:ok, signer_down: down} + end + + test "the DEFAULT profile still boots with the signer down", %{signer_down: down} do + assert :ok = Profile.check_receipts(:default, down), + "the default profile refused a boot because the signer was unavailable. " <> + "Trinity.Receipts.Supervisor is fail-open for every profile that is not :regulated, " <> + "and making it fail closed for everyone is the one change this work was told not to " <> + "make." + end + + test "the REGULATED profile does not boot with the signer down, and says why", %{ + signer_down: down + } do + assert {:error, {:regulated_requires_receipts, {:key_file, :enotdir}}} = + Profile.check_receipts(:regulated, down) + end + + test "regulated boots when the signer is available" do + assert :ok = Profile.check_receipts(:regulated, {:ok, %{algorithm: :ed25519}}) + end +end diff --git a/test/trinity/profile_test.exs b/test/trinity/profile_test.exs new file mode 100644 index 0000000..bb300e9 --- /dev/null +++ b/test/trinity/profile_test.exs @@ -0,0 +1,174 @@ +# SPDX-FileCopyrightText: Sudo Apt Holdings LLC +# SPDX-License-Identifier: Apache-2.0 +defmodule Trinity.ProfileTest do + @moduledoc """ + The `:regulated` profile's refusals, and the thing it must not change. + + Every criterion here is a refusal. The pair that matters most is the last describe block: the + default profile still boots with the signer down, and `:regulated` does not. If the first of + those two ever fails, `Trinity.Receipts.Supervisor` has been made to fail closed for everyone, + which is the one thing this work was told not to do. + + No `collect/3` anywhere and no mailbox synchronisation of any kind: every assertion is a pure + function call. AC4's pair lives in `profile_signer_down_test.exs`, which must be synchronous, + because inducing a real signer failure writes global state. + """ + use ExUnit.Case, async: true + + alias Trinity.Profile + + describe "AC1: the MCP authorization profile must be :production" do + test "regulated accepts production" do + assert :ok = Profile.check_mcp_auth(:regulated, :production) + end + + test "regulated refuses local, by name" do + assert {:error, {:regulated_requires_production_mcp_auth, :local}} = + Profile.check_mcp_auth(:regulated, :local) + end + + test "regulated refuses personal, by name" do + assert {:error, {:regulated_requires_production_mcp_auth, :personal}} = + Profile.check_mcp_auth(:regulated, :personal) + end + + test "default accepts every one of them, unchanged" do + for p <- [:local, :personal, :production], + do: assert(:ok = Profile.check_mcp_auth(:default, p)) + end + end + + describe "AC2: the personal issuer does not start under regulated" do + test "regulated refuses the personal profile before MCP.Boot can swallow it" do + assert {:error, :regulated_refuses_personal_issuer} = + Profile.check_embedded_as(:regulated, :personal) + end + + test "regulated does not object to production or local here" do + assert :ok = Profile.check_embedded_as(:regulated, :production) + assert :ok = Profile.check_embedded_as(:regulated, :local) + end + + test "default is untouched" do + assert :ok = Profile.check_embedded_as(:default, :personal) + end + end + + describe "AC3: the model endpoint must be on the allow-list" do + @allowed "https://models.internal, https://gateway.lab.example" + + defp model(id, base_url), do: %{id: id, base_url: base_url} + + test "an unset allow-list refuses, naming the variable" do + for raw <- [nil, ""] do + assert {:error, {:regulated_llm_endpoints_unset, var}} = + Profile.check_llm_endpoints(:regulated, raw, [model("m", "https://x.example")]) + + assert var == Profile.endpoints_env() + end + end + + test "an allow-list of only junk refuses too, rather than allowing everything" do + assert {:error, {:regulated_llm_endpoints_unset, _}} = + Profile.check_llm_endpoints(:regulated, " , ,not-a-url", []) + end + + test "a model on the list is accepted" do + models = [ + model("a", "https://models.internal/v1"), + model("b", "https://gateway.lab.example") + ] + + assert :ok = Profile.check_llm_endpoints(:regulated, @allowed, models) + end + + test "a model off the list is refused, naming the model and the endpoint" do + assert {:error, {:regulated_llm_endpoint_not_allowed, "b", "https://api.openai.com"}} = + Profile.check_llm_endpoints(:regulated, @allowed, [ + model("b", "https://api.openai.com/v1") + ]) + end + + test "a model with no base_url is refused, because an unstated endpoint cannot be shown to be allowed" do + for missing <- [nil, ""] do + assert {:error, {:regulated_llm_endpoint_unstated, "c"}} = + Profile.check_llm_endpoints(:regulated, @allowed, [model("c", missing)]) + end + end + + test "the scheme is part of the match, so http is not https" do + assert {:error, {:regulated_llm_endpoint_not_allowed, _, "http://models.internal"}} = + Profile.check_llm_endpoints(:regulated, @allowed, [ + model("d", "http://models.internal/v1") + ]) + end + + test "default accepts anything at all" do + assert :ok = Profile.check_llm_endpoints(:default, nil, [model("x", nil)]) + end + + test "the allow-list parser keeps scheme and host and drops the rest" do + assert [{"https", "models.internal"}, {"https", "gateway.lab.example"}] = + Profile.allowed_endpoints(@allowed) + + assert [] = Profile.allowed_endpoints("no-scheme-or-host") + assert [] = Profile.allowed_endpoints(nil) + end + end + + describe "AC5: regulated must not run on the local authority" do + test "Local is refused, by name" do + assert {:error, :regulated_refuses_local_authority} = + Profile.check_authority(:regulated, Trinity.Authority.Local) + end + + test "an unset TRINITY_AUTHORITY resolves to Local, and is therefore refused" do + # The rule is Selection's, asked here rather than duplicated. + assert {:ok, Trinity.Authority.Local} = Trinity.Authority.Selection.select(nil) + assert {:ok, Trinity.Authority.Local} = Trinity.Authority.Selection.select("") + assert {:ok, Trinity.Authority.Local} = Trinity.Authority.Selection.select("local") + + assert {:error, :regulated_refuses_local_authority} = + Profile.check_authority( + :regulated, + elem(Trinity.Authority.Selection.select(nil), 1) + ) + end + + test "any other module is accepted, and none is invented here" do + assert :ok = Profile.check_authority(:regulated, Trinity.TestAuthority.Full) + end + + test "default runs on Local, unchanged" do + assert :ok = Profile.check_authority(:default, Trinity.Authority.Local) + end + end + + describe "the profile is read from the environment, and a typo is not permissive" do + test "an unrecognised value raises rather than falling back to default" do + assert_raise ArgumentError, ~r/TRINITY_PROFILE is not default or regulated/, fn -> + System.put_env("TRINITY_PROFILE", "regulate") + + try do + Profile.current() + after + System.delete_env("TRINITY_PROFILE") + end + end + end + + test "regulated and default are read, and absence is default" do + System.put_env("TRINITY_PROFILE", "regulated") + assert Profile.current() == :regulated + assert Profile.regulated?() + + System.put_env("TRINITY_PROFILE", "default") + assert Profile.current() == :default + + System.delete_env("TRINITY_PROFILE") + assert Profile.current() == :default + after + System.delete_env("TRINITY_PROFILE") + end + end +end From e0e96edef2745d8dad61ad51dc8effd3d1bebd22 Mon Sep 17 00:00:00 2001 From: Ayla Croft Date: Sun, 27 Sep 2026 20:43:19 -0400 Subject: [PATCH 2/5] test: prove AC1 to AC5 at a boot, in a spawned OS process The profile suite proves the decisions Trinity.Profile makes. It does not prove a boot, and start/2 cannot be re-run in a VM where the application is already up, so calling that suite AC1 to AC5 proves the easy half. Four cases, each a separate OS process with its own environment: 1. default + a broken key store -> starts. This is rule 1 as a test: the receipts supervisor stays fail-open for every profile that is not :regulated, and if this case ever goes red that shape has been changed. 2. regulated + production mcp_auth + a non-Local authority + no endpoint allow-list -> does not start, naming regulated_llm_endpoints_unset. 3. the same, with the authority left to resolve to Local -> does not start, naming regulated_refuses_local_authority. 4. every condition met -> starts, then stops. Without this case, 2 and 3 pass for a profile that refuses everything and prove nothing. Isolation: XDG_DATA_HOME per case, so each child gets its own data directory, lock and database files. MIX_ENV=dev rather than test, because the test environment puts both repos on the Ecto sandbox and a child with no owner blocks on checkout instead of booting. mix run --no-start, so the script can set application environment and define the non-Local authority fixture before the code under test runs; that fixture stays in the child rather than going into lib/, because no adapter is supplied by this tree. Nothing here writes :persistent_term in this VM, and there is no collect/3. Verified by planting three violations and watching the right case go red: returning :ok from check_receipts/2 under :default changed nothing, which is rule 2 holding; running the receipts check for every profile turned case 1 red; short-circuiting the endpoint check turned case 2 red; short-circuiting the authority check turned case 3 red. lib/ is byte-identical to before the plants. Signed-off-by: Ayla Croft --- test/trinity/regulated_boot_node_test.exs | 218 ++++++++++++++++++++++ 1 file changed, 218 insertions(+) create mode 100644 test/trinity/regulated_boot_node_test.exs diff --git a/test/trinity/regulated_boot_node_test.exs b/test/trinity/regulated_boot_node_test.exs new file mode 100644 index 0000000..7cdb405 --- /dev/null +++ b/test/trinity/regulated_boot_node_test.exs @@ -0,0 +1,218 @@ +# SPDX-FileCopyrightText: Sudo Apt Holdings LLC +# SPDX-License-Identifier: Apache-2.0 +defmodule Trinity.RegulatedBootNodeTest do + @moduledoc """ + AC1 to AC5 at the only place they are real: a boot. + + `Trinity.ProfileTest` proves the decisions. It does not prove the boot, and a suite that proves + `Profile.check_*` and calls that AC1 to AC5 is proving the easy half. `start/2` cannot be re-run + in this VM, where the application is already up, so each case here spawns a **separate OS + process** with its own environment and asserts on what that process did. + + ## How each case is isolated + + `XDG_DATA_HOME` per case, which `Trinity.Paths.data_dir/2` reads on Linux, so the child gets its + own data directory, its own `Trinity.DataDir.Lock` and its own database files and cannot collide + with this suite or with another case. + + `MIX_ENV=dev`, deliberately, and not `test`. The test environment puts both repos on + `Ecto.Adapters.SQL.Sandbox`, and a child process with no sandbox owner blocks on checkout rather + than booting: that is the trap slice 126 was opened for and then retracted over. + + `mix run --no-start`, so the script can set application environment and define a fixture module + **before** `Application.ensure_all_started/1` runs the code under test. + + Nothing here touches `:persistent_term` in this VM. Every mutation happens in the child. + """ + use ExUnit.Case, async: false + + @moduletag timeout: 180_000 + + # A signer failure the child can reproduce: the key file exists as a directory, so `File.read/1` + # answers `{:error, :eisdir}` and `KeyCustody.boot!/1` answers `{:error, {:key_file, :eisdir}}`. + # `keys_dir` itself is a real directory, so nothing raises on the way there. + defp broken_keys_dir(root) do + dir = Path.join(root, "broken-keys") + File.mkdir_p!(Path.join(dir, "receipts-ed25519.key")) + dir + end + + defp tmp!(name) do + dir = Path.join(System.tmp_dir!(), "regboot-#{name}-#{System.unique_integer([:positive])}") + File.mkdir_p!(dir) + on_exit(fn -> File.rm_rf(dir) end) + dir + end + + # One child boot. Returns `{output, exit_status}`; the script prints BOOT_OK or BOOT_REFUSED and + # halts, so the status distinguishes the two without parsing prose. + defp boot(env, script) do + base = [ + {"MIX_ENV", "dev"}, + {"TRINITY_FAKE_PROVIDER", "1"}, + # Never inherit this suite's values for anything the code under test reads. + {"TRINITY_PROFILE", nil}, + {"TRINITY_AUTHORITY", nil}, + {"TRINITY_REGULATED_LLM_ENDPOINTS", nil}, + {"TRINITY_MCP_AUTH_PROFILE", nil} + ] + + System.cmd("mix", ["run", "--no-start", "-e", script], + env: base ++ env, + stderr_to_stdout: true + ) + end + + # The tail of every script: start the application and say what happened. + @report """ + case Application.ensure_all_started(:trinity) do + {:ok, _} -> IO.puts("BOOT_OK"); System.halt(0) + {:error, reason} -> IO.puts("BOOT_REFUSED " <> inspect(reason, limit: 3)); System.halt(3) + end + """ + + # A module that implements Trinity.Authority and is not Local. Defined in the child rather than + # added to lib/, because AC5 says no adapter is supplied by this tree and none is invented. + @fixture """ + defmodule RegulatedBootFixture do + @behaviour Trinity.Authority + def stage(staged, _ctx), do: {:ok, staged} + def decide(_staged, decision, _ctx), do: {:ok, decision, "fixture"} + def execute(_staged, _decision, _ctx), do: {:error, :not_used} + def receipt(_kind, _attrs), do: {:error, :not_used} + end + """ + + defp production_mcp_auth do + """ + Application.put_env(:trinity, :mcp_auth, profile: :production) + """ + end + + defp allowed_models do + """ + llm = Application.get_env(:trinity, :llm, []) + Application.put_env(:trinity, :llm, Keyword.put(llm, :models, [ + %{id: "allowed", provider: :req_llm, model: "openai:x", + base_url: "https://models.internal/v1", api_key_env: "NONE", + caps: [:stream], price: %{input: 0.0, output: 0.0}} + ])) + """ + end + + describe "1. the default profile boots with the signer down" do + test "a broken key store does not stop a default node" do + root = tmp!("default-signer-down") + + script = """ + r = Application.get_env(:trinity, :receipts, []) + Application.put_env(:trinity, :receipts, Keyword.put(r, :keys_dir, "#{broken_keys_dir(root)}")) + #{@report} + """ + + {out, status} = boot([{"XDG_DATA_HOME", root}], script) + + assert status == 0, + """ + the default profile refused to boot with the signer down. That is rule 1: \ + lib/trinity/receipts/supervisor.ex is fail-open for every profile that is not \ + :regulated, and this is the test that says so. + + #{String.slice(out, -3000, 3000)} + """ + + assert out =~ "BOOT_OK" + end + end + + describe "2. regulated refuses an unset endpoint allow-list" do + test "production mcp_auth and a non-Local authority, but no allow-list, does not start" do + root = tmp!("regulated-no-endpoints") + + script = """ + #{@fixture} + #{production_mcp_auth()} + #{@report} + """ + + {out, status} = + boot( + [ + {"XDG_DATA_HOME", root}, + {"TRINITY_PROFILE", "regulated"}, + {"TRINITY_AUTHORITY", "RegulatedBootFixture"} + ], + script + ) + + assert status == 3, "the node started with no endpoint allow-list:\n#{out}" + assert out =~ "BOOT_REFUSED" + + assert out =~ "regulated_llm_endpoints_unset", + "it refused, but not for the reason under test:\n#{String.slice(out, -3000, 3000)}" + + assert out =~ "TRINITY_REGULATED_LLM_ENDPOINTS" + end + end + + describe "3. regulated refuses the local authority" do + test "everything else correct, but TRINITY_AUTHORITY unset, does not start" do + root = tmp!("regulated-local-authority") + + script = """ + #{production_mcp_auth()} + #{allowed_models()} + #{@report} + """ + + {out, status} = + boot( + [ + {"XDG_DATA_HOME", root}, + {"TRINITY_PROFILE", "regulated"}, + {"TRINITY_REGULATED_LLM_ENDPOINTS", "https://models.internal"} + ], + script + ) + + assert status == 3, "the node started on the local authority:\n#{out}" + + assert out =~ "regulated_refuses_local_authority", + "it refused, but not for the reason under test:\n#{String.slice(out, -3000, 3000)}" + end + end + + describe "4. regulated boots when every condition is met" do + test "production mcp_auth, a non-Local authority, an allowed endpoint and a signer" do + root = tmp!("regulated-good") + + script = """ + #{@fixture} + #{production_mcp_auth()} + #{allowed_models()} + #{@report} + """ + + {out, status} = + boot( + [ + {"XDG_DATA_HOME", root}, + {"TRINITY_PROFILE", "regulated"}, + {"TRINITY_AUTHORITY", "RegulatedBootFixture"}, + {"TRINITY_REGULATED_LLM_ENDPOINTS", "https://models.internal"} + ], + script + ) + + assert status == 0, + """ + a correctly configured regulated node refused to boot. If this fails while 2 and 3 \ + pass, the profile refuses everything and proves nothing. + + #{String.slice(out, -4000, 4000)} + """ + + assert out =~ "BOOT_OK" + end + end +end From 8a08e409004200cabf8ef4fe82be3aba850c0907 Mon Sep 17 00:00:00 2001 From: Ayla Croft Date: Mon, 28 Sep 2026 07:52:37 -0400 Subject: [PATCH 3/5] test: two more boot cases, and the printed reason is now readable Case 5: TRINITY_PROFILE=regulated with TRINITY_MCP_AUTH_PROFILE set to personal and then to local, everything else as case 4. Neither starts, and the printed reason names regulated_requires_production_mcp_auth and the profile it refused. The variable goes through config/runtime.exs rather than Application.put_env, because that is the path an operator uses and the one that rejects a value outside the three. Case 6: case 4's configuration with case 1's broken key store. Does not start, and the printed reason is {:regulated_boot_refused, {:regulated_requires_receipts, {:key_file, :eisdir}}}. Case 1 keeps passing, so the same key store still does not stop a default node, which is the whole difference AC4 is about. This is also the first case to reach the error branch of the receipts check at an actual boot: cases 1 to 4 never did, so whether the reason survived Supervisor.stop/2 inside start/2 was untested until now. It does. Found while writing case 5: the report tail inspected the reason with limit: 3, which printed BOOT_REFUSED {:trinity, {{...}, ...}} and hid it completely. The earlier cases asserted the reason appeared somewhere in the captured output, and matched the crash report OTP writes to stderr rather than the line the node printed. Now inspected with limit: :infinity, and cases 5 and 6 assert against the BOOT_REFUSED line alone via refusal_line/1. Planted breaks, each reverted, lib/ left byte-identical: skipping the receipts gate entirely (verify_regulated_receipts) turned case 6 red and left case 1 green, which is the pair the profile exists to create. making AC1's check_mcp_auth/2 always pass turned case 5 red, and showed that :personal is still caught by AC2's check_embedded_as/2 with a different reason (regulated_refuses_personal_issuer). Case 5 catches that only because it asserts the specific reason rather than that a refusal happened. Signed-off-by: Ayla Croft --- test/trinity/regulated_boot_node_test.exs | 117 +++++++++++++++++++++- 1 file changed, 115 insertions(+), 2 deletions(-) diff --git a/test/trinity/regulated_boot_node_test.exs b/test/trinity/regulated_boot_node_test.exs index 7cdb405..3c7cca9 100644 --- a/test/trinity/regulated_boot_node_test.exs +++ b/test/trinity/regulated_boot_node_test.exs @@ -64,13 +64,30 @@ defmodule Trinity.RegulatedBootNodeTest do end # The tail of every script: start the application and say what happened. + # + # `limit: :infinity` is not decoration. This was `limit: 3`, which printed + # `BOOT_REFUSED {:trinity, {{...}, ...}}` and hid the reason entirely, so an assertion that the + # reason was named passed only because the same words appear in the crash report OTP writes to + # stderr. A test for "it refused, and said why" has to read what the node said. @report """ case Application.ensure_all_started(:trinity) do - {:ok, _} -> IO.puts("BOOT_OK"); System.halt(0) - {:error, reason} -> IO.puts("BOOT_REFUSED " <> inspect(reason, limit: 3)); System.halt(3) + {:ok, _} -> + IO.puts("BOOT_OK") + System.halt(0) + + {:error, reason} -> + IO.puts("BOOT_REFUSED " <> inspect(reason, limit: :infinity, printable_limit: :infinity)) + System.halt(3) end """ + # The line the child printed, and nothing else: not the crash report, not a log line. + defp refusal_line(out) do + out + |> String.split("\n") + |> Enum.find("", &String.starts_with?(&1, "BOOT_REFUSED")) + end + # A module that implements Trinity.Authority and is not Local. Defined in the child rather than # added to lib/, because AC5 says no adapter is supplied by this tree and none is invented. @fixture """ @@ -215,4 +232,100 @@ defmodule Trinity.RegulatedBootNodeTest do assert out =~ "BOOT_OK" end end + + describe "5. regulated refuses an MCP authorization profile that is not production" do + test "personal and local are both refused, where case 4's configuration is otherwise met" do + for profile <- ["personal", "local"] do + root = tmp!("regulated-mcp-#{profile}") + + # No `Application.put_env` for :mcp_auth here, deliberately. The variable goes through + # `config/runtime.exs`, which is the path an operator actually uses, and which raises on a + # value that is not one of the three. MIX_ENV=dev, so that block runs (it is skipped in + # :test, where the suite sets :mcp_auth itself). + script = """ + #{@fixture} + #{allowed_models()} + #{@report} + """ + + {out, status} = + boot( + [ + {"XDG_DATA_HOME", root}, + {"TRINITY_PROFILE", "regulated"}, + {"TRINITY_MCP_AUTH_PROFILE", profile}, + {"TRINITY_AUTHORITY", "RegulatedBootFixture"}, + {"TRINITY_REGULATED_LLM_ENDPOINTS", "https://models.internal"} + ], + script + ) + + assert status == 3, + """ + the node started on the #{profile} MCP authorization profile. Case 4 is the same \ + configuration with production, and it starts, so this is the only difference. + + #{String.slice(out, -3000, 3000)} + """ + + assert out =~ "BOOT_REFUSED" + + reason = refusal_line(out) + + assert reason =~ "regulated_requires_production_mcp_auth", + "it refused, but the printed reason is not the one under test:\n#{reason}" + + # The reason names the profile that was refused, not just that one was. + assert reason =~ ":#{profile}" + end + end + end + + describe "6. regulated refuses to keep running when receipts cannot be appended" do + test "case 4's configuration with case 1's broken key store does not start" do + root = tmp!("regulated-signer-down") + + # Exactly case 4, plus the one break case 1 survives. Nothing else differs, so a refusal here + # is attributable to the key store and to the profile, and to nothing else. + script = """ + #{@fixture} + #{production_mcp_auth()} + #{allowed_models()} + r = Application.get_env(:trinity, :receipts, []) + Application.put_env(:trinity, :receipts, Keyword.put(r, :keys_dir, "#{broken_keys_dir(root)}")) + #{@report} + """ + + {out, status} = + boot( + [ + {"XDG_DATA_HOME", root}, + {"TRINITY_PROFILE", "regulated"}, + {"TRINITY_AUTHORITY", "RegulatedBootFixture"}, + {"TRINITY_REGULATED_LLM_ENDPOINTS", "https://models.internal"} + ], + script + ) + + assert status == 3, + """ + a regulated node started with the signer down. Case 1 proves the same key store does \ + not stop a :default node, and that difference is the whole point of AC4. + + #{String.slice(out, -4000, 4000)} + """ + + assert out =~ "BOOT_REFUSED" + + reason = refusal_line(out) + + assert reason =~ "regulated_boot_refused", + "it refused, but the printed reason is not the receipts check:\n#{reason}" + + assert reason =~ "regulated_requires_receipts" + + # The underlying signer failure is carried through rather than flattened into a bare atom. + assert reason =~ "key_file" + end + end end From c02e56a412d73b728bd1953bf66019d2ab9fc7c9 Mon Sep 17 00:00:00 2001 From: Ayla Croft Date: Tue, 29 Sep 2026 14:21:25 -0400 Subject: [PATCH 4/5] test: every refusing case asserts on the line the node printed Cases 2 and 3 asserted the reason appeared somewhere in the captured output. With stderr_to_stdout: true that output also carries OTP's crash report, which repeats the same words, so those assertions passed whether or not the node ever printed a reason. They now go through refusal_line/1, as cases 5 and 6 already did, and there is no unscoped match left on any refusal. Proved by planting the original inspect limit back: with limit: 3 the node prints BOOT_REFUSED {:trinity, {:bad_return, ...}} and all four refusing cases go red, while cases 1 and 4 (which assert BOOT_OK) stay green. in the same configuration, run by hand, the reason is still present in the captured output, on the crash report line: ** (RuntimeError) TRINITY_PROFILE=regulated refuses to boot: {:regulated_llm_endpoints_unset, "TRINITY_REGULATED_LLM_ENDPOINTS"} so the old assertion would have passed exactly where the new one fails. That is the difference the change is for. Signed-off-by: Ayla Croft --- test/trinity/regulated_boot_node_test.exs | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/test/trinity/regulated_boot_node_test.exs b/test/trinity/regulated_boot_node_test.exs index 3c7cca9..e9d2159 100644 --- a/test/trinity/regulated_boot_node_test.exs +++ b/test/trinity/regulated_boot_node_test.exs @@ -81,7 +81,9 @@ defmodule Trinity.RegulatedBootNodeTest do end """ - # The line the child printed, and nothing else: not the crash report, not a log line. + # The line the child printed, and nothing else: not the crash report, not a log line. Every + # refusing case asserts through this, so a reason that reaches stderr but never reaches the line + # the node printed fails the test rather than passing it. defp refusal_line(out) do out |> String.split("\n") @@ -165,10 +167,13 @@ defmodule Trinity.RegulatedBootNodeTest do assert status == 3, "the node started with no endpoint allow-list:\n#{out}" assert out =~ "BOOT_REFUSED" - assert out =~ "regulated_llm_endpoints_unset", - "it refused, but not for the reason under test:\n#{String.slice(out, -3000, 3000)}" + reason = refusal_line(out) + + assert reason =~ "regulated_llm_endpoints_unset", + "it refused, but the printed reason is not the one under test:\n#{reason}" - assert out =~ "TRINITY_REGULATED_LLM_ENDPOINTS" + # The refusal names the variable the operator has to set, not only that something is unset. + assert reason =~ "TRINITY_REGULATED_LLM_ENDPOINTS" end end @@ -194,8 +199,10 @@ defmodule Trinity.RegulatedBootNodeTest do assert status == 3, "the node started on the local authority:\n#{out}" - assert out =~ "regulated_refuses_local_authority", - "it refused, but not for the reason under test:\n#{String.slice(out, -3000, 3000)}" + reason = refusal_line(out) + + assert reason =~ "regulated_refuses_local_authority", + "it refused, but the printed reason is not the one under test:\n#{reason}" end end From a17db983acf67364f1467eeef262318e03f86adc Mon Sep 17 00:00:00 2001 From: Ayla Croft Date: Wed, 30 Sep 2026 11:21:01 -0400 Subject: [PATCH 5/5] test: the spawned-boot cases do not run on the FIPS leg The FIPS leg went red on all six cases at 05f444d, and none of them was red because a refusal failed. The child runs `mix run` under MIX_ENV=dev; the leg has no prebuilt dev tree, so the child compiled the dev dependency tree from scratch and reached the network for the `tokenizers` precompiled NIF. OTP's TLS client cannot finish that download: it fails the HelloRetryRequest middlebox assertion (erlang/otp#8470, docs/fips-leg.md finding 2, owned by slice 002). The child died at `mix compile`, so every assertion about what a node booted was really an assertion about a failed download. The module now carries :needs_dev_compile, and test_helper.exs excludes that tag only when TRINITY_FIPS_LEG=1, which is the variable the fips job already sets. gate and postgres run all six cases unchanged. No workflow change was needed: the leg already declares itself and the helper already branches on it. MIX_ENV=test was tried first, as the preferred mechanism, and is written up in the moduledoc. It does not block on the Ecto sandbox: test_helper.exs never runs in the child, so the pool stays in :auto mode and the app boots. It was rejected for a different reason. config/test.exs fixes the database path and does not derive it from XDG_DATA_HOME, so six children write into the suite's own database; measured, a child appended its boot receipt at seq 360 on the live chain. Overriding both repos' paths and migrating inside the child fixes that on SQLite, measured at 1.45 s with 25 and 4 tables created and the boot receipt back at seq 1. It does not carry to the postgres job, where :database names a database rather than a file and each case would need one provisioned and dropped. That code is not written here because it could not be tested here. Nothing under lib/. No case deleted. No product change: this is a limit of the harness on one leg. Signed-off-by: Ayla Croft --- test/test_helper.exs | 11 +++++++++- test/trinity/regulated_boot_node_test.exs | 26 +++++++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/test/test_helper.exs b/test/test_helper.exs index be84b45..b64be77 100644 --- a/test/test_helper.exs +++ b/test/test_helper.exs @@ -7,6 +7,14 @@ # Slice 003: :fips (tests that need FIPS mode on) runs only where the leg declares itself with # TRINITY_FIPS_LEG=1; excluded by tag elsewhere, never skipped. test/fips/mode_test.exs runs # everywhere and is what makes a leg that failed to enter the mode red rather than quiet. +# Slice regulated-boot: :needs_dev_compile (tests that spawn a child OS process running +# `mix run` under MIX_ENV=dev) is excluded on the FIPS leg alone. The leg has no prebuilt dev +# tree, so the child compiles the dev dependency tree from scratch, and that reaches the network +# to fetch the `tokenizers` precompiled NIF. OTP's TLS client cannot complete that download +# (HelloRetryRequest middlebox assertion, erlang/otp#8470, docs/fips-leg.md finding 2 and slice +# 002), so the child dies at `mix compile` and every assertion about what it booted is really an +# assertion about a failed download. Excluded here rather than skipped by hand, and only here: +# gate and postgres run the file in full. # Slice 032: :local_model (the real embedder through the serving) runs where # TRINITY_LOCAL_MODEL_CACHE names a cache that already holds all-MiniLM-L6-v2 (no download, # no network: a missing model is a failure there, not a skip); excluded by tag elsewhere. @@ -16,7 +24,8 @@ local_model? = System.get_env("TRINITY_LOCAL_MODEL_CACHE") not in [nil, ""] ExUnit.start( exclude: [:live, :desktop, :eval] ++ - if(fips_leg?, do: [], else: [:fips]) ++ if(local_model?, do: [], else: [:local_model]) + if(fips_leg?, do: [:needs_dev_compile], else: [:fips]) ++ + if(local_model?, do: [], else: [:local_model]) ) # Slice 024's boot receipt is written by a transient Task after the tree starts; once the diff --git a/test/trinity/regulated_boot_node_test.exs b/test/trinity/regulated_boot_node_test.exs index e9d2159..2c7a81b 100644 --- a/test/trinity/regulated_boot_node_test.exs +++ b/test/trinity/regulated_boot_node_test.exs @@ -23,9 +23,35 @@ defmodule Trinity.RegulatedBootNodeTest do **before** `Application.ensure_all_started/1` runs the code under test. Nothing here touches `:persistent_term` in this VM. Every mutation happens in the child. + + ## Why this file does not run on the FIPS leg + + `MIX_ENV=dev` means the child needs a dev build. On a developer machine and on the `gate` and + `postgres` jobs one either exists or is cheap to produce. The FIPS leg has **no prebuilt dev + tree**, so the child compiles the whole dev dependency tree from scratch, and that reaches the + network to fetch the `tokenizers` precompiled NIF. OTP's TLS client cannot complete that + download: it fails the HelloRetryRequest middlebox assertion (erlang/otp#8470, recorded in + `docs/fips-leg.md` finding 2 and owned by slice 002). The child then dies at `mix compile`, and + every assertion here about what a node booted becomes an assertion about a failed download. + + So the module carries `:needs_dev_compile`, which `test/test_helper.exs` excludes **only** when + `TRINITY_FIPS_LEG=1`. `gate` and `postgres` run all six cases unchanged. This is a limit of the + harness on one leg, not a gap in the profile: nothing here was red because a refusal failed. + + `MIX_ENV=test` was tried first, since the test tree is already built everywhere and needs no + network. The child boots (`test_helper.exs` never runs, so the sandbox stays in `:auto` mode and + nothing blocks on checkout), but the database path is fixed by `config/test.exs` and is not + derived from `XDG_DATA_HOME`, so six children write into the suite's own database: measured, a + child appended its boot receipt at `seq 360` on the live chain. Overriding both repos' paths and + migrating inside the child fixes that for SQLite, and was measured working in 1.45 s with 25 and + 4 tables created and the boot receipt back at `seq 1`. It does not carry to the `postgres` job, + where `:database` names a database rather than a file and each case would need one provisioned + and dropped. That is not written here, because it could not be tested here. """ use ExUnit.Case, async: false + # Excluded on the FIPS leg alone. See "Why this file does not run on the FIPS leg" above. + @moduletag :needs_dev_compile @moduletag timeout: 180_000 # A signer failure the child can reproduce: the key file exists as a directory, so `File.read/1`