> ## Documentation Index
> Fetch the complete documentation index at: https://docs.blindsight.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Policies & agent API

> Centrally‑managed AI host rules, per‑App configuration that flows to the agent, and the backend endpoints the agent calls.

The endpoint agent is a thin enforcement point. Every meaningful decision
is made centrally in your Blindsight workspace and pushed to the agent
via `/agent/policy`. This page covers what that policy looks like and
which backend endpoints the agent talks to.

## What policy controls

| Setting | What it does |
| - | - |
| **AI host rules** | Which hostnames the agent intercepts (e.g. `api.openai.com`, `api.anthropic.com`). |
| **App attribution** | Which Blindsight App each host's traffic is attributed to. |
| **Verdict actions** | Whether the agent enforces `block` locally or just records (observe‑only mode). |
| **TLS MITM scope** | Per‑host inclusion / exclusion. Banking, healthcare portals, etc. can be exempted. |
| **Redaction reveal** | Whether the user sees `<EMAIL>` markers locally or just gets the original. |
| **Tamper sensitivity** | What counts as a tamper event for this agent build. |

All of these are managed centrally. The agent never makes a local
policy decision; it fetches the latest version from the cloud.

### Where to edit policy

Policy comes from two places, and the difference matters.

| Where | What it sets |
| - | - |
| **Endpoint → Configurations → Protections** | Which of the three protections devices run: prompt injection detection, personal data protection and Shadow AI, and how each behaves. See [Protections](https://docs.blindsight.io/endpoint/protections.md). |
| **Endpoint → Configurations → Default policy** | The floor every device in the workspace starts from, such as fail mode. The unapproved AI posture is set under Shadow AI on Protections, and this page shows it read-only. |
| **Endpoint → Policies** | Named policies layered on top of that floor, each assigned to people, groups or specific devices. |

**Policies has its own page in the Endpoint menu.** It used to sit inside
Configurations alongside the settings; it is a thing you come to manage
rather than a setting you adjust once, so it now stands on its own. The
old link still works and lands on the new page.

Thresholds, custom phrases, custom PII rules and tool policy are the same
as SDK-mode Runtime Security and are edited on the App the agent is
attributed to (see [Apps](https://docs.blindsight.io/runtime-security/apps.md)).

> [!NOTE]
>
> **The default policy is a floor, not the final answer.** Policy
> resolution merges most-restrictively, so a named policy assigned to
> somebody can be stricter than the workspace default but never looser.
>
> Fail mode is where this is felt most often: if the default says *Send
> anyway* and an assigned policy says *Stop*, the people that policy
> reaches get *Stop*. The Fail mode row names any assigned policy that
> overrides it and links straight to it, so the screen cannot show one
> answer while a device enforces another.

## AI host rules

The agent only intercepts traffic to hosts on the AI host list. Every
other connection passes through untouched.

```json theme={null}
{
  "ai_hosts": [
    {"host": "api.openai.com",                "app_id": "app_abc...", "enforce": true},
    {"host": "api.anthropic.com",             "app_id": "app_abc...", "enforce": true},
    {"host": "generativelanguage.googleapis.com", "app_id": "app_abc...", "enforce": false},
    {"host": "api.perplexity.ai",             "app_id": "app_def...", "enforce": true}
  ],
  "exclude_hosts": [
    "online-banking.example.com"
  ]
}
```

| Field | What it means |
| - | - |
| `host` | Exact hostname or wildcard pattern (e.g. `*.openai.com`). |
| `app_id` | Which Blindsight App owns this traffic. Determines thresholds and event attribution. |
| `enforce` | `true`: agent blocks on `block` verdict. `false`: observe‑only. |
| `exclude_hosts` | Hostnames the agent never MITMs even if they match a rule above. |

### Built-in host pack

Blindsight maintains a default host pack covering every supported
provider. **Endpoint policy → Use default host pack** toggles it on; you
can layer your own overrides on top.

### Rule actions

Beyond which App owns a host, every rule carries an **action** that
decides how far the agent goes on that connection.

| Action | What the agent does |
| - | - |
| `inspect` | TLS MITM with the org root CA, scan, enforce the verdict. |
| `protect` | TLS MITM and scan, pseudonymize personal data before the prompt leaves the device, score and log injection, and **never block**. |
| `observe` | Record the destination hostname only. Feeds [Shadow AI](https://docs.blindsight.io/endpoint/shadow-ai/overview.md). |
| `tunnel` | Pass through opaque and record nothing. |
| `block` | Refuse the connection on the device. |

The discovery catalog is merged in as `observe` rules, so anything AI
that you have not onboarded still shows up as an inventory row without
ever being decrypted. Overrides layer on top of the defaults and the
catalog, and the most specific matching host wins.

When two assignments disagree about a host, the strongest action
wins, ranked `block` > `inspect` > `protect` > `tunnel`. `protect`
sits below `inspect` because both decrypt and scan, but `protect`
never blocks the person, so an assignment that says `inspect` must
not be softened by one that says `protect`.

## Verdict enforcement on the device

The agent runs the verdict logic locally for speed, but the **same
scoring path** as the cloud. Each intercepted request:

1. Is MITM'd locally with the org root CA.
2. Goes to `/agent/scan` for a verdict.
3. Is enforced according to the verdict:
   * `allow`, forwarded to the upstream provider untouched.
   * `redact`, PII spans replaced with category markers (`<EMAIL>`,
     `<API_KEY>`, ...) before forwarding.
   * `block`, returned to the calling app as a provider‑shaped error
     so the source application surfaces a refusal.

Observe‑only mode (`enforce=false`) records every verdict but never
blocks. Useful when piloting a tighter threshold on a new App.

## Backend endpoints (agent ↔ cloud)

The agent uses a small, dedicated set of endpoints under
`/api/runtime-security/agent/`. Same workspace, same scanners, same
event store as in‑app Runtime Security.

| Endpoint | What it does |
| - | - |
| `POST /api/runtime-security/agent/enroll` | Exchange the fleet enrollment token for a per‑device token. |
| `GET /api/runtime-security/agent/policy` | Fetch the current AI host rules + App attribution. |
| `POST /api/runtime-security/agent/scan` | Submit an intercepted prompt and get back a verdict. |
| `POST /api/runtime-security/agent/identity/start` | Begin the OIDC device‑code identity binding flow. |
| `GET /api/runtime-security/agent/identity/status` | Poll for completion of the binding. |
| `POST /api/runtime-security/agent/scan-file` | Submit an intercepted file attachment and get back a verdict. |
| `POST /api/runtime-security/agent/tamper-events` | Drain the local tamper log to the cloud. |
| `POST /api/runtime-security/agent/heartbeat` | Liveness ping. Drives the `Last seen` column on the fleet view. |
| `POST /api/runtime-security/agent/discovery` | Report the per-host rollup that feeds [Shadow AI](https://docs.blindsight.io/endpoint/shadow-ai/overview.md). |
| `GET` / `POST /api/runtime-security/agent/protection/pause` | Read and set the per-device protection pause. See [Devices](https://docs.blindsight.io/endpoint/devices.md#pausing-protection). |
| `GET` / `PUT` / `POST` / `DELETE /api/runtime-security/agent/rules[/{name}]` | Device-scoped detection rules. See [Device rules](#device-scoped-rules). |

All of these authenticate with the per-device token, sent as
`X-Blindsight-Device-Token`. They are not for dashboard clients.

These endpoints share Apps, thresholds, and the event log with
`/scan/input`, `/scan/output`, and the proxy routes. Dashboards do
not distinguish endpoint events from SDK events except via the `source`
attribute on the event row.

### Event source

Events from the endpoint agent carry:

* `source = "agent"`.
* `device_id = "<uuid>"` referencing the row in **Endpoint → Devices**.
* `user_id = "<directory_user>"` once identity is bound.
* `host = "api.openai.com"` (or whatever was intercepted).
* The usual `verdict`, `injection`, `pii`, `redacted_text`,
  `blocked_reason` fields.

Filter on `source=agent` in [Observability](https://docs.blindsight.io/runtime-security/observability.md)
to slice endpoint traffic separately.

### Direction and attachment on the event row

Every event says which way it was going, and, when it was a file,
what happened to that file.

| Field | Values | What it tells you |
| - | - | - |
| `direction` | `input` · `output` · `tool_call` · `mcp_verify` | Prompt, model reply, tool call, or an MCP tool-definition check. |
| `attachment` | An object, or `null` for a text prompt. | What the agent did with an uploaded file. |

> [!NOTE]
>
> The `direction` query parameter on
> `GET /api/runtime-security/events` accepts `input`, `output`, and
> `tool_call` only. `mcp_verify` rows are returned in unfiltered
> listings but cannot be selected with that filter.

The `attachment` object:

| Field | Values |
| - | - |
| `handling` | `pseudonymized` · `masked` · `blocked` · `clean` |
| `kind` | The extractor used (`pdf`, `docx`, `xlsx`, ...), or `unscanned` for a file that went out uninspected. |
| `filename` | string or `null`. |
| `extension` | Lower-cased, or `null`. |
| `media_type` | string or `null`. |
| `bytes_seen` | Size the agent saw. |
| `extracted_chars` | How much text came out of it. |
| `pages` | Page count, where the format has one. |
| `pseudonymized` | bool. |
| `injection` | `{label, score}` or `null`. |
| `redaction` | `{mode, redacted_count, new_mime}` or `null`. |

`handling` is the field to read for "what happened to this file":

| `handling` | Shown as | What it means |
| - | - | - |
| `pseudonymized` | pseudonymized | Values were swapped for realistic stand-ins before upload, and restored for the user afterwards. |
| `masked` | **redacted** | Values were removed before upload. Not recoverable. |
| `blocked` | blocked | The file was not uploaded at all. |
| `clean` | *(no chip)* | Nothing sensitive was found. |

> [!WARNING]
>
> This used to be invisible. The agent reported the outcome under a
> key the console never read, so an uploaded file's fate simply did
> not appear on the event row. Agents older than 1.0.24 do not
> populate `attachment`.

### Rows recorded without any text

Some rows record that traffic happened and keep no words. They have the
verdict `allow` and the status `local_only`, and they are not prompts,
gaps or blocks: the prompt that asked is its own row.

| Row | `scan_status_reason` | What it records |
| - | - | - |
| **Answer recorded** | `REPLY_OBSERVED` | A model's answer, read on the device as it streamed. One row can stand for several answers, and then reads "N answers recorded". |
| **File downloaded** | `FILE_DOWNLOADED` | A file the AI service sent to the device: its name, type and size. |
| **Refused by service** | `PROVIDER_REFUSED` | The AI service refused the request, such as through a content filter. The row carries the service's own code, never the prompt. |

For an answer or a download, the counts are the device's own detection.
The answer text and the file contents stay on the device and are not
sent to the platform.

### Files inside JSON bodies

The agent also reads files that travel inside a request instead of as
an upload: a file embedded in a JSON body (as base64, a data URL or a
list of bytes), and a file part with no name in a multipart body. A file embedded in a JSON body is scanned
like any upload when it is text, PDF, `.docx`, `.xlsx` or `.pptx`. A
file part with no name is scanned when its type is PDF, CSV, `.doc` or a
modern Office format (`.docx`, `.xlsx`, `.pptx`). The older `.xls` and
`.ppt` formats are not read.

Everything else passes to the service: images, audio such as a voice
note, archives and any other type the on-device reader cannot open. The
event is recorded as **Not inspected** with the file's name, type and
size and no content. The reason is `IMAGE_NOT_INSPECTED` for an image
and `EMBEDDED_FILE_NOT_INSPECTED` for the rest.

## Device-scoped rules

A person can carry detection rules that apply to **their machine
alone**, added from the desktop app's Rules tab. The workspace's own
rules are listed alongside them, read-only.

| Route | What it does |
| - | - |
| `GET /api/runtime-security/agent/rules` | The device's view: workspace rules, its own rules, its overrides, and what it may do. |
| `PUT /api/runtime-security/agent/rules/{name}` | Create or replace a rule on this device. |
| `POST /api/runtime-security/agent/rules/{name}/toggle` | Switch one on or off on this device. |
| `DELETE /api/runtime-security/agent/rules/{name}` | Remove this device's own rule. |

Body for `PUT`:

```json theme={null}
{
  "name": "internal-ticket-id",
  "pattern": "\\bTCK-\\d{5}\\b",
  "label": "Ticket id",
  "flags": "i",
  "enabled": true
}
```

| Field | Constraint |
| - | - |
| `name` | Starts with a letter, then letters, digits, spaces, `_` or `-`. Up to 64 characters. |
| `pattern` | A regular expression, up to 512 characters. It must compile, or you get a `422` carrying the reason. |
| `flags` | Any of `i`, `m`, `s`, `x`. |
| `enabled` | Defaults to `true`. |

A device may hold up to **50** rules. The 51st returns `422`.

### How device rules merge with the workspace's

Workspace rules are laid down first, then the device's rules are
merged **by name** on top. A new name adds a rule for that machine
only. An existing name replaces it, and `enabled: false` switches it
off, on that machine only.

> [!WARNING]
>
> **A personal rule cannot switch the company's detection off by
> default.** Before a device rule is allowed to shadow a workspace
> rule, or one of the agent's built-in detectors, the server checks
> that the person holds `endpoint.rules.override_workspace`. Without it the
> device rule is dropped and the workspace rule ships instead. Only
> admin holds that permission by default, and every use is audited.

The check is made from live state on every read, never from a flag
stored when the rule was written, and it requires all of:

* the device belongs to that workspace,
* its identity is `bound`, which only the SSO device-code flow sets,
* it is attached to a user, and that account is active,
* that user currently holds `endpoint.rules.override_workspace`.

The built-in detector names a device rule can collide with are
`email_address`, `ipv4_address`, `ipv6_address`, `us_ssn`,
`phone_us`, `credit_card`, `iban`, `github_pat`, `aws_access_key`,
`anthropic_key`, `openai_key`, `jwt_token`, `api_key_label`, and
`password_assignment`.

> [!NOTE]
>
> The collision check deliberately ignores `enabled`. A rule saved as
> enabled with a pattern that matches nothing removes a detector just
> as thoroughly as switching it off, so the gate is on the name, not
> the flag.

Admins can list device rules across the whole fleet with
`GET /api/custom-rules?scope=device` (needs `endpoint.view` or
`endpoint.manage`) and delete one with `endpoint.manage`. Authoring a device
rule from the console is not offered: they belong to the device.

## Keeping prompts on the machine

Injection scoring is the one reason prompt text leaves a device at
all. Personal data is matched and redacted **on** the endpoint, and
only what is left is sent to be scored. A workspace that will not
have prompts leave its people's computers can switch that scoring
off. Switch **Prompt injection detection** off in
[Protections](https://docs.blindsight.io/endpoint/protections.md).

| Route | Permission |
| - | - |
| `GET /api/runtime-security/dlp/protections` | `endpoint.view` |
| `PUT /api/runtime-security/dlp/protections` | `endpoint.protections.configure` |
| `GET` / `PUT /api/runtime-security/dlp/settings` | Kept for existing integrations; same permissions and the same refusals |

```bash theme={null}
curl -X PUT $API_BASE/api/runtime-security/dlp/settings \
  -H "Authorization: Bearer $TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"injection_detection": false}'
```

Response: `{"injection_detection": false}`. Sending no key at all is
a no-op that returns the current state.

This is a **workspace floor no named policy can override**, because
the permission that authors policies is a different and wider one.

> [!WARNING]
>
> It buys privacy with detection. Those devices stop detecting
> injection and jailbreak attempts entirely. `endpoint.protections.configure`
> is held by admin and `endpoint-admin`, is never implied by an older umbrella
> permission, and every change is audited at high severity.

With it off:

* Host rules are still enforced first, so a `block` destination is
  still refused. Banning a destination needs the hostname, never the
  prompt.
* Everything else is recorded with `scan_status = "local_only"` and
  no text at all. The text is dropped on the server rather than
  trusted to be absent, so an older agent that still sends the words
  cannot cause them to be stored.
* An unscored prompt is filed as **not scanned**, never as a clean
  pass, so the console cannot show a green result for something
  nobody looked at.
* A device still running the older policy after you switch scoring
  back on is recorded the same way, rather than scanned as an empty
  string and filed as an inspected allow.

## Editing policy safely

Policy changes propagate to every agent within \~30 seconds. To roll
out a tightening change safely:

**1. Note the current policy version**

The endpoint policy page shows the active version number. Bookmark it
in case you need to roll back.

**2. Apply the change to a pilot App**

Clone the App, apply the change to the clone, point a pilot
device group at the cloned App via **Endpoint policy → App
attribution**.

**3. Watch drift**

Use [drift](https://docs.blindsight.io/runtime-security/observability.md) on the cloned App
to see what shifts. PSI > 0.25 on `verdict` mix is your
canonical "something just changed" signal.

**4. Promote to the full fleet**

Apply the change to the production App and push policy from
**Endpoint → Devices** for an immediate refresh, or wait the
cache TTL.

> [!NOTE]
>
> **A change reaches a browser or tool that is already open.** Nothing has
> to be restarted for a new rule to take effect: the agent reloads its
> scope in place and closes the live connections the change affects, so
> the next request is decided by the new rule.
>
> The exception is a connection the agent cannot close, a flow held over
> IPv6, which has no close API, or one whose close the operating system
> refused. When that happens the app names the one program still holding
> it and offers to restart just that program. It is the residue, not the
> normal path.

> [!WARNING]
>
> Removing a hostname from `ai_hosts` stops the agent from MITMing
> it. Existing connections complete; new requests go straight to the
> upstream provider untouched. There's no grace period, confirm
> you've moved that traffic to a different App or that you really
> want it unscanned.

## Common workflows

> **Add a new LLM provider to coverage**
>
> 1. **Endpoint policy → AI hosts → Add.** Enter the provider's API
>    hostname.
> 2. Pick which App owns the traffic.
> 3. Toggle `enforce=true` once you're confident.

> **Exempt a sensitive internal site**
>
> 1. **Endpoint policy → Exclude hosts → Add.** Add the hostname.
> 2. The agent passes through it without MITM. Useful for sites
>    that pin certs or are otherwise sensitive to inspection.

> **Run observe‑only for a launch week**
>
> 1. Clone the production App into `staging_<original>`.
> 2. Flip every host rule to `enforce=false` on the clone.
> 3. Move pilot devices to the clone for a week. The verdict log
>    fills up without breaking anyone's workflow.
> 4. Review the verdict mix before flipping `enforce=true` on the
>    production App.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.