Getting Started¶
Nautilus is a policy-first data broker built on Fathom. Define data sources
and routing rules in YAML, then call broker.request(...) to get
scope-enforced, attested results.
Installation¶
Requires Python 3.13 or later.
The broker, both transports and the adapters that need no driver (rest,
servicenow, llm, static) come with that. Database and object-store
drivers are extras:
uv add "nautilus-rkm[postgres]" # also: pgvector, elasticsearch, neo4j, influxdb, s3
uv add "nautilus-rkm[all]" # every built-in adapter
A configured source whose driver is not installed fails at startup, naming the
extra — it never fails later, mid-request. So does
session_store.backend: postgres, and analysis.provider.
Running the container image instead? Extras are chosen when the image is
built, because the runtime stage is distroless: no shell, no pip, nothing
to install into. The published image carries --extra otel and no drivers, so
anything on the list above is docker build --build-arg EXTRAS="--extra
postgres" rather than a config change. The full per-extra table is in
Deploying › Extras,
and every one of those startup failures prints the --build-arg line it wants.
First run¶
Nothing to configure, nothing to install beyond the package:
It declares two agent-to-agent handoffs and lets the broker decide each one:
analyst (confidential) hands confidential data to chief (secret)
handoff ALLOWED
chief (secret) hands secret data to intern (unclassified)
handoff DENIED
reason: receiving agent clearance does not dominate declared classification
rule: information-flow-violation
That is the whole product in miniature: a decision made by a rule, with the rule named, and an audit entry written for both outcomes. No database, no adapter, no network.
Configuration¶
writes a nautilus.yaml that runs as it stands. Its source has type static,
which serves rows declared in the config itself — so a first run needs no
database and no adapter code:
sources:
- id: orders
type: static
description: Sample order rows, served from this file.
classification: unclassified
data_types: [orders]
allowed_purposes: [support]
rows:
- {order_id: 1001, user_id: 42, total: 19.99}
- {order_id: 1002, user_id: 43, total: 7.50}
agents:
agent-alpha:
id: agent-alpha
clearance: confidential
allowed_purposes: [support]
attestation:
enabled: true
audit:
path: ./audit.jsonl
api:
keys:
- <generated per scaffold — read it out of the file, at mode 0600>
api.keys matters the moment you serve over HTTP. It defaults to empty,
and empty fails closed: /healthz answers 200 while /v1/sources and
/v1/request answer 401 Not authenticated. nautilus init generates a key
so the first nautilus serve works; replace it before anything but you can
reach the port. A config with no keys says so in the startup log.
For a real source, swap the static block for the type that matches your data
— postgres, elasticsearch, neo4j, rest, s3 and the rest are built in
— and point connection at it. To reach something with no built-in adapter,
nautilus adapters new scaffolds one; see the
Adapter SDK.
First request¶
Run this from the directory containing nautilus.yaml and demo_adapter.py:
from nautilus import Broker
with Broker.from_config("nautilus.yaml") as broker:
response = broker.request(
"agent-alpha",
"Find recent orders",
{"purpose": "support", "session_id": "s1"},
)
print(response.outcome) # "allowed"
print(response.data) # {"orders": [{"order_id": 1001, ...}, {"order_id": 1002, ...}]}
print(response.sources_queried) # ["orders"]
print(response.attestation_token)
The with block runs setup() on the way in — which is what creates the
schema for the persistent session stores — and closes the broker on the way
out. In async code the same shape is async with await Broker.afrom_config(...);
the two are genuinely different because close() refuses to run inside a
running event loop.
When a source is missing from response.data, the response says why:
response.denial_records carries the reason and the rule for anything
refused, response.skip_records for anything the intent never asked for, and
response.rule_trace is the rules that fired.
response is a BrokerResponse: response.data maps source IDs to result
rows, response.attestation_token is a signed JWS, and response.request_id
joins the response to its audit entry.
Note what the third argument is not doing: it names a purpose, not a
clearance. agent-alpha is declared in the config, so its clearance comes from
there. A config that declares no agents: takes the caller's word for both —
which the broker warns about at startup. See
Operator guide.
Next steps¶
- Architecture — understand the broker pipeline
- REST API — run Nautilus as a service
- Adapter SDK — build custom adapters