diff --git a/packages/apollo-react/src/canvas/components/Guardrails/README.md b/packages/apollo-react/src/canvas/components/Guardrails/README.md
index 5548e02b4..3c441f70b 100644
--- a/packages/apollo-react/src/canvas/components/Guardrails/README.md
+++ b/packages/apollo-react/src/canvas/components/Guardrails/README.md
@@ -4,10 +4,150 @@ Shared UI for the UiPath Guardrails experience, consumed by Flow (flow-workbench
later stage, Agents (`frontend-sw`). Lives in apollo-react next to canvas — MUI-free, built
entirely on `@uipath/apollo-wind` primitives and its `forms/` engine, strings on lingui —
and is exported through the narrow `@uipath/apollo-react/canvas/guardrails` subpath (also
-re-exported from `./canvas`). Members: `GuardrailBuilder` (the whole Add/Edit screen),
+re-exported from `./canvas`). Members: the definitions layer (wire types, parser, canonical
+copy and `useGuardrailDefinitions`), `GuardrailBuilder` (the whole Add/Edit screen),
`GuardrailFormLayout` (the screen shell), and `GuardrailValidatorForm` (the validator
parameter section, also rendered inside the builder).
+## Definitions layer
+
+Turns the `GET /api/execution/guardrails/definitions` payload into the
+`GuardrailDefinition`s the builder renders. Three pure steps and one hook over them:
+
+```
+unknown payload → parseGuardrailDefinitions → enrichGuardrailDefinitions → GuardrailBuilder
+ (zod, private) (canonical copy on lingui)
+ useGuardrailDefinitions composes all three
+```
+
+```tsx
+import { useGuardrailDefinitions } from '@uipath/apollo-react/canvas/guardrails';
+
+const { definitions, invalid, loading, error, refetch } = useGuardrailDefinitions({
+ baseUrl: `/${orgName}/${tenantName}/agents_`, // omit for same-origin
+ tenantId,
+});
+```
+
+Three host shapes, all supported:
+
+| Host | Call |
+| --- | --- |
+| Owns no transport | `useGuardrailDefinitions({ baseUrl, tenantId })` |
+| Already has SWR or React Query | `useGuardrailDefinitions(null, { definitions: data })` |
+| Never fetches (Flow's vsix, over postMessage) | `useGuardrailDefinitions(null, { definitions: fromMessage })` |
+
+`options.definitions` wins over the context: when it is present no request is made at all, and
+the value is parsed and enriched instead. That is the seam that lets a product keep its own
+cache rather than adopting a second one, and it is why the hook stays a `useState` plus
+`fetch` plus `AbortController` (the `useDiscoveryModels` idiom) instead of a query library.
+
+### Contract
+
+- **The parser never throws.** `parseGuardrailDefinitions(unknown)` returns
+ `{ definitions, invalid, inputError? }`. A payload that is not an array sets `inputError`;
+ an individual definition that fails validation is dropped whole and listed in `invalid`,
+ which is what both products already do entry by entry. Unknown keys are stripped. Surface
+ `invalid` as a status banner, never as an error page: the other definitions are fine.
+- **Transport errors and data errors are different channels.** `error` is a failed request.
+ A malformed payload arrives through `invalid` / `inputError` with `error` still `null`.
+- **zod does not cross the boundary.** The schema is private to `definitions-parse.ts`;
+ `GuardrailDefinitionWire` is hand-written, and the two are pinned to each other by a
+ compile-time assignability check in `toWireDefinition` plus two tests (a key-set assertion
+ and a source-level import guard), so the emitted `.d.ts` for this folder carries no schema
+ types and consumers take no zod dependency.
+- **Enrichment is pure and exported.** `enrichGuardrailDefinitions(wire, { copy, hiddenValidators })`
+ is React-free, so non-React and bridge callers use it directly.
+ `EnrichedGuardrailDefinition extends GuardrailDefinition`, so its output feeds
+ `GuardrailBuilder` with no mapping.
+- **Context and `hiddenValidators` are compared by content, not identity**, so a host can
+ build them inline. (`useDiscoveryModels` compares the context by identity; an inline object
+ there refetches on every render and never settles.) `options.definitions` is the exception,
+ compared by identity because hashing a whole payload every render would cost more than it
+ saves: pass a stable reference (an SWR or react-query result already is).
+- **`loading` starts `true` when the hook is about to fetch**, so a host rendering
+ `loading ? : ` does not flash the empty state on first paint. It starts
+ `false` when the hook is disabled (`null` context, or `options.definitions` supplied), and
+ `refetch()` is a no-op in that state.
+- **A failed request keeps the previous results.** `error` is set and `definitions` still hold
+ the last good payload, so a transient 503 on a `refetch` does not empty a list the user is
+ looking at. Render on `error` first if you want it to replace the data. Disabling the hook
+ does clear the fetched state.
+- **`hiddenValidators` hides nothing by default** and never hides a BYO definition. Which
+ validators a product exposes is an entitlement decision, so it stays with the caller: Flow
+ passes `['prompt_injection']`, Agents passes nothing.
+- **BYO folder placement stays host-side.** Resolving it needs each product's connections API
+ (Agents pages `fetchResources`, Flow calls `getConnectionById`), so the hook does not reach
+ for it. Stamp the result on afterwards:
+
+ ```ts
+ const withFolders = withGuardrailFolderMetadata(definitions, (id) => connections.get(id));
+ ```
+
+### Canonical copy
+
+The display copy for the six built-in validators lives here, as lingui messages in the shared
+canvas catalog, rather than in each product's own table. Both products get the same wording,
+and the strings enter the real localization pipeline instead of a host-side constant.
+
+**English only, like every other string in this package.** The other thirteen catalogs get
+these ids from `chore(l10n): sync from Localization`, which appends new keys every week or
+two. Until it runs, `useSafeLingui` renders the English default, so nothing is missing on
+screen. Do not hand-write translations here.
+
+This narrows, deliberately, the rule the family shipped with in #1138: that domain copy never
+ships in this package. The rule still holds for copy this package cannot know, which is why
+wire copy wins at parameter level and a BYO definition takes no curated copy at all. What
+moved is the six validators both products had already transcribed by hand, where keeping two
+copies in sync is what produced `finNationalId` in one product and `fiNationalId` in the
+other. The components are unchanged: they still resolve nothing and render what they are
+handed, so a host that would rather keep its own table simply does not call
+`enrichGuardrailDefinitions`.
+
+Message ids use the raw wire values, never a transcribed slug:
+
+```
+guardrails.definitions..display-name | .description | .usage-note
+guardrails.definitions..param..label | .tooltip
+guardrails.definitions..option..
+```
+
+Transcribing is exactly how the two products ended up keying the same Finland entity as
+`finNationalId` and `fiNationalId`; `USSocialSecurityNumber` is the value we persist, so it is
+also the id.
+
+Copy precedence, unchanged from what both products already do:
+
+| level | non-BYO | BYO |
+| --- | --- | --- |
+| display name | curated, wire, `validator` | wire, `validator` |
+| description | curated, wire, `''` | wire, `''` |
+| usage note | curated only | none |
+| parameter label | **wire**, curated, humanized id | wire, humanized id |
+| parameter tooltip | **wire**, curated | wire |
+| option labels | curated merged under wire | wire only |
+
+Curated wins at definition level because that table is what product and localization review;
+wire wins at parameter level because a BYO manifest and a newly shipped backend parameter
+describe themselves. A BYO definition takes no curated copy at any level, even when its
+validator id collides with a UiPath one.
+
+Where the two products' English differed, the choice is declared with a reason in
+`definitions-parity.test.ts` (17 entries) and asserted against both products' transcribed
+copy in `__fixtures__/host-copy-baselines.ts`. That suite fails on an undeclared difference,
+a stale declaration, or a third wording we invented, so the table cannot quietly drift from
+the products it is meant to replace.
+
+`GUARDRAIL_COPY_EN` is the English table the pure layer defaults to;
+`GUARDRAIL_COPY_EN_MESSAGES` is the same copy flattened to id-to-English, exported so hosts
+can diff their remaining local tables against it in CI while they migrate off them.
+
+> `src/canvas` uses no lingui macros, so `lingui extract` does not feed this catalog: its
+> English entries are hand-authored. Two tests do what extraction would: every message reaches
+> `src/canvas/locales/en.json` with the same English, and no catalog keeps a
+> `guardrails.definitions.*` id the source has dropped. The second scans all fourteen files,
+> so a rename cannot leave the sync's translations behind as dead entries.
+
## GuardrailBuilder
The complete Add/Edit screen for an OOTB guardrail validator: status banners, usage note,
@@ -21,7 +161,7 @@ import { GuardrailBuilder } from '@uipath/apollo-react/canvas/guardrails';
=>
+ JSON.parse(readFileSync(join(localesDir, `${locale}.json`), 'utf8'));
+
+/**
+ * Ids the source declares that English does not carry, and ids whose English has drifted away
+ * from the source default. A drifted entry is the worse of the two: it translates, but into
+ * something the component never says.
+ */
+export function findCatalogDrift(messages: Readonly>): {
+ missing: string[];
+ drifted: string[];
+} {
+ const catalog = readCanvasCatalog('en');
+ const missing: string[] = [];
+ const drifted: string[] = [];
+ for (const [id, message] of Object.entries(messages)) {
+ if (!(id in catalog)) missing.push(id);
+ else if (catalog[id] !== message)
+ drifted.push(`${id}\n src: ${message}\n en: ${catalog[id]}`);
+ }
+
+ return { missing, drifted };
+}
+
+/**
+ * `locale: id` for every id under `prefix` that the source no longer declares, across all
+ * fourteen catalogs. A renamed id otherwise leaves the sync's translations behind as dead
+ * entries nothing will ever clean up.
+ */
+export function findCatalogOrphans(
+ messages: Readonly>,
+ prefix: string
+): string[] {
+ return CANVAS_LOCALES.flatMap((locale) =>
+ Object.keys(readCanvasCatalog(locale))
+ .filter((id) => id.startsWith(prefix))
+ .filter((id) => !(id in messages))
+ .map((id) => `${locale}: ${id}`)
+ );
+}
diff --git a/packages/apollo-react/src/canvas/components/Guardrails/__fixtures__/definitions-wire.fixtures.ts b/packages/apollo-react/src/canvas/components/Guardrails/__fixtures__/definitions-wire.fixtures.ts
new file mode 100644
index 000000000..2d41fba70
--- /dev/null
+++ b/packages/apollo-react/src/canvas/components/Guardrails/__fixtures__/definitions-wire.fixtures.ts
@@ -0,0 +1,246 @@
+import type { GuardrailDefinitionWire } from '../definitions-wire';
+
+/**
+ * Wire payloads shaped like what `GET /api/execution/guardrails/definitions` actually
+ * returns, trimmed to the fields the definitions layer reads. Kept close to the real
+ * payload (option lists, threshold bounds, BYO fields) so the enrichment tests exercise the
+ * same shapes the products see.
+ */
+
+/** The 25 PII entities the backend offers today. */
+export const PII_ENTITY_OPTIONS: string[] = [
+ 'Person',
+ 'Address',
+ 'Date',
+ 'PhoneNumber',
+ 'EugpsCoordinates',
+ 'Email',
+ 'CreditCardNumber',
+ 'InternationalBankingAccountNumber',
+ 'SwiftCode',
+ 'ABARoutingNumber',
+ 'USDriversLicenseNumber',
+ 'UKDriversLicenseNumber',
+ 'USIndividualTaxpayerIdentification',
+ 'UKUniqueTaxpayerNumber',
+ 'USBankAccountNumber',
+ 'USSocialSecurityNumber',
+ 'UsukPassportNumber',
+ 'NOIdentityNumber',
+ 'FINationalID',
+ 'FIPassportNumber',
+ 'SENationalID',
+ 'DKPersonalIdentificationNumber',
+ 'NLCitizensServiceNumber',
+ 'URL',
+ 'IPAddress',
+];
+
+export const PII_DETECTION_WIRE: GuardrailDefinitionWire = {
+ validator: 'pii_detection',
+ allowedScopes: ['Agent', 'Llm', 'Tool'],
+ status: 'Available',
+ parameters: [
+ {
+ id: 'entities',
+ type: 'enum-list',
+ required: true,
+ defaultValue: ['Email', 'CreditCardNumber'],
+ options: PII_ENTITY_OPTIONS,
+ },
+ {
+ // The backend omits the bounds here, and enrichment leaves them off: only `step` is
+ // defaulted. See the map-enum branch in `definitions-enrich.ts`.
+ id: 'entityThresholds',
+ type: 'map-enum',
+ required: false,
+ defaultValue: { Email: 0.8, CreditCardNumber: 0.8 },
+ keySource: 'entities',
+ },
+ ],
+};
+
+export const HARMFUL_CONTENT_WIRE: GuardrailDefinitionWire = {
+ validator: 'harmful_content',
+ allowedScopes: ['Llm', 'Tool'],
+ status: 'Available',
+ parameters: [
+ {
+ id: 'harmfulContentEntities',
+ type: 'enum-list',
+ required: true,
+ defaultValue: ['Hate', 'Violence'],
+ options: ['Hate', 'SelfHarm', 'Sexual', 'Violence'],
+ },
+ {
+ id: 'harmfulContentEntityThresholds',
+ type: 'map-enum',
+ required: false,
+ defaultValue: { Hate: 2, Violence: 2 },
+ keySource: 'harmfulContentEntities',
+ min: 0,
+ max: 6,
+ step: 2,
+ },
+ ],
+};
+
+export const PROMPT_INJECTION_WIRE: GuardrailDefinitionWire = {
+ validator: 'prompt_injection',
+ allowedScopes: ['Llm'],
+ status: 'Available',
+ parameters: [
+ {
+ id: 'threshold',
+ type: 'number',
+ required: true,
+ defaultValue: 0.7,
+ min: 0,
+ max: 1,
+ step: 0.1,
+ },
+ ],
+};
+
+export const USER_PROMPT_ATTACKS_WIRE: GuardrailDefinitionWire = {
+ validator: 'user_prompt_attacks',
+ allowedScopes: ['Llm'],
+ status: 'Available',
+ parameters: [],
+};
+
+export const INTELLECTUAL_PROPERTY_WIRE: GuardrailDefinitionWire = {
+ validator: 'intellectual_property',
+ allowedScopes: ['Llm', 'Tool'],
+ status: 'FeatureDisabled',
+ parameters: [
+ {
+ id: 'ipEntities',
+ type: 'enum-list',
+ required: true,
+ defaultValue: ['Text'],
+ options: ['Text', 'Code'],
+ },
+ ],
+};
+
+export const LLM_AS_JUDGE_WIRE: GuardrailDefinitionWire = {
+ validator: 'llm_as_judge',
+ allowedScopes: ['Agent', 'Llm', 'Tool'],
+ status: 'Available',
+ parameters: [
+ {
+ id: 'guardrailText',
+ type: 'text',
+ required: true,
+ defaultValue: null,
+ maxLength: 4000,
+ },
+ {
+ id: 'model',
+ type: 'enum',
+ required: true,
+ defaultValue: null,
+ options: ['gpt-4o-mini-2024-07-18', 'gpt-4o-2024-11-20'],
+ },
+ {
+ // Agents' schema has no `defaultValue` for text-list at all; this mirrors that.
+ id: 'positiveExamples',
+ type: 'text-list',
+ required: false,
+ maxItems: 5,
+ maxLength: 1000,
+ },
+ {
+ id: 'negativeExamples',
+ type: 'text-list',
+ required: false,
+ defaultValue: null,
+ maxItems: 5,
+ maxLength: 1000,
+ },
+ {
+ id: 'threshold',
+ type: 'number',
+ required: true,
+ defaultValue: 4,
+ min: 0,
+ max: 6,
+ step: 2,
+ },
+ ],
+};
+
+/** A bring-your-own guardrail: manifest copy only, no curated table entry applies. */
+export const BYO_WIRE: GuardrailDefinitionWire = {
+ validator: 'pii_detection',
+ allowedScopes: ['Llm'],
+ status: 'Available',
+ displayName: 'Acme PII scan',
+ description: 'Runs the Acme detector over prompts and completions.',
+ byoValidatorName: 'acme-pii',
+ byoConnectorName: 'Acme AI Guardrails',
+ byoConnectorKey: 'acme',
+ byoGuardrailConnectionId: 'conn-1',
+ byoConfigurationId: 'cfg-1',
+ parameters: [
+ {
+ id: 'sensitivity',
+ type: 'enum',
+ required: true,
+ defaultValue: 'medium',
+ displayName: 'Sensitivity',
+ description: 'How aggressively to flag.',
+ options: ['low', 'medium', 'high'],
+ optionLabels: { low: 'Low', high: 'High' },
+ },
+ ],
+};
+
+/** A UiPath validator the curated table has never heard of. */
+export const UNCURATED_WIRE: GuardrailDefinitionWire = {
+ validator: 'topic_drift',
+ allowedScopes: ['Llm'],
+ status: 'Available',
+ parameters: [
+ { id: 'maxDriftScore', type: 'number', required: true, defaultValue: 0.5 },
+ {
+ id: 'allowedTopics',
+ type: 'enum-list',
+ required: false,
+ defaultValue: [],
+ options: ['finance', 'legal'],
+ },
+ ],
+};
+
+export const ALL_BUILT_IN_WIRE: GuardrailDefinitionWire[] = [
+ PII_DETECTION_WIRE,
+ PROMPT_INJECTION_WIRE,
+ HARMFUL_CONTENT_WIRE,
+ USER_PROMPT_ATTACKS_WIRE,
+ INTELLECTUAL_PROPERTY_WIRE,
+ LLM_AS_JUDGE_WIRE,
+];
+
+/**
+ * A raw payload as it arrives over the wire: unknown keys the backend added, one entry that
+ * fails validation, and the empty display strings Flow's schema normalizes away.
+ */
+export const RAW_PAYLOAD_WITH_NOISE: unknown = [
+ {
+ ...PROMPT_INJECTION_WIRE,
+ // Fields a newer backend added that this package does not model.
+ executionStage: 'PreLlm',
+ internalRanking: 3,
+ displayName: '',
+ description: '',
+ },
+ {
+ // Missing `status`, so the whole definition is dropped.
+ validator: 'broken_validator',
+ allowedScopes: ['Llm'],
+ parameters: [],
+ },
+ USER_PROMPT_ATTACKS_WIRE,
+];
diff --git a/packages/apollo-react/src/canvas/components/Guardrails/__fixtures__/host-copy-baselines.ts b/packages/apollo-react/src/canvas/components/Guardrails/__fixtures__/host-copy-baselines.ts
new file mode 100644
index 000000000..4c50c3fa0
--- /dev/null
+++ b/packages/apollo-react/src/canvas/components/Guardrails/__fixtures__/host-copy-baselines.ts
@@ -0,0 +1,208 @@
+import type { GuardrailCopyTable } from '../definitions-copy';
+
+/**
+ * The English validator copy each product ships today, transcribed into this package's copy
+ * shape so `definitions-parity.test.ts` can diff all three tables mechanically.
+ *
+ * Sources, both read at the time this layer was written:
+ *
+ * - Agents `origin/main`:
+ * `frontend-sw/src/components/definition/AddGuardrailPalette/AddGuardrailPalette.utils.tsx`
+ * (`OOB_GUARDRAILS_I8N`; its `name` / `params[].infoTooltip` / `params[].options` map onto
+ * `displayName` / `paramTooltips` / `optionLabels` here)
+ * - Flow `origin/develop`:
+ * `packages/canvas/src/components/properties-panel/guardrails/ootb-guardrail-definitions.ts`
+ * (`buildValidatorDisplayInfo`)
+ *
+ * Only the English defaults are transcribed, never the products' message keys: the whole
+ * point of the shared table is that the keys stop mattering. When a product changes its copy,
+ * update the baseline here and the parity test will say whether our choice still holds.
+ */
+
+/**
+ * The 24 PII entity labels the two products spell identically. Shared rather than duplicated
+ * because they genuinely are the same strings; the one entity they disagree about
+ * (`FIPassportNumber`, Agents only) is added below.
+ */
+const SHARED_PII_ENTITY_LABELS: Record = {
+ Person: 'Person',
+ Address: 'Address',
+ Date: 'Date',
+ PhoneNumber: 'Phone Number',
+ EugpsCoordinates: 'EU GPS Coordinates',
+ Email: 'Email',
+ CreditCardNumber: 'Credit Card Number',
+ InternationalBankingAccountNumber: 'International Banking Account Number (IBAN)',
+ SwiftCode: 'SWIFT Code',
+ ABARoutingNumber: 'ABA Routing Number',
+ USDriversLicenseNumber: "US Driver's License Number",
+ UKDriversLicenseNumber: "UK Driver's License Number",
+ USIndividualTaxpayerIdentification: 'US Individual Taxpayer Identification Number (ITIN)',
+ UKUniqueTaxpayerNumber: 'UK Unique Taxpayer Number (UTR)',
+ USBankAccountNumber: 'US Bank Account Number',
+ USSocialSecurityNumber: 'US Social Security Number (SSN)',
+ UsukPassportNumber: 'US/UK Passport Number',
+ NOIdentityNumber: 'Norway Identity Number',
+ FINationalID: 'Finland National ID',
+ SENationalID: 'Sweden National ID',
+ DKPersonalIdentificationNumber: 'Danish Personal Identification Number',
+ NLCitizensServiceNumber: 'Netherlands Citizens Service Number',
+ URL: 'URL',
+ IPAddress: 'IP Address',
+};
+
+const SHARED_LLM_AS_JUDGE_TOOLTIPS: Record = {
+ guardrailText:
+ 'Describe the rule the judge will enforce. Be specific about what should pass and what should fail.',
+ model: 'The model used to evaluate the policy against each payload.',
+ positiveExamples:
+ 'Optional payloads that should pass the policy. Used by the judge as calibration anchors.',
+ negativeExamples:
+ 'Optional payloads that should fail the policy. Used by the judge as calibration anchors.',
+};
+
+/** Agents `frontend-sw`, `OOB_GUARDRAILS_I8N`. */
+export const AGENTS_COPY_EN: GuardrailCopyTable = {
+ pii_detection: {
+ displayName: 'PII detection',
+ description:
+ 'This validator is designed to detect personally identifiable information using Azure Cognitive Services',
+ paramLabels: {
+ entities: 'Entities to detect',
+ entityThresholds: 'Detection threshold',
+ },
+ paramTooltips: {
+ entityThresholds:
+ 'Value between 0 and 1. The sensitivity level for PII detection. Higher thresholds detect more potential PII but may result in more false positives.',
+ },
+ optionLabels: {
+ entities: { ...SHARED_PII_ENTITY_LABELS, FIPassportNumber: 'Finland Passport Number' },
+ },
+ },
+ prompt_injection: {
+ displayName: 'Prompt injection',
+ description:
+ 'This validator is provided by Noma Security and is built to detect malicious attack attempts (e.g. prompt injection, jailbreak) in LLM calls.',
+ paramLabels: { threshold: 'Detection threshold' },
+ paramTooltips: {
+ threshold:
+ 'Value between 0 and 1. The sensitivity level for Prompt Injection detection. Higher thresholds detect more potential Prompt Injection but may result in more false positives.',
+ },
+ },
+ harmful_content: {
+ displayName: 'Harmful content',
+ description:
+ 'This validator is provided by Microsoft Azure AI Content Safety and is built to detect harmful content (e.g. hate, violence, etc.) in LLM calls.',
+ paramLabels: {
+ harmfulContentEntities: 'Entities to detect',
+ harmfulContentEntityThresholds: 'Detection threshold',
+ },
+ paramTooltips: {
+ harmfulContentEntityThresholds:
+ 'Integer value between 0 and 6 (step 2). The severity threshold for harmful content detection. Higher values require more severe content before triggering.',
+ },
+ optionLabels: {
+ harmfulContentEntities: {
+ Hate: 'Hate',
+ SelfHarm: 'SelfHarm',
+ Sexual: 'Sexual',
+ Violence: 'Violence',
+ },
+ },
+ },
+ user_prompt_attacks: {
+ displayName: 'User prompt attacks',
+ description:
+ 'This validator is provided by Microsoft Azure AI Content Safety and is built to detect user prompt attacks (e.g. jailbreak, prompt injection) that attempt to bypass system instructions.',
+ paramLabels: {},
+ },
+ intellectual_property: {
+ displayName: 'Intellectual property',
+ description:
+ 'This validator is provided by Microsoft Azure AI Content Safety and is built to detect potential intellectual property violations in text and code.',
+ paramLabels: { ipEntities: 'Entities to detect' },
+ optionLabels: { ipEntities: { Text: 'Text', Code: 'Code' } },
+ },
+ llm_as_judge: {
+ displayName: 'LLM as Judge',
+ description: 'Detect violations of a rule you define, using an LLM as the judge.',
+ paramLabels: {
+ guardrailText: 'Rule prompt',
+ model: 'Judge model',
+ positiveExamples: 'Positive examples',
+ negativeExamples: 'Negative examples',
+ threshold: 'Threshold',
+ },
+ paramTooltips: {
+ ...SHARED_LLM_AS_JUDGE_TOOLTIPS,
+ threshold:
+ 'Integer value between 0 and 6 (step 2). Lower values are stricter — borderline payloads will fail. Higher values are more lenient — only clear violations are flagged.',
+ },
+ },
+};
+
+/** Flow `packages/canvas`, `buildValidatorDisplayInfo()`. */
+export const FLOW_COPY_EN: GuardrailCopyTable = {
+ pii_detection: {
+ displayName: 'PII detection',
+ description: 'Detect personally identifiable information using Azure Cognitive Services.',
+ paramLabels: {
+ entities: 'Entities to detect',
+ entityThresholds: 'Detection thresholds',
+ },
+ optionLabels: { entities: SHARED_PII_ENTITY_LABELS },
+ },
+ prompt_injection: {
+ displayName: 'Prompt injection',
+ description:
+ 'Detect malicious attack attempts (e.g. prompt injection, jailbreak) in LLM calls.',
+ paramLabels: { threshold: 'Detection threshold' },
+ },
+ harmful_content: {
+ displayName: 'Harmful content',
+ description: 'Detect harmful content (e.g. hate, violence) using Azure AI Content Safety.',
+ paramLabels: {
+ harmfulContentEntities: 'Content categories',
+ harmfulContentEntityThresholds: 'Severity thresholds',
+ },
+ optionLabels: {
+ harmfulContentEntities: {
+ Hate: 'Hate',
+ SelfHarm: 'Self-harm',
+ Sexual: 'Sexual',
+ Violence: 'Violence',
+ },
+ },
+ },
+ user_prompt_attacks: {
+ displayName: 'User prompt attacks',
+ description:
+ 'Detect user prompt attacks that attempt to bypass system instructions using Azure AI Content Safety.',
+ paramLabels: {},
+ },
+ intellectual_property: {
+ displayName: 'Intellectual property',
+ description:
+ 'Detect potential intellectual property violations in text and code using Azure AI Content Safety.',
+ paramLabels: { ipEntities: 'Content types' },
+ optionLabels: { ipEntities: { Text: 'Text', Code: 'Code' } },
+ },
+ llm_as_judge: {
+ displayName: 'LLM as Judge',
+ description: 'Detect violations of a rule you define, using an LLM as the judge.',
+ usageNote:
+ "Judge model calls consume Agent units the same way the agent's own LLM calls do. Apply selectively to keep cost predictable, and choose the judge model with cost in mind — rates vary by model.",
+ paramLabels: {
+ guardrailText: 'Rule prompt',
+ model: 'Judge model',
+ positiveExamples: 'Positive examples',
+ negativeExamples: 'Negative examples',
+ threshold: 'Strictness',
+ },
+ paramTooltips: {
+ ...SHARED_LLM_AS_JUDGE_TOOLTIPS,
+ threshold:
+ 'Strictness on a 0–6 scale. Lower values are stricter — the judge flags anything that hints at a violation. Higher values are more lenient — only clear, unambiguous violations are flagged.',
+ },
+ },
+};
diff --git a/packages/apollo-react/src/canvas/components/Guardrails/builder-types.ts b/packages/apollo-react/src/canvas/components/Guardrails/builder-types.ts
index c233c0854..3ab02181b 100644
--- a/packages/apollo-react/src/canvas/components/Guardrails/builder-types.ts
+++ b/packages/apollo-react/src/canvas/components/Guardrails/builder-types.ts
@@ -66,8 +66,10 @@ export type GuardrailDefinitionStatus =
/**
* The display-ready definition of an OOTB guardrail validator. `displayName` and `usageNote`
- * arrive pre-resolved (host-localized); `parameters` reuses the validator-form definition
- * type.
+ * arrive pre-resolved - either from the host's own table or from
+ * `enrichGuardrailDefinitions`, which resolves the built-in validators from the shared canvas
+ * catalog; the builder renders what it is handed either way. `parameters` reuses the
+ * validator-form definition type.
*/
export interface GuardrailDefinition {
validator: string;
@@ -75,7 +77,7 @@ export interface GuardrailDefinition {
allowedScopes: GuardrailScope[];
parameters: GuardrailParameterDefinition[];
status: GuardrailDefinitionStatus;
- /** Pre-localized informational note rendered above the form. */
+ /** Pre-resolved informational note rendered above the form. */
usageNote?: React.ReactNode;
/** Present for bring-your-own guardrail definitions; stamped onto saved values. */
byoValidatorName?: string;
diff --git a/packages/apollo-react/src/canvas/components/Guardrails/components/guardrail-chip.tsx b/packages/apollo-react/src/canvas/components/Guardrails/components/guardrail-chip.tsx
index 1ae27b88a..fd51a5574 100644
--- a/packages/apollo-react/src/canvas/components/Guardrails/components/guardrail-chip.tsx
+++ b/packages/apollo-react/src/canvas/components/Guardrails/components/guardrail-chip.tsx
@@ -2,15 +2,22 @@ import { cn, Toggle } from '@uipath/apollo-wind';
import { cva, type VariantProps } from 'class-variance-authority';
import * as React from 'react';
+/**
+ * Pill geometry, shared with the read-only `GuardrailStatusChip` so the interactive and
+ * label chips stay one system. h-6/px-2.5/text-xs is the compact chip scale.
+ */
+export const GUARDRAIL_CHIP_GEOMETRY =
+ 'min-w-0 gap-1 rounded-full border px-2.5 text-xs font-medium [&_svg]:size-3';
+
// `cn` is twMerge(clsx(...)), and a `future:`-prefixed class sits in a different merge group from
// its unprefixed counterpart — so Toggle's own `future:text-muted-foreground` and
// `future:data-[state=on]:text-foreground` survive alongside anything set here without a prefix,
// and win under a `.future-*` root. Every colour the chip overrides therefore needs a `future:`
// twin, or the pressed chip renders a brand fill with plain foreground text.
const guardrailChipVariants = cva(
- // Pill geometry over Toggle's base (which contributes the focus ring, disabled handling,
- // and data-[state] hooks). h-6/px-2.5/text-xs matches the compact chip scale.
- 'h-6 min-w-0 gap-1 rounded-full border px-2.5 text-xs font-medium [&_svg]:size-3',
+ // Geometry over Toggle's base, which contributes the focus ring, disabled handling and the
+ // data-[state] hooks.
+ `h-6 ${GUARDRAIL_CHIP_GEOMETRY}`,
{
variants: {
appearance: {
diff --git a/packages/apollo-react/src/canvas/components/Guardrails/components/guardrail-status-chip.stories.tsx b/packages/apollo-react/src/canvas/components/Guardrails/components/guardrail-status-chip.stories.tsx
new file mode 100644
index 000000000..584e8a7cc
--- /dev/null
+++ b/packages/apollo-react/src/canvas/components/Guardrails/components/guardrail-status-chip.stories.tsx
@@ -0,0 +1,67 @@
+import type { Meta, StoryObj } from '@storybook/react-vite';
+import { GuardrailStatusChip } from './guardrail-status-chip';
+
+const meta = {
+ title: 'Components/UiPath/Guardrail Status Chip',
+ component: GuardrailStatusChip,
+ parameters: {
+ layout: 'centered',
+ docs: {
+ description: {
+ component: `
+Read-only status label for a guardrail row: definition status in the palette, governance
+origin in the centralized section. It is a span carrying the wind badge classes, not a
+control, so it is safe inside a button and stays out of the tab order. Use
+\`GuardrailChip\` instead when the pill is meant to be toggled.
+ `,
+ },
+ },
+ },
+ tags: ['autodocs'],
+ args: { children: 'Governance managed' },
+} satisfies Meta;
+
+export default meta;
+type Story = StoryObj;
+
+export const Default: Story = {};
+
+/**
+ * One tone per definition status, plus the two both products already ship: green for a BYO
+ * origin or connector, blue for a "Preview" lifecycle label.
+ */
+export const Tones: Story = {
+ render: () => (
+
+ Governance managed
+ Preview
+ Bring your own
+ Feature disabled
+ Unauthorized
+
+ ),
+};
+
+/**
+ * Host text of any length: the label truncates rather than wrapping out of the pill, and the
+ * full text is on the chip's `title`.
+ */
+export const LongLabel: Story = {
+ render: () => (
+
+
+ Contoso Content Safety (EU West production)
+
+
+ ),
+};
+
+/** Valid inside a button, which is what the palette entry needs. */
+export const InsideAButton: Story = {
+ render: () => (
+
+ ),
+};
diff --git a/packages/apollo-react/src/canvas/components/Guardrails/components/guardrail-status-chip.test.tsx b/packages/apollo-react/src/canvas/components/Guardrails/components/guardrail-status-chip.test.tsx
new file mode 100644
index 000000000..82c93c836
--- /dev/null
+++ b/packages/apollo-react/src/canvas/components/Guardrails/components/guardrail-status-chip.test.tsx
@@ -0,0 +1,82 @@
+import { render, screen } from '@testing-library/react';
+import { axe } from 'jest-axe';
+import { createRef } from 'react';
+import { describe, expect, it } from 'vitest';
+import { GuardrailStatusChip } from './guardrail-status-chip';
+
+// The label lives in an inner span so it can truncate, so `getByText` returns that span rather
+// than the chip. Everything asserted here is on the chip itself.
+const chip = () => document.querySelector('[data-slot="guardrail-status-chip"]');
+
+describe('GuardrailStatusChip', () => {
+ it('renders a label, not a control', () => {
+ render(Governance managed);
+
+ expect(screen.getByText('Governance managed')).toBeInTheDocument();
+ expect(screen.queryByRole('button')).not.toBeInTheDocument();
+ });
+
+ it('carries the chip family geometry', () => {
+ render(Disabled);
+
+ expect(chip()).toHaveClass('rounded-full');
+ });
+
+ it('renders a span carrying the badge classes, not a div', () => {
+ // wind's `Badge` renders a `
`, and the palette entry puts these chips inside its
+ // `