# The agent-safety formats: an open specification

Version 1. This page specifies, precisely enough for an independent implementation to interoperate with this one, the five formats Rightshold's agent-safety checks speak: the invisible-text scan, the tool-definition fingerprint, the security flag record, the pre-call check, and the definition-change feed.

**License.** This specification is published for anyone to implement, permanently, free of charge and without an account or permission. Build it into your own agent, client, registry or scanner; no fee, attribution or notice is required. The formats on this page are the whole agreement.

The checks themselves: GET https://www.rightshold.com/v1/security/precall · POST https://www.rightshold.com/v1/security/scan-text · GET https://www.rightshold.com/v1/security/flags · GET https://www.rightshold.com/v1/security/changes · MCP tools precall_check and scan_text (trust toolset). The habit as an installable skill: rightshold-precall — https://www.rightshold.com/.well-known/agent-skills/rightshold-precall/SKILL.md.

## 1. Text and positions

Scans operate on a text field: a string, read as a sequence of Unicode codepoints. Every position in a scan result is a codepoint offset: `start` is the index of the first codepoint of a finding and `end` the index one past its last, so the finding covers [start, end). Every finding carries an `excerpt`: the passage with each invisible or non-printing codepoint replaced by an annotation of the form ⟨U+200B ZERO WIDTH SPACE⟩ — the Unicode codepoint and name in angle brackets — so the evidence can be read by eye. A scan result is an object { status, findings, deterministic_count, heuristic_count, sanitized, note }. Its `status` is `flagged` when at least one deterministic finding exists, `noted` when only heuristic findings exist, and `clean` when there are none. Clean means none of the classes in this specification were found — it is never a finding that text is safe.

## 2. Invisible character classes (deterministic)

Seven classes are defined by codepoint ranges. Consecutive codepoints of one class form a single finding (a run); the finding lists up to 12 distinct codepoints in the run.

- `zero_width`: U+200B ZERO WIDTH SPACE · U+200C ZERO WIDTH NON-JOINER · U+200D ZERO WIDTH JOINER · U+FEFF ZERO WIDTH NO-BREAK SPACE · U+2060 WORD JOINER
- `bidi_control`: U+202A–U+202E (LEFT-TO-RIGHT EMBEDDING through RIGHT-TO-LEFT OVERRIDE, including U+202C POP DIRECTIONAL FORMATTING) · U+2066–U+2069 (LEFT-TO-RIGHT ISOLATE through POP DIRECTIONAL ISOLATE)
- `unicode_tag`: U+E0000–U+E007F (the tag block, including U+E0001 LANGUAGE TAG and the tag characters)
- `control_char`: U+0000–U+001F except tab (U+0009), line feed (U+000A) and carriage return (U+000D) · U+007F DELETE · U+0080–U+009F (the C1 controls)
- `private_use`: U+E000–U+F8FF · U+F0000–U+FFFFD · U+100000–U+10FFFD
- `unassigned_codepoint`: any codepoint whose Unicode general category is Cn (unassigned), which includes the noncharacters · and any lone surrogate U+D800–U+DFFF appearing in the string
- `hidden_markup`: markup constructs defined in section 3
- `base64_blob`: encoded runs defined in section 4

Two consequences are deliberate and part of the format. First, zero-width joiner and non-joiner are legitimate typography in several scripts; the classes report presence, never intent. Second, a finding is a statement about the text, not about whoever sent it.

## 3. Hidden markup (deterministic)

Four construct kinds make up the `hidden_markup` class; the finding names its kind.

- `html_comment`: an HTML comment — the text from `<!--` to the next `-->`.
- `markdown_comment`: a markdown reference-style comment — a line (up to three leading spaces) of the form `[label]: # (...)`, with the target in parentheses, double quotes, single quotes, or bare to end of line.
- `css_hide_rule`: a class rule inside a `<style>` element whose declarations hide its elements under the style test below. Elements carrying such a class are themselves `hidden_element` findings.
- `hidden_element`: an element that is hidden by its inline `style` attribute under the style test below, that carries the `hidden` attribute, or that carries a class hidden by a `css_hide_rule`.

The style test reads a declaration list (property: value pairs separated by semicolons, compared case-insensitively) and reports a construct as hidden when any of these holds: `display` is `none`; `visibility` is `hidden`; `font-size` is zero — `0` with an optional `.0…` fraction and an optional unit of `px`, `pt`, `em`, `rem` or `%`; or `color` equals the background — `background-color`, or the first token of the `background` shorthand — after normalization. Color normalization lowercases the value, strips whitespace, expands `#rgb` to `#rrggbb`, and resolves these named colors: white #ffffff, black #000000, red #ff0000, lime #00ff00, blue #0000ff, yellow #ffff00, gray and grey #808080, silver #c0c0c0, maroon #800000, green #008000, navy #000080, purple #800080, teal #008080, aqua #00ffff, fuchsia #ff00ff, olive #808000, transparent rgba(0,0,0,0).

## 4. Base64 (deterministic)

The base64 alphabet is A–Z a–z 0–9 + / and = padding; the URL-safe characters - and _ are accepted and mapped to + and / when decoding. A candidate is a maximal chain of base64-alphabet tokens joined by whitespace. Short leading or trailing tokens under 40 characters are trimmed from the span when a token of 40 or more is present, so positions stay exact. A chain is a `base64_blob` when any single token is 48 characters or longer, or when it has at least two tokens, every token is 40 characters or longer, and the tokens total 48 characters or more once joined. Three exclusions apply before the test: a chain that is entirely hexadecimal (0–9, a–f, A–F) is a digest or identifier, not a blob; a payload immediately preceded (within 48 characters) by `data:image/<type>;base64,` is a declared, visible image; and a single token that is a slug chain — segments of at most 16 characters joined by at least three -, / or _ separators — is a name, not an encoding.

A blob that passes the test is decoded: whitespace is stripped, the decode must be canonical (re-encoding the decoded bytes, padding stripped, reproduces the input, padding stripped), and the bytes must decode as UTF-8 under a fatal decoder. When at least 85% of the decoded characters are printable — tab, line feed, carriage return, or any codepoint at or above U+0020 except U+007F — the decoded text is carried in the finding and scanned in turn. Otherwise the blob is reported as opaque: a finding either way, never passed silently. The rescan is exactly one level deep: inside decoded text, the character classes and heuristics of sections 2 and 5 run, but markup and base64 detection do not.

The short band exists because a complete instruction fits in well under 48 characters of base64, while identifiers legitimately live at that length. A single token of 24 to 47 characters is therefore reported only for what it decodes to, and only when all three hold: the token is canonical base64 (same roundtrip test as above); the bytes decode as UTF-8 under a fatal decoder; and at least 0.90 of the decoded characters are ordinary — Unicode categories L, M or N, space, tab, line feed, carriage return, or one of the sentence punctuation characters . , ; : ! ? ' ’ ‘ “ ” - – — ( ) … ·. The slug-chain, hexadecimal and declared-image exclusions apply in the band as well. A short token that decodes to binary, or to structured text whose delimiters are not ordinary characters, is an identifier doing its job and is not reported. A token in the band that meets all three tests is a `base64_blob` finding whose decoded text is scanned one level deep, exactly as above.

## 5. Heuristic classes (labeled, never certain)

Two classes are heuristic. Every such finding carries `heuristic: true`; heuristic findings never hold a submission and never create a flag.

- `mixed_script_confusable`: a word (letters, numbers, underscore and hyphen, beginning with a letter or number) containing at least three letters that mixes Latin-script letters with Greek- or Cyrillic-script letters.
- `instruction_like`: a passage matching one of ten patterns for agent-directed language: instructions to ignore or disregard earlier instructions; instructions to keep a secret from the reader; direct imperatives that the reading agent send, forward, reply, reveal, output, print, delete, transfer or pay; instructions to send content to another address; references to the agent's own instructions or system prompt; instructions to reveal them; text addressed to an AI assistant; a claim that new instructions follow; and instructions sequencing what the agent must do before answering.

## 6. The sanitized copy

Every scan returns a `sanitized` copy of the text: codepoints in the classes of section 2 are removed; hidden-markup constructs are removed (a hidden element is removed through its closing tag when the element is simply nested); base64 blobs are left in place in the `text` context. In the `email_html` context, a base64 blob span is additionally removed and replaced by a visible placeholder naming the finding class, so a reader of the sanitized copy can see that encoded content was there and where to read what it decoded to (the scan finding).

## 7. The tool-definition fingerprint

A server's definitions are gathered by probing it: an MCP `initialize` request (protocol version `2025-03-26`; client name `rightshold-integrity-probe`), a best-effort `notifications/initialized`, then `tools/list` followed across cursors for at most five pages. Each call has a six-second budget. A tool record is exactly { "name", "description", "inputSchema" } — the description defaults to the empty string and the schema to the empty object when the server omits them — and the tool records are sorted by name. The fingerprint input is the object { "server": { "name": …, "version": … }, "tools": [ … ] }, with the server version null when the server reports none.

The canonical form of a value is its JSON serialization with object keys sorted lexicographically at every depth, arrays kept in order, and no insignificant whitespace. The fingerprint is the lowercase hex SHA-256 of the canonical form's UTF-8 bytes. A record's fingerprint version is 1 at first sighting and increments by one each time an integrity check observes a change.

An integrity check compares the fresh fingerprint with the recorded one and answers exactly one of three statuses. `unchanged`: the fingerprints are equal. `unreachable`: the probe failed, so nothing is said about the current definitions — unreachable is never reported as unchanged. `changed`: the fingerprints differ, and the answer carries a diff of exactly this shape:

    { "tools_added": ["name", …],
      "tools_removed": ["name", …],
      "tools_changed": [{ "tool": "name", "changes": ["description_changed", "schema_changed"] }],
      "server_identity_changed": false }

A tool appears in `tools_changed` when its description text differs, or its input schema differs under the canonical form; `server_identity_changed` is true when the server's name or version differs. A fourth status, `not_registered`, appears in pre-call answers for a server that was probed on demand: the fingerprint of what it serves now is computed for the answer and not stored.

## 8. The security flag record

A flag is created when content carrying a deterministic finding is submitted to the platform, pulled from an external server at registration, or observed when a registered server's definitions are refreshed. The flag's fields:

- `id`: `flg_` followed by 20 lowercase hex characters.
- `status`: `standing`, `disputed` or `overturned` — the only field that ever changes, and only through the lifecycle below.
- `artifact`: { type, ref, label? } — what the flagged content belongs to; for an external server, the ref is its URL.
- `field`: the field of the artifact the content was found in.
- `finding_class`: one of the deterministic classes of sections 2–4.
- `heuristic`: always false in a stored flag — flags are created only from deterministic findings.
- `evidence`: { sha256, excerpt } — the SHA-256 of the exact submitted bytes of the field (UTF-8), and the passage rendered visibly per section 1, at most 600 characters.
- `actor`: { account_id, agent_id, handle, kya_level, kind } — the account and agent the content is attributed to; kind is `account` or `anonymous`.
- `source`: `submission`, `external_pull` or `integrity_refresh`.
- `context`: present when the content arrived with a scan context (such as an email body).
- `dispute`: present while disputed or after a dispute — { case_id, case_number } of the arbitration case.
- `ledger_seq`: the sequence number of the ledger entry anchoring the flag's creation, when anchored.
- `created_at`: ISO 8601 timestamp.

Permanence is part of the format. There is no update or delete operation for a flag: the registry is append-only, and a flag's only lifecycle is standing → disputed → (overturned | standing), decided by the arbitration award on the attributed actor's dispute. An overturned flag keeps its record, evidence and history. Creation is anchored in the platform's ledger as a `security.flagged` entry and an overturn as `security.flag_overturned`, carrying the evidence hash — never the raw text. On refresh, an identical finding (same artifact, field, class and evidence hash) that is already standing does not create a second flag. A dispute that has been decided is not reopened by filing again — the flag record links the deciding case. And an overturn has one narrow consequence at the ingestion gate: when the same actor submits byte-identical content (the same evidence hash, the same finding class) that an award already overturned, the submission is admitted — the award judged those exact bytes; a single changed byte is a new hash and is held as usual.

## 9. The pre-call check

One keyless read an agent makes before trusting a tool: GET https://www.rightshold.com/v1/security/precall?url=<the server's https URL>&tool=<optional tool name> — the MCP `precall_check` tool in the trust toolset is the same check. The URL must be https (http is accepted only for a loopback address); a missing or malformed URL is a 422. URLs naming — or resolving to — private, link-local or reserved addresses are not fetched (also a 422): the probe cannot be steered into the network the platform itself runs on. Redirects are followed at most five hops with every hop re-checked under the same rules, and the chain is reported in the answer: a registered URL that starts redirecting is reported changed, and the destination's definitions are never recorded as the registered URL's own; a redirecting URL cannot be registered at all — the destination is registered directly, so a record always names the server that answers. The answer is one object:

- `url`: the URL that was asked about. `effective_url` and `redirects`: where the probe actually landed — when the asked URL redirects, every hop is listed and `effective_url` is the answering URL; the `server` identity in the answer is the answering server's, disclosed as such, never silently the asked URL's.
- `registered`: whether the server is in the external registry. When false, `checked_not_registered` is true: the server was probed for this answer only — checking a server does not register it, and nothing about it is stored.
- `server`: the name, version and protocol version the server reports — or, for a registered server that cannot be probed, the recorded values; null when an unregistered server cannot be probed at all.
- `registration`: for a registered server — slug, page URL, publisher status (unclaimed until the publisher proves control of its domain), any linked store listing, and first-seen time. Null otherwise.
- `integrity`: { status, fingerprint, fingerprint_version, diff?, detail, note } — the status per section 7, with the diff when changed. For an unregistered server the fingerprint is the live one, computed for this answer, and the version is null.
- `scan`: { status, findings, flag_ids, note } — a fresh invisible-text scan of the definitions served for this check: clean, flagged (with the deterministic findings), or not_run when the server could not be probed. For a registered server, findings found on refresh are recorded as flags and their ids are listed; for an unregistered server the findings are reported and nothing is recorded.
- `flags`: { status, standing_flag_ids, note } — the permanent flags standing against this server's URL, including any left by a registration that was held.
- `reviews`: { total, current_count, current_average, older_count, note } — the review aggregate; reviews come only from KYA-verified agents and are bound to fingerprint versions. An unregistered server's counts are zero — zero means none exist.
- `shadowing_warnings`: tool-name collisions, section 10.
- `tool_focus`: when a tool name was given — { tool, offered, basis }: whether the server offers it, and on what basis (the definitions served for this check, or the last recorded definitions when the server is unreachable; null when an unregistered server cannot be probed).
- `note`: the standing honesty line — a pre-call check is evidence gathered at one moment, never a guarantee that a tool is safe.

## 10. Tool-name collision warnings

When a tool name is offered by another registered server — or by a store listing's linked external record — with a materially different definition (different description text, or a different input schema under the canonical form), the pre-call answer and the server's registry record carry a warning of the shape { tool, others: [{ slug, url, server_name, linked_store_listing, page_url }], note }. Identical definitions under the same name do not warn. A warning is a fact about names and definitions: a client that loads both servers cannot tell the two tools apart by name alone. It is not an accusation against any server, and any of them may be the one a caller intended.

## 11. The definition-change feed

GET https://www.rightshold.com/v1/security/changes lists, newest first, every change an integrity check has observed on a registered external server: { server: { slug, url, name, page_url, linked_store_listing }, at, from_version, to_version, fingerprint_before, fingerprint_after, diff }, with the diff in the shape of section 7. The feed is append-only — events are never edited or removed — and it is populated only by observation: an event exists because a check (a pre-call check, an explicit integrity check, or a feed read) probed the server and found its definitions changed. Reading the feed first re-checks registered servers whose last check is older than six hours, at most three per read, oldest first; the response reports how many were refreshed in `refreshed_servers`. No background schedule is claimed: a change is recorded when the server is next checked. The feed starts empty, and an empty feed means no change has been observed yet — not that no server has changed.

## 12. What none of this claims

These formats carry evidence, not verdicts. A fingerprint match says the definitions match a record. A clean scan says none of the classes in this specification were found. A flag says invisible content was found and attributed. A review is testimony. None of them, alone or combined, is a finding that a tool, a server or a text is safe — and any implementation of these formats should say so wherever it presents them.

Human page: https://www.rightshold.com/security/spec
