Repository navigation
feat: regulated refuses an unnamed gateway; and the fail-open narrowing - #114
Merged
Merged
Conversation
SCR-815. Trinity.Receipts.Supervisor fails open for every profile that is not
:regulated: it logs, sets the alarm, and lets the tree start with every effect
denied until a signer returns. It does that on an error tuple.
Both ways of resolving the keys directory create it with the raising File
functions. KeyCustody.keys_dir/0 calls File.mkdir_p!/1 on a configured path, and
Trinity.Paths.keys_dir/0 calls File.mkdir_p!/1 and File.chmod!/2 on the default
one. A raise is not an error tuple, so it never reached the code that decides to
fail open, and the boot went down under every profile including :default, where
the documented promise is the opposite. Measured from a spawned :default boot:
BOOT_REFUSED {:trinity, {{:shutdown, {:failed_to_start_child,
Trinity.Receipts.Supervisor, {%File.Error{reason: :enotdir, ...
The shape is ordinary rather than exotic: a regular file where the keys directory
belongs, left by a sync client, a restore from backup, or a packaging mistake.
boot!/1 now resolves the directory through resolve_keys_dir/1, which rescues
File.Error and answers {:error, {:keys_dir, reason}}. The rescue sits at the
boundary where the contract changes rather than at either call site, and neither
keys_dir/0 nor Trinity.Paths.keys_dir/0 changes contract for its other callers.
receipts/supervisor.ex is untouched. It already handled the error tuple
correctly; the defect was that it never received one.
Red first, both tests failing on the raise before the fix:
** (File.Error) could not make directory (with -p) "/tmp/keys-dir-as-file-9989": not a directory
Signed-off-by: Ayla Croft <aylacroft@proton.me>
A gateway carries conversation text to somewhere that is not this host. That is the same egress a model endpoint is, with the same contents. The profile already refused an unlisted model endpoint by name and said nothing at all about gateways, so a regulated node could boot with a consumer chat cloud attached while being refused an unlisted model. That asymmetry is the whole defect. TRINITY_REGULATED_GATEWAYS is now the allow-list, mirroring TRINITY_REGULATED_LLM_ENDPOINTS: under :regulated every adapter in config :trinity, :gateways's :adapters must be named, or the boot is refused before any child starts. The refusal names the adapter, or names the variable when nothing is set. :default is untouched. A node with no gateway configured needs no allow-list. Requiring the variable where nothing reaches out would make every operator set one they do not use, which is how a control becomes a formality. This is deliberately not a ban on gateways, and the research behind that is worth recording because it was close to going the other way. An authorizable channel exists: Mattermost runs at IL4, IL5 and IL6 including JWICS, holds a Certificate to Field under Platform One's continuous ATO, authenticates with CAC through SAML, and carries the interactive dialogs an approval round-trip needs. Platform One's own ChatOps is Mattermost. Refusing the category would forbid what the deployment target itself runs; what this refuses is an unnamed channel, which is how a consumer cloud gets in by configuration. No such adapter ships here. The allow-list and the channel cap are independent and both still apply: gateways/cap.ex admits :read, :network and :write from a paired channel and refuses :exec and :destructive whatever the profile. The list decides whether a channel exists; the cap decides what it may do. Proven at a boot, not only as a decision function. Node case 7 runs two children with the same configured adapter and only the allow-list differing: without it the node refuses naming regulated_gateways_unset and the variable, with it the node starts. Without that second half a refusal would prove only that the check fires, not that it can be satisfied. mix gate: 960 passed, exit 0. Signed-off-by: Ayla Croft <aylacroft@proton.me>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Two independent changes, one per commit. Both come out of the DoD-first direction and the research behind it.
1.
TRINITY_REGULATED_GATEWAYS— an unnamed channel does not startThe profile refused an unlisted model endpoint by name and said nothing at all about gateways. So a regulated node could boot with a consumer chat cloud attached while being refused an unlisted model, with the same conversation text crossing the boundary either way. That asymmetry was the defect.
Under
:regulated, every adapter inconfig :trinity, :gateways's:adaptersmust now be named inTRINITY_REGULATED_GATEWAYSor the boot is refused before any child starts.:defaultis untouched.A node with no gateway configured needs no allow-list. Requiring the variable where nothing reaches out would make every operator set one they do not use, which is how a control becomes a formality.
Why this is an allow-list and not a ban
It nearly went the other way. An authorizable channel exists:
Banning the category would forbid what the deployment target itself runs. What this refuses is an unnamed channel, which is how a consumer cloud gets in by configuration. No such adapter ships in this tree.
The allow-list and the channel cap are independent and both apply.
lib/trinity/gateways/cap.exadmits:read,:networkand:writefrom a paired channel and refuses:execand:destructivewhatever the profile. The list decides whether a channel exists; the cap decides what it may do.Proven at a boot
Node case 7 runs two children with the same configured adapter and only the allow-list differing:
Without that second half a refusal would prove only that the check fires, not that it can be satisfied. Six pure tests cover the decision function, including that every adapter must be listed rather than one of them, and that
Foo.BarandElixir.Foo.Barare the same entry.Documented for a reviewer in
docs/regulated/authorization-boundary.md, in the "what crosses the boundary" table and a section that states plainly what the control does and does not stop.2. SCR-815 — a keys directory that cannot be made is an error, not a raise
Trinity.Receipts.Supervisorfails open for every profile that is not:regulated: it logs, sets the alarm, and lets the tree start with every effect denied. It does that on an error tuple.Both ways of resolving the keys directory create it with the raising
Filefunctions —KeyCustody.keys_dir/0callsFile.mkdir_p!/1on a configured path,Trinity.Paths.keys_dir/0callsFile.mkdir_p!/1andFile.chmod!/2on the default one. A raise is not an error tuple, so it never reached the code that decides to fail open:A
:defaultnode refused to start, which is the opposite of the documented promise. The shape is ordinary: a regular file where the keys directory belongs, left by a sync client, a restore, or a packaging mistake.boot!/1now resolves throughresolve_keys_dir/1, which rescuesFile.Errorand answers{:error, {:keys_dir, reason}}. The rescue sits at the boundary where the contract changes rather than at either call site, and neitherkeys_dir/0norTrinity.Paths.keys_dir/0changes contract for its other callers.receipts/supervisor.exis untouched. It already handled the error tuple correctly; the defect was that it never received one.Red first, both tests failing on the raise:
Evidence
960 against 951 on
main: six profile tests, two key-custody tests, one boot case.No new slice id, no ROADMAP row. Not merged.