All postsNext post

CameoDB v0.3.1: Secure by Default, With Paging and Exact Sorting

API keys with roles and per-index scope, HTTPS, posture profiles and an audit trail, plus paging and exact sorting for every caller.

Secure by Default

The 0.3 line is the release where CameoDB learns who is calling. A knowledge base that agents read from all day deserves the same care as any production database, so 0.3.0 brought authentication, encryption and an audit trail, and 0.3.1 rounds it out with paging, exact sorting and MCP tools that read like the questions they answer.

It starts with where a node listens. Out of the box it binds to 127.0.0.1, so a fresh node serves this machine and nothing else. To open it up, you declare how far it can be reached with a posture profile:

[node]
profile = "internal"   # local | internal | external

A profile asserts rather than rewrites. The node checks its configuration against the rules for that reach and starts only when the two agree. You can run the same check by hand, or in CI, before a config ever reaches a server:

cameodb check-config -c /etc/cameodb/cameodb.toml

It prints the posture matrix and exits non-zero on anything that does not hold, which makes it a natural gate in a deployment pipeline.

Keys That Know Their Place

Switch security on and every route except the health check asks for an Authorization: Bearer key. CameoDB mints them itself:

cameodb keygen --role reader

A key is cameo_v1_ followed by 256 bits of OS entropy. The command prints the key once and hands you the config entry that accepts it. The config holds only its SHA-256 digest, compared in constant time, so a config file on its own can never authenticate anyone.

[security]
enabled = true

[[security.api_keys]]
key_hash = "sha256:…"
role = "reader"            # admin | writer | reader
label = "research-agent"
allowed_indexes = ["docs", "wiki"]

Three roles bundle four capabilities: reader reads, writer reads and writes, and admin does everything. allowed_indexes narrows any role to the indexes you name, and that scope holds everywhere the key goes. Listing indexes, reading catalog totals and browsing MCP resources all show a scoped key only its own indexes, counts included.

The design detail we like most: authorization lives in one route table in front of the router, and it denies by default. No handler checks a key, so no handler can forget to. The tests read the router's own source and fail the build if a route has no row in the table.

Agents Under the Same Rules

An agent connects with the same key the HTTP API takes, and MCP is authorized per tool and per index, not just at the endpoint. tools/list advertises only what that key can call, and a session stays bound to the key that opened it.

Agents are busy callers, so 0.3.0 also gave each key a budget. [security.limits] runs a token bucket per key, shared across tools, which suits the bursty way agents work:

[security.limits]
tool_calls_per_minute = 120

A call over budget comes back as a regular tool error with a retry delay, so the agent knows exactly when to try again.

A Record of Who Read What

[security.audit] keeps a trail of access, and it is shaped for a knowledge base. Reads are recorded in detail: which key, which index, which tool. Writes, which arrive in far greater numbers, are folded into regular totals. The trail is there to answer the question an incident asks, who read which index, without burying it under ingest traffic. Recent records are served at /_admin/audit, and a file can keep the history.

Encrypted on Every Hop

The HTTP server speaks HTTPS through rustls. Point it at a certificate and a key, and both files are loaded and checked before storage starts, so a problem shows up at the first line of startup:

[network.http.tls]
enabled = true
cert_file = "/etc/cameodb/certs/cert.pem"
key_file  = "/etc/cameodb/certs/key.pem"

Between nodes, every connection is already encrypted with Noise. A cluster pre-shared key adds a private network on top, so only nodes that hold the key can join the mesh:

[network.cluster]
psk_file = "/etc/cameodb/cluster.psk"   # openssl rand -hex 32

The client follows the same habits. --api-key-file reads a key from disk, and the key travels in the client's default headers, so every call carries it.

Paging, Everywhere

0.3.1 adds offset to every search surface at once: both HTTP search routes, both MCP search tools, the client SDK, the CLI and the query grammar itself.

// inline, from the prompt or an agent
title:rust limit 10 offset 20

// in a request body
{"query": "science fiction", "limit": 10, "offset": 20}

// from the shell
cameodb search books 'title:rust' --limit 10 --offset 20

Under the hood, every shard and every index is asked for offset + limit hits from the front, and the page is cut once, after the merge. That is what keeps a page correct across a scatter-gather, where all ten hits on page three might come from a single shard. Every response reports the offset and limit it ran with beside total_hits, and a request past the last result returns an empty page with a note naming the last offset that holds a document.

Exact Sorting on Text

Declare a text or string field fast and CameoDB builds a sort column for it as documents are indexed. Sorting on that field is then a true alphabetical order over every match:

title:rust sort title:asc limit 20

Every field description now carries sortable beside searchable, so you and your agents can see which fields have the column before asking for an order. A sort on a text field without one is still answered, from the best-scoring candidates, and the response says so with _approximate_sort. Nothing about the order is left to guesswork.

Tools That Read Like Questions

The MCP tools now carry the names of the questions they answer:

search_across_indexes

One query over several indexes, merged into a single ranked page, with each hit tagged by its source.

describe_index

One index in full: its fields, their types, what each can do, and its numbers.

get_catalog_stats

Totals across every index the key can see.

validate_query also got a real upgrade: it runs the engine's own parser against the index. It reports whether the query parses, any syntax_errors with their position, the normalized_query the engine will actually run, and any clauses it would set aside. An agent can check its work before it searches.

One Description of an Index

Every surface that describes an index now returns the same shape, built once by the engine: GET /_indexes, GET /api/{index}/_config, the cluster listing and describe_index. Each field carries its name, type, indexed, stored, fast, searchable, sortable and its description. Because the listing now includes field types, cameodb list indexes gets everything it shows in a single request.

Try It

Every feature here is in the current release. Grab it from the downloads page, mint a key with cameodb keygen, and connect your agent with the snippets on the Build page. The Operate page covers profiles, keys and limits in more depth.