# AI privacy and control

> Run AI on your own hardware, control what may leave, and audit every AI action.

_Updated: 2026-07-21_

Most AI features are a data-export decision in disguise: your documents and
prompts go to someone else's servers, and you find out afterwards. Railbase is
built the other way round. The AI engine is part of the core binary on your
server, it can run a model on your own hardware, and every route to an outside
model is one you switch on deliberately.

## The AI can run entirely on your hardware

Two ways to run a model locally, both keeping the text on the machine.

**A local model server.** Point Railbase at any OpenAI-compatible server on
loopback or your LAN — Ollama, LM Studio, a `llama.cpp` server, or the bridge to
a Mac's built-in foundation model. In **Settings → AI → LLM** Railbase probes
well-known loopback ports and lists what it found under **Detected local
endpoints**, so a running server is one click to connect. Nothing is connected
for you. This is a provider of kind `local.endpoint`, and its address is
pinned: it must resolve to a loopback or private address. One pointed at a public
address is refused, so it can never quietly become a route to a cloud model.

**A model file executed inside the core process.** Railbase can load a native
inference library and a `.gguf` weights file and run generation *in its own
process* — no subprocess, no port, no network call at all. The **Local models
(local.native)** card on the same screen installs both; the download streams into
a content-addressed store and is checked fail-closed against the SHA-256 you
enter from the signed catalog entry, so a tampered or truncated file leaves
nothing installed — and an install with no hash is refused outright. The **This
host** card shows this machine's CPU, RAM, free disk and GPU, and the model tier
it can run.

> [!NOTE]
> The in-process runtime ships verified for **macOS**; on other platforms — and
> anywhere the library or weights aren't installed — it reports itself
> unavailable as a typed, degraded state the AI screen surfaces, rather than
> failing calls hard. A local model server is the portable local option today.

## Secrets are stripped on both boundaries

Redaction runs at two separate places, and neither depends on the other.

**Before anything is written to the audit trail.** The AI audit writer redacts
at the single point where rows are created, whatever a caller passes in: it masks
`Authorization`-style values, vendor API-key shapes (`sk-…`, `AKIA…`, `ghp_…`,
`xox…`), JWTs, passwords inside URLs, and any `key: value` fragment whose key
counts as sensitive. Structured context attached to a row is walked by key name
and blanked the same way. The reason is blunt: the trail is append-only and
tamper-evident, so a secret written into it could never be scrubbed out later.

**Before anything is sent to an external provider.** The same text-level filter
runs on the prompt and system text on the way out to a cloud model, so what the
audit row stores is what actually left your process. **Local providers get the
text unchanged** — a loopback endpoint and the in-process model never cross a
trust boundary, so their input passes through byte-identical.

On top of that, a payload carries a **sensitivity class**. `restricted` never
goes to an external model; `confidential` goes only if you raised the ceiling. An
unrecognised or unresolved class is **blocked**, not treated as ordinary data — a
false positive costs you a call, a false negative costs you a leak.

## What you control as an operator

**Settings → AI → Policies** holds the switches:

| Control | Setting key | Effect |
|---|---|---|
| External models allowed | `ai.external.allowed` | Off means no cloud provider is used at all |
| Sensitivity ceiling | `ai.external.sensitivity_ceiling` | Highest class permitted to leave: `normal`, `confidential`, `restricted` |
| Default provider | `ai.provider.default` | Which configured provider generation routes to |

A provider you connect with your own API key is **write-only** in the admin:
after saving, the screen shows only whether a key is set plus a short hint, never
the key. There is no reveal and no in-place edit, so rotating a key means
deleting the provider entry and connecting again.

## The AI audit trail is tamper-evident

Every AI call lands in an append-only `_ai_audit` row: who acted, which provider
and model, capability, token counts, the policy decision, the outcome, and the
redacted prompt and output. Each row also stores a SHA-256 of the raw prompt and
output — a correlator, so a row ties back to what was really sent without the raw
text ever being persisted.

The rows form a **hash chain**: each commits to the previous row's hash and to
its own contents. **Settings → AI → Audit** has a **Verify integrity** action
that recomputes the whole chain and reports how many rows verified, or marks the
chain broken — the response names the first row that fails to recompute. Editing
a row, deleting one, or reordering them all break verification. The same screen
exports the filtered stream as an `.xlsx` workbook.

Retention is set with `ai.audit.retention_days` (empty or `0` keeps everything).
When it's on, a nightly job **archives before it prunes**: the old prefix of the
chain is sealed into a JSON bundle under `<data dir>/ai_audit_archive`, and only
then are those rows removed. Each bundle records its own chain seed and anchor,
so it stays independently verifiable offline, and the live chain still verifies
from where the prune left off. If the archive write fails nothing is deleted — and if
the chain is *already* broken the prune refuses to run, so pruning can never hide
tampering.

> [!NOTE]
> This is the AI-specific trail. The core's general audit log is a separate
> append-only hash chain with its own verification — see
> [How Railbase is secured](trust-security).

## The AI acts under its own account

The AI is not a service key borrowed from a person. Each company gets exactly one
platform AI principal, stored in `_ai_agents` and created the first time that
agent is used in that company. When the agent executes an action it goes through
the ordinary request pipeline under **its own identity** — the same authentication,
role checks and audit a human request gets, resolving to the AI, not to a user.
Audit rows carry the actor type `ai`, so an AI action is never indistinguishable
from a person's after the fact. Disabling the agent pauses execution without
deleting the principal or its permission grants, so attribution survives.

## A module declares whether it may use an external model

A business module declares its AI needs in its bundle manifest, and the platform
enforces the declaration rather than trusting it:

- The declared `requires` list is a **closed set** — calling an AI capability
  outside it is denied.
- `external_allowed` defaults to **false**. When a module has not declared
  external use, cloud providers are **removed from the candidate list** for that
  module's calls — not warned about and allowed. Local backends still work.
- If such a module runs where the only configured provider is a cloud one, it
  gets a clear policy denial: add a local provider, or the module must declare
  the need.

## The only route to an external model is the AI engine

A module cannot bypass the redaction and policy layer by making its own HTTP
call. The core's outbound-HTTP capability for modules refuses foundation-model
API hosts outright — a built-in list covering the major vendors — and re-checks
on every redirect hop, so a harmless-looking URL cannot redirect into one. You
can extend that list for your own environment with `ai.egress.deny_hosts`
(**Settings → Network access**), which applies live. The block is one-sided by
design: it constrains a module's raw fetch, not the AI engine's own client — the
engine is the sanctioned channel precisely because it redacts and applies policy
first.

## FAQ

### Can I use AI with no internet access at all?

Yes, if you run a local model — a local server or the in-process model file.
Turning off `ai.external.allowed` means no cloud provider is used at all.

### If I use a cloud provider, what exactly leaves?

The prompt and system text after secret redaction, to the provider you configured
with your own key, and only if the payload's sensitivity class clears your
ceiling — `restricted` never does. The audit row stores that same redacted text,
so you can read afterwards what left. railbase.app is never in the AI path — it
handles the catalog, licences and signed artifacts only.

### Can someone quietly delete an AI action from the log?

Not without it showing. Deleting, editing or reordering a row breaks the hash
chain, and **Verify integrity** fails, naming the first row that no longer
recomputes.
