Putting an AI model in an authorization path sounds like a category error. Authorization should be deterministic, explainable, and conservative. Models are probabilistic, fallible, and dependent on the data and inference services they use.
That tension was the interesting part of integrating TypeSafe Jev with OpenTDF.
Jev is what TypeSafe calls a System One model. It evaluates structured state and returns typed answers: a choice, a score, or a Noul probability for a yes/no proposition. It does not generate an explanation that application code must parse. TypeSafe also makes an important qualification: calibration is measured across groups of predictions and does not guarantee that an individual answer is correct.
The design principle we landed on is:
Jev supplies a probabilistic judgment. Deterministic code limits how that judgment can affect access.
Why a model at all?
The obvious objection first: a request for 4,812 secret resources in one call is also catchable with a one-line volume rule. A deterministic rule is cheaper, faster, explainable, and does not send anything to a third party.
The experiment below shows that the integration responds differently to two examples. That is a wiring result. It does not establish that a model outperforms deterministic controls.
A defensible evaluation would compare rules and model judgments on legitimate bulk exports, suspicious small requests, and ambiguous cases. For example: a quarterly compliance export that should be allowed, or a small set of documents whose sensitivity depends on context beyond their count. We have not run that comparison.
What we can say is narrower: if a model is consulted to restrict an existing authorization decision, the integration should prevent its returned answer from expanding access. That is the property this post examines.
One request, four steps
1. Baseline policy decides
OpenTDF’s Policy Decision Point (PDP) evaluates access using
resolved entitlements and policy. Call the resulting permit set
policyPermits.
2. The model judges
The restrictor builds a small state object about the request as a whole and asks Jev a configured set of typed questions.
3. Deterministic code restricts
The model-facing component returns resources to deny. The caller applies those denials only to resources policy already permitted:
type DecisionRestrictor interface {
Deny(
context.Context,
RestrictionRequest,
) (map[string]string, error)
}
The return type cannot say “permit.” For this integration point:
finalPermits ⊆ policyPermits
Scope matters. The subset relationship applies to
the restrictor’s effect on the PDP result. With the Jev
entity-resolution integration disabled, it cannot feed
model-derived claims into that baseline. If enabled, those claims
can influence the entitlements that produce
policyPermits in the first place. That is a separate
threat model, discussed below.
The interface supports the guarantee; the caller enforces it. An in-process Go interface does not prevent arbitrary side effects or shared-state corruption. The precise claim concerns the returned value and the caller’s application of denials.
Deny-only does not mean harmless. A false negative misses a restriction. A false positive denies legitimate work. An unavailable model requires an explicit fail-open or fail-closed choice. Limiting privilege expansion guarantees neither detection nor availability.
4. Enforcement and audit
The restrictor evaluates and records every configured rule, even if an earlier rule already matched. Its observations include mode, certainty, and whether enforcement selected a denial. The caller then applies restrictions and reconciles the audit copies with the final decision. Shadow mode returns no additional denials.
These are different responsibilities: rule observations describe the model integration; the final decision record describes the result returned to the caller.
What leaves the platform
When an enabled integration invokes the model, it sends state to OpenRouter, which routes it to TypeSafe. This matters for Arkavo’s commitment to user control: classification attributes and entity claims can themselves be sensitive.
The implementation’s controls are specific:
-
A mandatory
state_allowlist. Only named top-level keys are included. An enabled client with an empty allowlist is a configuration error. -
A limited candidate state. The restrictor
offers
entity_id,action,resource_count,attribute_value_fqns, andpermitted_countto that allowlist. -
Two filters for entity resolution.
input_mappingselects JWT claims as parameters;state_allowlistfilters those parameters again. - Top-level filtering only. An allowed key carries its entire value, including nested maps. Use narrow, already-shaped values rather than whole claim sets.
- Response caching off by default. Reusing authorization inputs requires a separate decision about freshness and context.
Shadow mode still sends data. For the same input
and configuration, it builds the same outbound model request as
enforce mode: {model, state, questions}. It
suppresses the additional authorization effect, not the external
call.
The API key is read when the client is constructed, so rotating it requires a restart.
The live test
TestLiveRedactionPreventsEgress
checks that specified identifiers are absent from the redacted
state and performs a live smoke check. Its model-response
assertion is conditional on clearing a confidence threshold. A
model’s report cannot prove what crossed the network: the stronger
egress check must inspect the serialized outbound request.
The threshold trap
Our first documented confidence threshold was 0.95. It sounded appropriately conservative for authorization. For the tested inputs, it prevented the intended restriction from firing.
The threshold experiment used four allowlisted state keys:
action was read;
resource_count and permitted_count were
both 4812; and attribute_value_fqns contained a
single secret-classification value. No timestamp, role, or entity
identifier was included.
The Noul question was: “Does this access request look like bulk
data exfiltration rather than ordinary work?” The provider
reported typesafe/jev-1.13-20260917. Separate calls
at five thresholds produced:
| Threshold | Certainty observed | Denied? |
|---|---|---|
| 0.70 | 0.82 | Yes |
| 0.80 | 0.83 | Yes |
| 0.90 | 0.81 | No |
| 0.95 | 0.82 | No |
| 0.99 | 0.83 | No |
In these examples, 0.95 suppressed the added restriction while baseline policy still permitted the request. The current implementation records the below-threshold certainty; the lack of a denial does not mean the audit trail is empty. The default threshold is now 0.80.
These observations do not establish a ceiling on the model’s certainty, or a suitable production threshold. The companion routine case—one permitted confidential resource, with the same question—was recorded as a confident false at 0.96 certainty. The distinction between the two cases was in the answer, not simply in how certain it was.
Probability, confidence, and certainty
-
Choice and Score answers carry TypeSafe’s native
confidence, derived from their probability distributions. - Noul carries the probability that the proposition is true, without a separate confidence field.
-
Our integration derives Noul
certaintyasmax(p, 1-p), so confident-true and confident-false answers can use the same threshold mechanism.
That transformation does not make Noul certainty interchangeable with Choice or Score confidence. It also does not establish calibration, which requires labeled outcomes across many predictions. TypeSafe’s confidence documentation explains the distinction between probability and confidence.
Below-threshold behavior must be explicit. This restrictor leaves baseline policy unchanged when an answer does not qualify. A workflow that instead needs step-up verification, review, or denial must implement that response deliberately.
Typed does not mean “trust the network”
Jev’s question defines a constrained answer domain. A remote response still crosses a trust boundary.
Our integration validates decoded responses against the submitted questions: answer types must match, Choice values must belong to the configured options, probabilities must be finite and within the unit interval, and Score values must fit the configured scale. Typed outputs do not remove the application’s responsibility to validate what it receives.
Two integration failures
Component tests were green. Two problems survived them.
The entity-resolution mapper was tested but disconnected from
the production strategy path.
Unit tests exercised a component that the assembled service did
not reach as assumed. The fix includes
TestMultiStrategyService_JevProviderEndToEnd, which drives the service rather than just the mapper.
The obligation hook made one serial external call per resource. A fifty-resource decision meant fifty model round trips in the authorization path. The trigger now implements a batch interface: it evaluates the whole decision once, adding a triggered obligation to every resource while preserving resource-specific static obligations.
Both failures involved locally correct components joined by incorrect wiring. A green component suite cannot establish a cross-component security property. The most useful tests exercise the assembled decision path and reconcile its returned decision with the audit record.
The exception: entity resolution
The restrictor is deny-only and the obligation trigger is add-only. The entity-resolution service (ERS) integration is neither.
It turns model answers into derived claims. OpenTDF’s subject mappings evaluate entity representations to produce entitlements. A claim is not itself an entitlement, but it can influence a grant through policy. This integration can therefore increase access. The restrictor’s subset guarantee does not cover that change to its baseline.
Derived claims must use a
jev. prefix
to distinguish them from authoritative identity claims. Prefix
validation alone does not prove collision prevention; that also
depends on reserving the namespace across incoming claims and
merge behavior. Remote answers are validated against the question
domain, and user-influenceable inputs must be treated as hostile.
A high-value grant should not depend solely on a model-derived claim. Deterministic policy can faithfully grant access using an incorrect input. This integration warrants its own evaluation and write-up.
What we would deploy
Our default is still off. The next evaluation stage is shadow, with deliberate approval of the data being sent.
That evaluation needs to capture answer and certainty distributions, latency, cost, provider failures, and what alternative thresholds would have done. The current restrictor observations do not provide all of this: certainty is recorded before thresholding, but the interpreted answer is assigned only after it passes. Existing audit observations alone cannot reconstruct every counterfactual threshold decision. That instrumentation gap needs to be closed with appropriate data-handling controls.
False-positive and false-negative rates also require trustworthy labels: reviewed cases, confirmed incidents, or other adjudicated outcomes. Shadow mode does not create those labels.
Only after that evaluation would we enable enforcement. We prefer an additional verification obligation over denial when verification is sufficient. The Policy Enforcement Point (PEP) must actually perform the obligation before releasing access. OpenTDF’s PDP can report the directive, but cannot compel or independently verify its enforcement. OpenTDF’s obligation documentation makes that boundary explicit.
The decision restrictor narrows an existing permit set. The obligation trigger adds controls the PEP must enforce. Entity resolution can change the inputs used to grant access. Keeping those three roles separate is essential to stating what the integration guarantees.
Inspect and reproduce the experiment
Start with the model-facing restrictor and the caller that applies its denials. Reviewing the guarantee also requires configuration, failure handling, enforcement, and audit integration.
The
live restrictor tests
contain the exact state, allowlist, question, and rule.
TestLiveThresholdCalibration makes a separate call at
each configured threshold and logs certainty and denial. Despite
its historical name, this is a threshold experiment, not a
calibration study.
In a checkout of the pinned revision, configure
OPENROUTER_API_KEY in your environment. The live
tests require network access, incur provider charges, and skip
when the key is absent:
git checkout 83058aad72c017f96437b12a436f6e68defa349b
cd service
go test -tags jevlive -v \
./internal/jev/... \
./internal/access/v2/jevrestrictor/...
Version caveat: configuration requests
typesafe/jev-1.13; the provider reported the dated
model identifier above in the recorded runs. We did not explicitly
pin that dated identifier. The tests check that the returned model
name contains jev, so a later run may use a different
build.
The historical numbers are recorded in the threshold commit message. Earlier exploratory terminal output was not retained, and those measurements are excluded here. A future evaluation should retain sanitized inputs, outputs, configuration, and resolved model identifiers.
Re-run the experiment against your own questions rather than treating our table as a recommended threshold. Different results are findings to investigate.