Skip to content
Contextely

Trust boundary

The security model

One design decision carries this product, so it is worth stating precisely and being honest about its edges.

Entitlement is a factor in the score, not a filter on the answer

The common design for permissioned RAG retrieves broadly and asks a model to leave out what the reader should not see. That is not access control. A model that has read a salary can allude to it.

Contextely applies the asker's entitlement inside the ranking pass itself:

const entitled = isEntitled(asker, candidate.requiredScopes);
const entitlementFactor = entitled ? 1 : 0;

const score = relevance
            * (1 - W + W * freshness)
            * entitlementFactor;

// only score > 0 survives, then:
assertAllEntitled(ranked);   // throws rather than leaks
no redaction passno synthesis step ever holds itnot in a log line either

Why the ordering closes a side channel

A staleness refresh calls out to your system of record. Scoring runs before that call, so an unentitled asker never causes the record to be read on their behalf.

The other way round, the read is visible in your database logs and measurable in the response time, whether or not the answer ever came back.

The scope algebra

A scope is a colon-delimited path such as finance:payroll:uk, and a grant may end in * to cover a subtree. Requirements are a conjunction: an object requiring finance and exec needs both.

Owners and admins hold an implicit *. An unrecognised role holds nothing, so the model fails closed.

The refusals are written down

Every retrieval writes a row: who asked, how many candidates were considered, and how many were refused on entitlement grounds. That last number is a stored fact, not something reconstructed after an incident.

Keys are scoped to people, not workspaces

A key resolves to a member, and retrieval is scored against that member's entitlement, so an agent can never see more than the person its key belongs to. Only an owner or admin can mint a key that speaks as somebody else.

Source credentials

An API key is only ever compared, so it is stored as a SHA-256 hash and shown once. A source credential has to be replayed on every refresh, so it cannot be hashed, and the honest answer is encryption.

Source configuration is sealed with AES-256-GCM and bound to its workspace, so one cannot be moved to another tenant's row and still open. The key lives in the deployment environment, so a database dump carries ciphertext.

The dashboard never renders a stored credential back.

Encryption at rest protects the dump, not the process

Two things reduce the blast radius further, and both are worth doing.

Where the guarantee stops: passthrough reads

Everything above is about memory objects. There is a second, narrower path: an administrator can enable one of a connected system's own read tools so an agent can call it directly. The guarantee there is weaker.

Entitlement is a gate on the call, not a factor in a score. Contextely decides whether this asker may call at all, checks it twice, and writes a row either way.

What it cannot do is rank what comes back, because those records were never condensed. A passthrough answer is the reach of the credential your administrator connected, and the agent is told so.

Tenancy

There is no Row Level Security. DATABASE_URL is server-only, and the tenant boundary is an explicit workspace predicate on every query.

A test reads the query layer and fails the build if one loses its predicate. A convention nothing checks is a convention that erodes.

What this does not claim