Skip to content

Your own detection service

Some data cannot be recognised by its shape or by a list of names. A client's number looks like any other number until someone looks it up in the customer database. For such data the guard (Donkit's security layer, described in Agent security) can ask a small web service of your own whether a text contains it. This page is for the engineers who build and run that service. Detectors that work by examples or by a list run on the platform and need none of this. The people who connect the service to the guard use Your own data on the Security page — see Your own data.

What the guard sends

The guard calls your service with POST, at the address entered on the Security page, with a JSON body: the texts to check, and how long it is willing to wait.

{
  "segments": [
    {"id": "s1", "text": "Contract CN-123456 was signed", "lang": null}
  ],
  "deadline_ms": 1600
}
  • Each entry of segments is one text. Its id means nothing to you: return it as the segment_id of everything you find in that text.
  • The texts are what the organization's rules need checked: the parts of a request to a model — the agent's instructions, the conversation, the results of tools — or the values in the arguments of a tool call. Expect personal data and secrets in them.
  • Nothing else leaves — no user, agent, organization, tool or rule information — except the token described below.
  • One call carries all the new texts of one action. A text longer than 32 KiB is cut at line boundaries, and each piece is a segment with an id the guard made up; offsets are into the piece you received.
  • lang is a hint: ru for a text written mostly in Cyrillic, null otherwise. Do not rely on it.
  • deadline_ms is 80% of the Response time set for your service, so 1600 for the default of 2000 ms.

What your service answers

{
  "spans": [
    {"segment_id": "s1", "start": 9, "end": 18, "entity_type": "CONTRACT", "score": 0.95}
  ]
}
  • start and end count Unicode code points, as a Python str index does, and end is exclusive. In 😀 CN-123456 the number starts at 2: the emoji is one code point, not two UTF-16 units or four bytes. A service written in a language that indexes strings differently converts before it answers. The guard drops a span that lies outside the text it names, so wrong offsets cost you findings, not errors.
  • entity_type is a name of your own for what you found. The person who connects the service maps each name to a kind of data and a subtype on the Security page; names that are not mapped are ignored. score is a confidence from 0 to 1.
  • Nothing found is an empty spans — and only when you did analyse the text.
  • A service that cannot answer says so: "failed": true and a short "error". Out of time, out of memory, a model that did not load — none of these is an empty spans, which the guard reads as "nothing found". The spans of a failed answer are ignored. Both fields may be left out when all went well.
  • Answer with status 200 (any 2xx is accepted), also when failed is true, and answer at the address you were given: redirects are not followed.
  • Answer without compression and within 1 MiB. The guard asks for Accept-Encoding: identity and refuses an answer that comes with any other Content-Encoding, so leave this endpoint out of a proxy that compresses on its own.
  • Fields the guard does not know are ignored, so you may add your own. GET /healthz is optional; the guard never calls it.

Keep the texts out of your logs

The texts can hold personal data and secrets. Do not log or store them unless your own policy says you may.

This page is the short version. The full contract, with every field, lives in the open-source core of the guard, donkit-guard: see Remote detectors, and the contract as an OpenAPI file. The path carries the contract's version, /v1/; changes that keep every conforming service working stay inside it.

The token

If the service needs one, an account owner or admin enters its token on the Security page. The platform stores it encrypted, and the guard sends it with every call as Authorization: Bearer <token>. Without a token the guard sends no Authorization header at all.

The token is bound to the address it was saved for — the scheme, host and port of the service's address — and the guard sends it to that address and to no other. When the service moves to another address, the token has to be saved again for the new one: until then the guard does not call the service there, and every check it would have made counts as a failed call.

Whether to require it is up to you: a service that does answers 401 to a call without a valid token, and the guard counts that as a failed call. Use a token of plain printable characters; one that cannot go into an HTTP header is refused before anything is sent. A new token takes effect within about half a minute, so to rotate it without a gap, accept both for a while.

Where the service may live

  • On Donkit's hosted platform the address must be https and public. The platform calls it from outside your network, so it refuses plain http, addresses on private networks (such as 10.x, 192.168.x, loopback or link-local) and names that resolve to them. The certificate has to be valid for the name and issued by a public authority.
  • On a closed / on-prem installation internal addresses can be allowed, http included, so that the service can run next to the platform. If the installation has its own certificate authority, its administrator makes the platform trust it.

An address the platform may not call is refused when the rules are saved, with the reason. Anyone can reach a public address, so protect it with the token above.

Load, speed and failures

  • When the guard asks. Only for actions where a rule could use what your service finds. For the rules the Security page makes, that is model requests when the finds are masked, tool calls when they are blocked, and both when they are only recorded. A model request carries the whole conversation each time, but the guard asks about each text once: an answer is reused for an hour, and a failed answer never is. So a call is made only for an action that brings new text, and it carries only that text. Busy agents call in parallel.
  • How fast. When the finds are masked or blocked, the request waits for your answer, for at most the Response time (from 100 to 30000 ms, 2000 by default). A fast service keeps agents fast. A service that knows it will not make it should answer failed: true instead of making the guard wait.
  • When a call fails. No answer in time, an error status, a redirect, a compressed or oversize answer, a body that does not follow the contract, and failed: true all count as a failed call, and so does a refusal on the platform's side — an address it may not call, a token that is not stored or is saved for another address. The choice under If the service does not answer then decides. Do not let the request through, the default, stops the request in Enforce mode, with the notice that the guard could not finish checking. Let it through and mark it unchecked lets it go on and marks the entry not checked in Decisions. A service that only feeds recording rules never stops anything; its failures are marked not checked too.

Check the service before you connect it

donkit-guard ships a check that your service keeps to the contract. It needs only the service's address:

pip install 'donkit-guard[http]'
python -m donkit_guard.conformance https://detector.example.com/v1/analyze

It prints OK and exits with 0, or prints one problem per line and exits with 1. A service that needs a token gets the name of the environment variable that holds it, as --token-env NAME.

The check sends an empty request, texts that begin with an emoji (offsets that do not count code points show up as spans that run past the end of their text), and a request with a one-second deadline, which must be answered, or failed, within a second and a half. It checks structure only: it cannot tell whether your service finds the right things.

Once it passes, connect the service on the Security page. Check connection calls it with a made-up sample text and lists the entity types it reports. Test on text runs it on a text you paste in; that text goes to your service, so use it for your own test cases.

An example to start from

examples/remote-detector in the repository of the core is a service to copy: a FastAPI application in one file that finds contract numbers such as ДГ-123456 and reports them as the entity type CONTRACT. It implements POST /v1/analyze and GET /healthz, answers failed: true when it runs out of time, and requires a bearer token when the environment variable DETECTOR_TOKEN is set. Replace its detection with your own and keep the rest.

What to hand to the person who connects it

The address, the token, the entity types your service reports with a word on what each one stands for, and the response time you can keep.