From f4b41a22a2981ab530d066943b9109db4c9faef3 Mon Sep 17 00:00:00 2001 From: Ayla Croft Date: Sun, 27 Sep 2026 16:11:04 -0400 Subject: [PATCH 1/2] docs: a regulated-lab pack under docs/regulated Seven documents for an ISSM or an assessment team deciding whether Trinity can run inside an accredited boundary. Documentation only: no lib/ change, no behaviour change, no control asserted that the tree does not already implement. The rule the pack is written to is that every claim names a path in this repository and anything without one says NOT IN TREE. A census over the finished files resolves 59 distinct cited paths and all 59 exist. Three passages are quoted verbatim from the tree rather than paraphrased: the two mix task moduledocs and the CMVP paragraph in docs/fips-leg.md. What the pack refuses to claim, in its own words: not FedRAMP authorized, not CMMC certified, not a HIPAA attestation, and NIST has evaluated nothing here. Where docs/fips-leg.md cites CMVP certificate #4857 it is a Red Hat module's certificate, cited so a reader can check their own host, and that document already says Trinity does not claim the binaries are the same or claim a validation. crypto-inventory.md repeats the qualification rather than softening it, and every certificate cell reads UNKNOWN. The honest gaps are written as gaps. There is no allow-list of approved models in the tree, so data-flow.md says enforcement is configuration control and egress policy rather than code. No KMS or HSM adapter exists behind the key seam. Nothing forwards logs. No witness or transparency service exists, so a reader who does not trust the host cannot detect backdated forgery. incident.md leaves every contact as TODO, deliberately: a blank gets filled and an invented name gets used. authorization-boundary.md states the standalone limit rather than leaving it to be discovered: Trinity is judge, actor and scribe in one process, which is strong evidence to a reader who accepts the host was not compromised and no evidence at all to one who does not. Signed-off-by: Ayla Croft --- docs/regulated/README.md | 71 ++++++++++++++ docs/regulated/authorization-boundary.md | 93 ++++++++++++++++++ docs/regulated/crypto-inventory.md | 75 ++++++++++++++ .../customer-responsibility-matrix.md | 78 +++++++++++++++ docs/regulated/data-flow.md | 83 ++++++++++++++++ docs/regulated/incident.md | 83 ++++++++++++++++ docs/regulated/receipts-verify.md | 98 +++++++++++++++++++ 7 files changed, 581 insertions(+) create mode 100644 docs/regulated/README.md create mode 100644 docs/regulated/authorization-boundary.md create mode 100644 docs/regulated/crypto-inventory.md create mode 100644 docs/regulated/customer-responsibility-matrix.md create mode 100644 docs/regulated/data-flow.md create mode 100644 docs/regulated/incident.md create mode 100644 docs/regulated/receipts-verify.md diff --git a/docs/regulated/README.md b/docs/regulated/README.md new file mode 100644 index 0000000..d1b466e --- /dev/null +++ b/docs/regulated/README.md @@ -0,0 +1,71 @@ + + +# Trinity for a regulated lab: what this pack is, and what it is not + +For an ISSM or an assessment team deciding whether Trinity can run inside an accredited boundary. +It is written to be checked, not believed. Every claim below names a path in this repository, and +anything without one says **NOT IN TREE**. + +## Read this part first + +**Trinity is customer-controlled software. It is not a cloud service and this project is not a CSP.** +You run the binary, on your host, in your boundary. There is no Trinity-operated control plane, no +Trinity tenancy, and no Trinity staff with access to your data. That is what makes the rest of this +pack short: most of the controls an assessor asks about are yours, because the software is yours to +run. + +**Trinity is not FedRAMP authorized.** There is no FedRAMP package, no 3PAO assessment and no agency +ATO for this software. A FedRAMP authorization is a property of a service offering; this is not one. + +**Trinity is not CMMC certified.** There is no C3PAO assessment of this project. CMMC certifies an +organization's handling of CUI, not a library or an application, so the certifiable thing is your +lab, not this repository. + +**This pack is not a HIPAA attestation.** No SOC 2, no HITRUST, no third-party audit of any kind has +been performed on this software. Sudo Apt Holdings LLC is not a Business Associate by virtue of you +running this binary, and nothing here should be read as offering a BAA. + +**NIST has not evaluated, certified or endorsed anything in this repository.** Where this pack cites +a CMVP certificate it is the certificate of an operating system's cryptographic module, named so you +can check it against your own running binary, and `docs/fips-leg.md` says in terms that Trinity does +not claim the binaries are the same. + +## What the project does claim + +Three things, each of which an assessor can verify from the tree rather than from this page. + +1. **Every governed action is recorded in a hash-linked, signed chain**, and the chain can be + verified by a reader who does not trust the program that wrote it. The verifier is + `lib/trinity/receipts/verifier.ex`; the task is `lib/mix/tasks/trinity.receipts.verify.ex`; + a standalone script that needs no application is `bin/verify_receipt.exs`. See + `receipts-verify.md` in this directory. +2. **The set of code paths that can cause an effect is closed**, and that closure is asserted by a + census over the tree rather than by intent. The boundary is `lib/trinity/effects.ex`; the + published population is `docs/effects-catalog.md`. +3. **Standalone, Trinity decides, acts and records by itself**, which is a specific and limited kind + of evidence. `authorization-boundary.md` in this directory states the limit rather than leaving + it to be discovered. + +## What this pack does not decide for you + +Whether Trinity may process CUI or PHI in your boundary is your determination, not this project's. +`customer-responsibility-matrix.md` sets out which controls are yours, which are the software's, and +which belong to the host or the cloud account underneath it. `data-flow.md` states what may enter a +model prompt and what may not. Read both before the others. + +## The files + +| File | What it answers | +|---|---| +| `authorization-boundary.md` | What is inside the boundary, what is outside, and where the trust ends | +| `customer-responsibility-matrix.md` | Who owns each control: the software, you, or the host | +| `data-flow.md` | What may enter a prompt, and what must never leave the boundary | +| `incident.md` | Reporting clocks and who to contact | +| `receipts-verify.md` | How an assessor verifies the record without trusting the application | +| `crypto-inventory.md` | Every cryptographic capability, its module, and its certificate status | + +## Status of this pack + +This directory is documentation only. It adds no code, changes no behaviour, and asserts no control +that is not already in the tree. Where the tree does not implement something this pack names, the +row says **NOT IN TREE** rather than describing an intention. diff --git a/docs/regulated/authorization-boundary.md b/docs/regulated/authorization-boundary.md new file mode 100644 index 0000000..b88a484 --- /dev/null +++ b/docs/regulated/authorization-boundary.md @@ -0,0 +1,93 @@ + + +# Authorization boundary + +What sits inside the accredited boundary when Trinity runs, what sits outside it, and the one thing +about the standalone configuration an assessor has to understand before anything else in this pack +is useful. + +## The boxes + +``` + ACCREDITED BOUNDARY (yours) + +--------------------------------------------------------------+ + | | + | [ Trinity process ] [ Receipts store ] | + | BEAM/OTP application hash-linked signed chain | + | permission gate, effect own SQLite file, own | + | membrane, session loop connection pool | + | | + | [ Customer files ] | + | the filesystem roots you | + | grant, and nothing else | + | | + +--------------------------------------------------------------+ + | | + | prompt + completion | OIDC / OAuth 2.1 + v v + [ Model endpoint ] [ Customer IdP ] + OUTSIDE the boundary OUTSIDE the boundary +``` + +| Box | Inside the boundary? | Path | +|---|---|---| +| Trinity process | Yes. Runs on your host, under your accounts | `lib/trinity/application.ex` | +| Receipts store | Yes. Its own database file, separate from the primary | `lib/trinity/repo/receipts.ex`, `lib/trinity/paths.ex` | +| Customer files | Yes. Only the roots you grant | `lib/trinity/tools/fs/` | +| Model endpoint | **No.** Whatever you point it at, wherever it runs | `config/llm.exs:14` (the model list) | +| Customer IdP | **No.** Yours, and Trinity is a resource server to it | `lib/trinity/mcp/auth/`, `config/runtime.exs:100` | + +Trinity holds a single-instance lock on the data directory so two processes cannot share one store: +`lib/trinity/data_dir/lock.ex`. + +## Standalone: judge, actor and scribe + +In the default configuration (`TRINITY_AUTHORITY` unset, or `local`) Trinity decides whether an +effect may happen, performs it, and writes the record of having done so. All three roles are the +same process. + +- The decision is `lib/trinity/permissions/`, through `lib/trinity/authority/local.ex`. +- The effect crosses one boundary, `lib/trinity/effects.ex`, which re-derives the fingerprint from + the arguments actually passed before admitting the call. +- The record is `lib/trinity/receipts/chain_writer.ex`, signed before the row is written. + +**What that is worth.** It is strong evidence to a reader who accepts that the host was not +compromised: the chain is hash-linked and signed, the effect population is closed and published, +and the verifier runs without the application. It proves the software did what the record says. + +**What it is not.** There is no separation of duties. A party who does not already trust the host +has only the host's word for it, because the same process made the decision and wrote the record of +the decision. `docs/10-assurance-case.md` states this, and criterion C6 there records that a signing +key in a file proves the records were not altered by anything lacking read access to that file, and +proves nothing about custody of the key. + +**A signed receipt with an untrusted clock is weaker evidence than one with a trusted clock**, and +Trinity's clock is the host's own reading: `lib/trinity/receipts/clock.ex`. A deployment that needs +the time itself to be trustworthy supplies a trusted time source. `docs/09-standards-register.md` +carries that as a real-world dependency rather than a property of this tree. + +## Regulated use requires an external authority + +For a boundary where the decision must not be the machine's to make, Trinity supports pointing +`TRINITY_AUTHORITY` at an adapter that lives outside this repository. When one is in force, Trinity +keeps **no executor** for the effects that adapter governs, and that absence is asserted by a census +over the tree rather than by intent (`lib/trinity/authority.ex`, and the census recorded in slice +027's record). + +The adapter itself is **NOT IN TREE**. ADR-0008 (`docs/adr/0008-authority-is-an-adapter.md`) states +that no slice in this repository builds one and that it is downstream work in a downstream +repository. An assessor evaluating a regulated deployment is evaluating that adapter as well as +this software, and this pack says nothing about it. + +## What crosses the boundary, and when + +| Crossing | Direction | Gate | Path | +|---|---|---|---| +| Prompt and completion | Out, then in | Every turn | `lib/trinity/sessions/prompt.ex:45` | +| Tool effects on files | Inside only | Permission gate, then membrane | `lib/trinity/effects.ex` | +| Web fetch and search | Out | Permission gate | `lib/trinity/tools/` | +| MCP server requests | In | OAuth 2.1 resource server | `lib/trinity/mcp/auth.ex` | +| Receipt forwarding | Out, if configured | Optional authority callback | `lib/trinity/receipts/forwarder.ex` | + +A witness or transparency service that would let a third party detect backdated forgery without +trusting the host is **NOT IN TREE**. diff --git a/docs/regulated/crypto-inventory.md b/docs/regulated/crypto-inventory.md new file mode 100644 index 0000000..5cd6eea --- /dev/null +++ b/docs/regulated/crypto-inventory.md @@ -0,0 +1,75 @@ + + +# Cryptographic inventory + +Every cryptographic capability Trinity uses, the module that calls it, and what is underneath. + +**Read the certificate column carefully.** Trinity implements no cryptography of its own. Every +primitive is OTP's `:crypto`, which calls the OpenSSL the host provides. A CMVP certificate belongs +to that provider, on that host, not to this software. **Anything not established from a document is +`UNKNOWN` and stays `UNKNOWN`.** + +## The table + +| Capability | Trinity module | Underlying | CMVP cert | Notes | +|---|---|---|---|---| +| Receipt signing, default | `lib/trinity/receipts/signer/ed25519.ex` | `:crypto.sign(:eddsa, :none, …, :ed25519)`, line 21 | **UNKNOWN** | Ed25519, RFC 8032. `available?/0` line 14 returns false when `:crypto.info_fips() == :enabled`, so it is not selected in FIPS mode | +| Receipt signing, FIPS mode | `lib/trinity/receipts/signer/p384.ex` | `:crypto.sign(:ecdsa, :sha384, …, :secp384r1)`, line 25 | **UNKNOWN** | ECDSA P-384 with SHA-384. Selected when the mode is enabled | +| Receipt signing, opt-in | `lib/trinity/receipts/signer/mldsa87.ex` | `:crypto` ML-DSA-87, FIPS 204 | **UNKNOWN** | Never the default. `available?/0` line 24 checks `:mldsa87 in :crypto.supports(:public_keys)` | +| Receipt hashing | `lib/trinity/receipts/envelope.ex:36` | `:crypto.hash(:sha256, …)` | **UNKNOWN** | Over the DSSE pre-authentication encoding, not the bare payload | +| Merkle tree over a chain | `lib/trinity/receipts/merkle.ex` | `:crypto.hash(:sha256, …)` | **UNKNOWN** | RFC 6962 hashing: distinct leaf and node prefixes, odd nodes promoted not duplicated | +| Blob encryption at rest | `lib/trinity/vault.ex:180` | `:crypto.crypto_one_time_aead(:aes_256_gcm, …)` | **UNKNOWN** | Fresh 256-bit data key per blob, `docs/encryption-at-rest.md` | +| Data key wrapping | `lib/trinity/keys/local.ex:179` | `:crypto.crypto_one_time_aead(:aes_256_gcm, …)` | **UNKNOWN** | The key encryption key wraps the data key beside the ciphertext | +| Key derivation from a passphrase | `lib/trinity/keys/local.ex:280` | `:crypto.pbkdf2_hmac(:sha512, …)`, 600,000 iterations (line 57) | **UNKNOWN** | Only when `TRINITY_KEYS_PASSPHRASE` is set | +| Sub-key derivation | `lib/trinity/keys/local.ex:415` | `:crypto.mac(:hmac, :sha256, …)` | **UNKNOWN** | | +| Key id | `lib/trinity/keys/local.ex:226` | `:crypto.hash(:sha256, "trinity/key-id" <> root)` | Not applicable | An identifier, not a secret | +| MCP request state envelope | `lib/trinity/mcp/server/envelope.ex:49` | `:crypto.crypto_one_time_aead(:aes_256_gcm, …)` | **UNKNOWN** | Slice 061, key in the keys directory | +| Device identifier | `lib/trinity/receipts/clock.ex` | `:crypto.strong_rand_bytes/1` | Not applicable | Random, names nothing about the host | +| JWK thumbprint | `lib/trinity/receipts/signer.ex` | `:crypto.hash(:sha256, …)` | Not applicable | RFC 7638 | +| Canonical JSON | `lib/trinity/receipts/envelope.ex:15` | `Jcs.encode/1` | Not applicable | RFC 8785, not a cryptographic primitive | + +## Why every certificate says UNKNOWN + +Because the honest answer depends on the host, and this repository cannot know it. + +`docs/fips-leg.md` is the only place a certificate number appears in this tree, and it appears with +a qualification that this inventory repeats rather than softens. From `docs/fips-leg.md:53-59`: + +> Red Hat's page, https://access.redhat.com/compliance/fips, read 2026-09-20, lists for Red Hat +> Enterprise Linux 9.8 the module "OpenSSL", version `3.0.7-395c1a240fbfffd8`, CMVP certificate +> #4857, status "Active". The version string the image reports and the version string the page +> lists differ in their build hash. Trinity does not claim they are the same binary, and does not +> claim a validation. What the leg establishes is narrower and exact: the gate and the suite run +> with OTP in FIPS mode against the FIPS provider that this distribution ships and names, with the +> algorithms the mode removes listed below. Whether a deployment's provider is a validated one is +> that deployment's question, answered from the certificate and the running binary, not from this +> file. + +So: **certificate #4857 is the certificate of a Red Hat module**, cited for your convenience in +checking your own host. It is not Trinity's certificate, it does not travel with this software, and +on a different distribution it is irrelevant. + +## What the FIPS leg does establish + +`docs/fips-leg.md`, and the `fips` job in `.github/workflows/gate.yml`: + +- The full gate and test suite run with OTP in FIPS mode, on every push, in a container built from + `ci/fips/Containerfile`. +- The mode is entered by `ERL_AFLAGS="-crypto fips_mode true -eval application:load(crypto)"`. +- The algorithms the mode removes are listed in that document, and the signer selection responds: + Ed25519 reports unavailable in the mode (`lib/trinity/receipts/signer/ed25519.ex:14`) and P-384 is + selected instead. + +That is a statement about behaviour under a mode. It is **not** a validation, and no part of this +repository should be cited as one. + +## What an assessor should do with this page + +1. Establish which OpenSSL provider your host actually loads, and its certificate, from the running + binary. Not from this file. +2. Decide whether Ed25519 is acceptable in your boundary. If it is not, run in FIPS mode and the + selection changes by itself. +3. Note that **key custody is not addressed by any row above.** The default is a 0600 file + (`lib/trinity/paths.ex`), the seam for a KMS or HSM exists (`lib/trinity/keys.ex`), and **no KMS + or HSM adapter is in the tree**. +4. Note that **ML-DSA-87 is never selected by default** and requires the linked OpenSSL to carry it. diff --git a/docs/regulated/customer-responsibility-matrix.md b/docs/regulated/customer-responsibility-matrix.md new file mode 100644 index 0000000..74ff00a --- /dev/null +++ b/docs/regulated/customer-responsibility-matrix.md @@ -0,0 +1,78 @@ + + +# Customer responsibility matrix + +Who owns each control. **Trinity software** means this repository implements it and a path is named. +**Customer** means your lab owns it and Trinity cannot do it for you. **Host or AWS** means the +operating system, the volume, or the cloud account underneath the process. + +A row with no path says **NOT IN TREE**, which means the software does not implement it and you +should not plan as though it does. + +## Identity provider + +| Control | Trinity software | Customer | Host or AWS | +|---|---|---|---| +| Operating the IdP | No | **Yes.** Trinity is a resource server, never an authorization server for your users | No | +| Validating a bearer token on an inbound MCP request | **Yes.** `lib/trinity/mcp/auth.ex`, `lib/trinity/mcp/auth/token.ex` | Configure issuer, audience, resource | No | +| Choosing the profile | **Yes**, three exist: `:local`, `:production`, `:personal` at `config/runtime.exs:92-98` | **Yes.** `TRINITY_MCP_AUTH_PROFILE` is yours to set | No | +| Mapping a subject to an authorization decision | Scopes only, `lib/trinity/mcp/auth/scopes.ex` | **Yes.** Entitlement policy is yours | No | +| Token revocation and session lifetime | Introspection when enabled, `config/runtime.exs:105` | **Yes** | No | + +## Model + +| Control | Trinity software | Customer | Host or AWS | +|---|---|---|---| +| Which model is called | Configuration only, `config/llm.exs:14` | **Yes.** You choose the endpoint and the provider | Network path | +| Enforcing that only an approved model may be called | **NOT IN TREE.** No allow-list mechanism exists in the tree; the model list is configuration and nothing refuses an unapproved one at runtime | **Yes.** Enforce by configuration control and egress policy | Egress filtering | +| Whether the model provider trains on your data | No | **Yes.** A contract question with your provider | No | +| Keeping the model inside your boundary | No | **Yes**, if required. Self-hosting is a customer deployment choice | **Yes** | + +## Keys + +| Control | Trinity software | Customer | Host or AWS | +|---|---|---|---| +| Receipt signing key generation and use | **Yes.** `lib/trinity/receipts/key_custody.ex`, seam at `lib/trinity/keys.ex` | No | No | +| Where key material lives | Default is a 0600 file in the data directory, `lib/trinity/paths.ex` (`keys_dir/0`) | **Yes.** Custody is a deployment decision | Filesystem permissions | +| A KMS or HSM adapter | Seam exists (`lib/trinity/keys.ex`); **no KMS or HSM adapter is in the tree** | **Yes**, if required | **Yes** | +| Custody of the signing key | **No, and this is stated rather than implied.** `docs/10-assurance-case.md` C6 | **Yes** | **Yes** | +| Envelope encryption of blobs | **Yes.** AES-256-GCM, `docs/encryption-at-rest.md` | No | No | +| Encryption of everything SQLite indexes | **No, by design.** `docs/encryption-at-rest.md` | No | **Yes.** Volume or page level below SQLite | + +## Egress + +| Control | Trinity software | Customer | Host or AWS | +|---|---|---|---| +| Suppressing network access in the test suite | **Yes.** `test/network_guard_test.exs` | No | No | +| Refusing an effect that was not approved | **Yes.** `lib/trinity/effects.ex` re-derives the fingerprint before admitting | No | No | +| Restricting which hosts the process may reach | **NOT IN TREE** | **Yes** | **Yes.** Security group, egress proxy, firewall | +| Redacting credential-shaped strings from logs | **Yes**, as a last line. `lib/trinity/telemetry/redaction.ex` | No | No | +| Deciding what may leave the boundary at all | No | **Yes.** See `data-flow.md` | **Yes** | + +## Backups + +| Control | Trinity software | Customer | Host or AWS | +|---|---|---|---| +| Producing a portable archive | **Yes.** `mix trinity.export`, `lib/mix/tasks/trinity.export.ex`, `docs/backup.md` | Run it | No | +| Restoring onto a fresh install | **Yes.** `lib/mix/tasks/trinity.import.ex` | Run it and test it | No | +| Scheduling, offsite copies, retention of backups | **NOT IN TREE** | **Yes** | **Yes** | +| Encrypting the archive at rest | Archive blobs are encrypted, `docs/encryption-at-rest.md` | Storage of the archive | **Yes** | + +## Retention + +| Control | Trinity software | Customer | Host or AWS | +|---|---|---|---| +| Ageing memory entries | **Yes.** Stale at 30 days, archived at 90, `lib/trinity/memory/curator.ex` | Tune the thresholds | No | +| Deleting anything on a schedule | **No. The curator deletes nothing**, it marks and archives in place (`lib/trinity/memory/curator.ex`) | **Yes**, if your policy requires deletion | No | +| Receipt chain retention | **Append only. Nothing in the tree deletes a receipt** | **Yes.** How long you keep the store | **Yes** | +| A records schedule, or legal hold | **NOT IN TREE** | **Yes** | No | + +## Incidents + +| Control | Trinity software | Customer | Host or AWS | +|---|---|---|---| +| Raising an alarm when the signer is unavailable | **Yes.** `lib/trinity/receipts/alarm.ex`, and every effect is denied until one is | Monitor it | No | +| Telemetry events an operator can subscribe to | **Yes.** `docs/telemetry.md`, `lib/trinity/telemetry.ex` | Collect and alert | **Yes** | +| Shipping logs or metrics anywhere | **NOT IN TREE.** Trinity emits; it does not forward | **Yes** | **Yes** | +| Reporting to a government customer within a clock | No | **Yes.** See `incident.md` | No | +| A published vulnerability disclosure process for this software | **Yes.** `SECURITY.md` | Report to it | No | diff --git a/docs/regulated/data-flow.md b/docs/regulated/data-flow.md new file mode 100644 index 0000000..cba7c41 --- /dev/null +++ b/docs/regulated/data-flow.md @@ -0,0 +1,83 @@ + + +# Data flow: what may enter a prompt, and what must not leave + +The model endpoint is outside the accredited boundary (`authorization-boundary.md`). Everything in a +prompt has therefore left your boundary by the time the model sees it. This page says what Trinity +puts in a prompt, what it will not put there, and which of those is enforced in code rather than by +policy. + +## What enters a prompt + +Assembled by `Trinity.Sessions.Prompt.build/5`, `lib/trinity/sessions/prompt.ex:45`. + +| Tier | What it is | Path | +|---|---|---| +| Persona | The always-on instructions for the session's persona | `lib/trinity/personas.ex` | +| Always-on memory | Memory entries in the session, persona and global scopes | `lib/trinity/memory.ex` | +| Project context | `AGENTS.md` from the project root, when one is set | `lib/trinity/context.ex` | +| Conversation | The recent turns of this session | `lib/trinity/sessions/store.ex` | +| Tool definitions | Name, description and JSON schema of each available tool | `lib/trinity/tools/registry.ex` | +| Retrieved memory | Entries recalled for this turn | `lib/trinity/memory.ex` | + +Each tier has a token budget and is truncated rather than dropped silently; a truncation is itself +recorded as a query receipt. The budgets are `config :trinity, :prompt_budgets`. + +**Everything in that table is customer data by default.** A file read by a tool, a memory entry, a +line of `AGENTS.md` and the text of a conversation all go to the model. If any of it is CUI or PHI, +then CUI or PHI is leaving your boundary on every turn that includes it. + +## What never enters a prompt + +**Token material does not reach model context.** An inbound MCP request is validated and reduced to +a principal before anything downstream sees it, and the principal is defined not to carry the token. +`lib/trinity/mcp/auth/principal.ex:7`: + +> Who a validated request is from (slice 062): the issuer, the subject, the scopes, the client, the +> token's id when it has one, and the profile that admitted it. What the server puts in the context +> and the receipts carry; never the token. + +The same rule holds on the way out: a refusal reason is "a word or two, never a token" +(`lib/trinity/mcp/auth_host.ex:168`), and `Trinity.MCP.Auth.authorize/2` returns "the principal, or +why not (never the token)" (`lib/trinity/mcp/auth.ex:45`). + +The receipt signing key never enters a prompt either. It is read only by the custody module, +`lib/trinity/receipts/key_custody.ex`, which no prompt tier touches. + +**A last line, not the control.** `lib/trinity/telemetry/redaction.ex` masks credential-shaped +strings on their way into a log. It is a Logger filter, it does not sit between the session and the +model, and its own moduledoc says it is not the control. Do not rely on it to keep a secret out of a +prompt. + +## CUI and PHI + +**This is the part that is policy, not code.** + +The requirement: CUI or PHI must not leave the boundary except to a model the lab has approved for +that category of data, under a contract that permits it. + +The enforcement: **NOT IN TREE.** There is no allow-list of approved models in this repository, and +nothing refuses a call to an unapproved endpoint at runtime. The model in force is configuration +(`config/llm.exs:14`, and `TRINITY_MODEL` at runtime). Trinity will call whatever it is pointed at. + +What this means for an accreditation: + +1. **Model selection is a configuration-controlled item.** Treat a change to the model endpoint as a + change to the boundary, because it is one. +2. **Egress restriction is yours.** A security group, an egress proxy or a firewall rule is what + actually stops a prompt reaching an unapproved endpoint. See `customer-responsibility-matrix.md`. +3. **If no approved model exists for the category, the data must not enter a session at all.** Once + it is in the conversation it is in the prompt on the next turn, because the conversation tier is + sent verbatim. + +## Where the data rests + +| Store | Contents | Encrypted by Trinity? | Path | +|---|---|---|---| +| Primary database | Sessions, messages, memory, approvals | **No.** Left to the volume, by design | `docs/encryption-at-rest.md` | +| Receipts database | The signed chain, separate file and pool | **No.** Left to the volume | `lib/trinity/repo/receipts.ex` | +| Skill files, staged changes, archives | Blobs nothing indexes | **Yes.** AES-256-GCM, fresh data key each | `docs/encryption-at-rest.md` | +| Keys directory | Signing key, device id | Mode 0600 in a 0700 directory | `lib/trinity/paths.ex` | + +The split is deliberate and the reasoning is in `docs/encryption-at-rest.md`: anything SQLite indexes +is left to the volume beneath it, because encrypting it in code would defeat the index. diff --git a/docs/regulated/incident.md b/docs/regulated/incident.md new file mode 100644 index 0000000..947cb48 --- /dev/null +++ b/docs/regulated/incident.md @@ -0,0 +1,83 @@ + + +# Incident reporting + +Clocks that may apply when Trinity is inside an accredited boundary, and what the software gives you +to work with. **The reporting obligation is the lab's, not this project's.** Trinity is customer +controlled software; there is no provider-side incident process that runs on your behalf. + +## CONTACT = TODO + +**This pack does not yet name a contact.** Fill this in before the pack is used in an assessment: + +| Role | Contact | +|---|---| +| Lab incident response lead | TODO | +| Contracting officer or COR | TODO | +| Privacy official, if PHI is in scope | TODO | +| Security reports about this software | `SECURITY.md` in this repository | + +Leaving these as TODO is deliberate. An invented name is worse than a blank, because a blank gets +filled and a wrong name gets used. + +## The 72-hour DFARS clock + +If the lab holds a contract with **DFARS 252.204-7012**, a cyber incident affecting a covered +contractor information system, or the covered defense information on it, is **reported to DoD within +72 hours of discovery**, through the DoD reporting mechanism. A medium assurance certificate is +required to submit. The clock runs from discovery, not from confirmation, and the obligation is the +contractor's. + +**What Trinity contributes:** a signed, hash-linked record of every governed action, with the +verifier in `receipts-verify.md`. That is evidence for the report. It is not the report, and +producing it does not start, stop or satisfy the clock. + +**What Trinity does not do:** it does not detect an incident, does not notify anyone, and does not +ship logs anywhere. Forwarding is **NOT IN TREE** (`customer-responsibility-matrix.md`, Incidents). + +## The HIPAA clock, if PHI is in scope + +If PHI is in scope, the Breach Notification Rule applies to the covered entity: individuals without +unreasonable delay and **no later than 60 days** from discovery, the Secretary on the same clock for +a breach of 500 or more individuals, and annually otherwise. + +Two things to be clear about before relying on that paragraph: + +1. **This pack is not a HIPAA attestation** and nothing in this repository has been audited against + the Security Rule. +2. **Sudo Apt Holdings LLC is not your Business Associate** by virtue of you running this software. + Running a binary on your own host does not create the relationship, and no BAA is offered here. + +Whether an event is a breach under 45 CFR 164.402, including the risk assessment, is the covered +entity's determination. + +## What the software gives you at the moment of an incident + +| Need | What exists | Path | +|---|---|---| +| A record of what was decided and done | Signed hash chain, per scope | `lib/trinity/receipts/chain_writer.ex` | +| Verify that record without trusting the app | Standalone verifier and a mix task | `receipts-verify.md` | +| Know the signer was unavailable | Alarm, and every effect denied until a signer returns | `lib/trinity/receipts/alarm.ex` | +| Subscribe to events | Telemetry, documented | `docs/telemetry.md` | +| Take a copy of the whole state | `mix trinity.export` | `docs/backup.md` | +| Know which code paths can cause an effect | Published, closed population | `docs/effects-catalog.md` | + +## What the software does not give you + +- **No intrusion detection.** Trinity does not decide that something bad happened. +- **No alerting.** It emits telemetry; collecting and alerting on it is yours. +- **No log shipping.** NOT IN TREE. +- **No third-party witness.** Nothing outside the host attests to the chain, so a reader who does not + trust the host cannot detect backdated forgery. NOT IN TREE, and stated in + `authorization-boundary.md`. +- **No tamper alarm on the store itself.** The chain is verifiable on demand; nothing watches it + continuously. + +## A note on the clock and the clock + +The receipt clock is the host's own reading (`lib/trinity/receipts/clock.ex`). It never goes +backwards within a chain, and it is signed, so the ordering cannot be edited afterwards by anything +that cannot sign. It is **not** a trusted timestamp and must not be offered to an investigator as +one. If your incident process needs authoritative time, supply a trusted time source; the standards +register records that as a deployment dependency rather than a property of this tree +(`docs/09-standards-register.md`). diff --git a/docs/regulated/receipts-verify.md b/docs/regulated/receipts-verify.md new file mode 100644 index 0000000..c50acec --- /dev/null +++ b/docs/regulated/receipts-verify.md @@ -0,0 +1,98 @@ + + +# Verifying the record + +How an assessor checks Trinity's record of what it did, **without trusting the program that wrote +it**. Everything here quotes a task or a script that exists in the tree. Nothing is invented. + +## Two entry points, and why there are two + +| Entry point | Needs | Path | +|---|---|---| +| `mix trinity.receipts.verify` | The application, or just the exported file | `lib/mix/tasks/trinity.receipts.verify.ex` | +| `bin/verify_receipt.exs` | Elixir only. **Not the application** | `bin/verify_receipt.exs` | + +The second is the one that matters for an assessment. A verifier that has to start Trinity to check +Trinity's records is asking you to trust the thing under examination. `bin/verify_receipt.exs` reads +an exported file and needs nothing from this application. + +## The task, quoted + +From `lib/mix/tasks/trinity.receipts.verify.ex`: + +> Runs `Trinity.Receipts.Verifier` over a scope in the database or over an exported file +> (slice 024): +> +> mix trinity.receipts.verify --scope session: +> mix trinity.receipts.verify --file receipts.json +> +> Exit codes, the same vocabulary as `bin/verify_receipt.exs`: 0 verified, 1 invalid, +> 2 usage, 5 trust not established (a key id the registry does not know), 6 compromised key. + +## Exporting a scope first + +From `lib/mix/tasks/trinity.receipts.export.ex`: + +> Exports a scope for the standalone verifier (slice 024, AC7): +> +> mix trinity.receipts.export --scope session: --out receipts.json +> mix trinity.receipts.export --scope boot --out boot.json +> +> The file is `Trinity.Receipts.export/1`'s map: the rows in seq order, the checkpoints and the +> registry, so `bin/verify_receipt.exs` needs nothing else. Exit 2 on usage. + +## The exit vocabulary + +Both entry points use the same codes. Treat them as the finding, not the console text. + +| Code | Meaning | +|---|---| +| 0 | Verified | +| 1 | Invalid | +| 2 | Usage | +| 5 | Trust not established: a key id the registry does not know | +| 6 | Compromised key | + +Codes 5 and 6 are the interesting ones for an assessor. **5 is not a failure of the chain**; it says +the chain refers to a key the reader cannot place, which is a question about key distribution rather +than about the records. **6 is a refusal**: the registry marks that key compromised and the verifier +will not vouch for rows signed under it. + +## What the verifier actually checks + +`lib/trinity/receipts/verifier.ex`, per row, in order: + +1. `seq` is the previous plus one, and `prev_hash` is the previous row's `receipt_hash`. +2. The scheme is one the caller allowed, and the key id resolves in the registry. +3. The row's algorithm is the scheme's family, taken **from the registry row and never from the + receipt**. +4. The stored hash equals the hash of the pre-authentication encoding over the stored body. +5. The stored columns agree with the signed body, so a row edited after the fact is caught. +6. For a signed kind, the signature verifies under the registry's public key. + +Checkpoints are verified as well, so query receipts that are covered rather than individually +signed are still accounted for. + +## What a clean verification does and does not prove + +**Does prove:** the rows are in the order they claim, nothing was altered after signing, and each +signed row was signed by the key the registry names. + +**Does not prove:** that the host was honest. Standalone, the same process decided, acted and wrote +the record, so a clean chain is the host's own consistent account. `authorization-boundary.md` sets +this out, and `docs/10-assurance-case.md` C6 records that a key in a file proves nothing about +custody of that key. + +**Does not prove when.** The clock is the host's reading (`lib/trinity/receipts/clock.ex`). It is +signed and monotone within a chain, so the ordering cannot be rewritten; it is not a trusted +timestamp. + +## Scheme versions an assessor will meet + +Rows exist under two scheme versions and both verify. `receipt_v2_*` carries ten signed fields; +`receipt_v3_*` carries eleven, the extra one being the clock. A chain that spans the change verifies +row by row under each row's own scheme. The mapping to published constructs, including the three +fields that have no equivalent, is `docs/receipt-scheme-mapping.md`. + +That document also states, in terms, that Trinity issues SCITT **Signed Statements** and never SCITT +**Receipts**, because it runs no Transparency Service. No conformance to RFC 9943 is claimed. From 240f44b82ffcb9f564c09f61daadb6af81a09413 Mon Sep 17 00:00:00 2001 From: Ayla Croft Date: Sun, 27 Sep 2026 16:33:33 -0400 Subject: [PATCH 2/2] docs: state the authority adapter claim precisely "The adapter itself is NOT IN TREE" was too strong and it was wrong. An adapter does ship: Trinity.Authority.Local. Grepping the behaviour resolves two files, lib/trinity/authority/local.ex and a test fixture at test/support/authority/adapters.ex. Corrected to: the seam is in the tree, selection happens once at boot from TRINITY_AUTHORITY, one implementation ships and it is Local, and no adapter other than Local ships in this tree. An external authority adapter lives in another repository and nothing in mix.exs or mix.lock depends on one, which an assessor can confirm from the dependency list rather than from the page. Checked while correcting it: the only occurrence of the platform name anywhere in mix.exs, mix.lock, config/ or lib/ is the forbidden-name list inside the name check that exists to fail the build if such a name appears. Not a dependency. The parallel claim about the key seam holds unchanged: one implementation of Trinity.Keys, lib/trinity/keys/local.ex, so no KMS or HSM adapter is in the tree. README gains one sentence: this pack documents the gap; it does not close it. Signed-off-by: Ayla Croft --- docs/regulated/README.md | 2 ++ docs/regulated/authorization-boundary.md | 18 ++++++++++++++---- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/docs/regulated/README.md b/docs/regulated/README.md index d1b466e..0decb16 100644 --- a/docs/regulated/README.md +++ b/docs/regulated/README.md @@ -69,3 +69,5 @@ model prompt and what may not. Read both before the others. This directory is documentation only. It adds no code, changes no behaviour, and asserts no control that is not already in the tree. Where the tree does not implement something this pack names, the row says **NOT IN TREE** rather than describing an intention. + +This pack documents the gap; it does not close it. diff --git a/docs/regulated/authorization-boundary.md b/docs/regulated/authorization-boundary.md index b88a484..964d260 100644 --- a/docs/regulated/authorization-boundary.md +++ b/docs/regulated/authorization-boundary.md @@ -74,10 +74,20 @@ keeps **no executor** for the effects that adapter governs, and that absence is over the tree rather than by intent (`lib/trinity/authority.ex`, and the census recorded in slice 027's record). -The adapter itself is **NOT IN TREE**. ADR-0008 (`docs/adr/0008-authority-is-an-adapter.md`) states -that no slice in this repository builds one and that it is downstream work in a downstream -repository. An assessor evaluating a regulated deployment is evaluating that adapter as well as -this software, and this pack says nothing about it. +**Stated precisely, because the loose version of this sentence is wrong.** The seam is in the tree: +`lib/trinity/authority.ex` is the behaviour, and `lib/trinity/authority/selection.ex` picks an +implementation once at boot from `TRINITY_AUTHORITY` (ADR-0010). **One implementation ships here, +and only one:** `Trinity.Authority.Local` (`lib/trinity/authority/local.ex`), which is the sole +module in the tree that calls a tool's `execute/2` for an effectful tool. Grepping the behaviour +resolves two files and the second is a test fixture (`test/support/authority/adapters.ex`). + +**No adapter other than `Local` ships in this tree.** An external authority adapter is a module in +another repository, and ADR-0008 (`docs/adr/0008-authority-is-an-adapter.md`) states that no slice +in this repository builds one. Nothing in `mix.exs` or `mix.lock` depends on one; an assessor can +confirm that from the dependency list rather than from this page. + +An assessor evaluating a regulated deployment is evaluating that adapter as well as this software, +and this pack says nothing about it. ## What crosses the boundary, and when