diff --git a/packages/apollo-react/package.json b/packages/apollo-react/package.json
index 7f6df523ea..d3de446ebe 100644
--- a/packages/apollo-react/package.json
+++ b/packages/apollo-react/package.json
@@ -123,6 +123,11 @@
"import": "./dist/canvas/constants.js",
"require": "./dist/canvas/constants.cjs"
},
+ "./canvas/guardrails": {
+ "types": "./dist/canvas/components/Guardrails/index.d.ts",
+ "import": "./dist/canvas/components/Guardrails/index.js",
+ "require": "./dist/canvas/components/Guardrails/index.cjs"
+ },
"./canvas/xyflow/*.css": "./dist/canvas/xyflow/*.css",
"./canvas/styles/*.css": "./dist/canvas/styles/*.css"
},
@@ -171,6 +176,7 @@
},
"dependencies": {
"@dnd-kit/core": "^6.3.1",
+ "@dnd-kit/modifiers": "^9.0.0",
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@emotion/cache": "^11.14.0",
@@ -211,6 +217,7 @@
"@uipath/apollo-wind": "workspace:*",
"@xyflow/react": "12.8.2",
"@xyflow/system": "0.0.66",
+ "class-variance-authority": "^0.7.1",
"d3-hierarchy": "^3.1.2",
"d3-sankey": "^0.12.3",
"d3-scale": "^4.0.2",
@@ -267,6 +274,7 @@
"@types/d3-scale-chromatic": "^3.0.3",
"@types/d3-selection": "^3.0.11",
"@types/d3-zoom": "^3.0.8",
+ "@types/jest-axe": "^3.5.9",
"@types/lodash": "^4.17.21",
"@types/luxon": "^3.7.1",
"@types/mdast": "^4.0.4",
@@ -284,6 +292,7 @@
"esbuild": "^0.28.1",
"glob": "^13.0.0",
"happy-dom": "^20.0.0",
+ "jest-axe": "^10.0.0",
"react": "19.2.3",
"react-dom": "19.2.3",
"typescript": "^5.9.3",
diff --git a/packages/apollo-react/rslib.config.ts b/packages/apollo-react/rslib.config.ts
index d1999eae4c..cfd4500446 100644
--- a/packages/apollo-react/rslib.config.ts
+++ b/packages/apollo-react/rslib.config.ts
@@ -55,6 +55,10 @@ export default defineConfig({
'!./src/**/*.test.{ts,tsx}',
'!./src/**/*.stories.{ts,tsx}',
'!./src/**/storybook-utils/**',
+ // Test-only, like `src/test/**`: fixtures are data for the suites, not API, and they
+ // reach for devDependencies (the dnd geometry helper needs `@testing-library/react`),
+ // so there is nothing to gain from publishing them.
+ '!./src/**/__fixtures__/**',
'!./src/test/**',
'!./src/icons/.cache',
'!./src/**/*.md',
diff --git a/packages/apollo-react/src/canvas/components/Guardrails/README.md b/packages/apollo-react/src/canvas/components/Guardrails/README.md
new file mode 100644
index 0000000000..b599e8ef29
--- /dev/null
+++ b/packages/apollo-react/src/canvas/components/Guardrails/README.md
@@ -0,0 +1,514 @@
+# Guardrails components
+
+Shared UI for the UiPath Guardrails experience, consumed by Flow (flow-workbench) and, in a
+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: the definitions layer (wire types, parser, canonical
+copy and `useGuardrailDefinitions`), `GuardrailList` (the applied-guardrails section),
+`GuardrailPalette` (the add-guardrail picker), `GuardrailBuilder` (the whole Add/Edit
+screen), `GuardrailFormLayout` (the screen shell), and `GuardrailValidatorForm` (the
+validator parameter section, also rendered inside the builder), plus the leaves the
+sections compose: `GuardrailStatusChip`, `GuardrailStatusBanner` and `MixedScopesBanner`.
+
+## 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, a runtime key-set assertion and a source-level 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.)
+- **`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 same translations, and the strings sit in the real localization pipeline.
+
+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
+> entries are hand-authored. A test asserts every message reaches `src/canvas/locales/en.json`
+> with the same English, and that the catalog carries no `guardrails.definitions.*` id the
+> source no longer declares. That test is what extraction would otherwise be doing for you.
+
+## GuardrailList
+
+The guardrails applied to an agent or a tool: an ordered list of rows with add, edit, remove
+and reorder affordances, the two bring-your-own configuration notices, and optional status
+chips.
+
+```tsx
+import { GuardrailList } from '@uipath/apollo-react/canvas/guardrails';
+
+ persist(spliceBack(reordered, move))}
+/>;
+```
+
+### Contract
+
+- **The host filters, the list renders.** Feature flags, entitlements and scope filtering
+ never cross this boundary: both products decide what a user may see, then pass the
+ survivors as `guardrails` and `definitions`. The list renders every row it is given, even
+ one whose definition never arrived.
+- **Callbacks are intents.** `onRemove` reports the request; the confirmation dialog, the
+ scoped-removal unwind and the write stay host-side, because the two products confirm with
+ different copy and unwind tool-scoped guardrails differently. Same for `onAdd` and
+ `onEdit`. No telemetry ships in the package: hosts wrap their own callbacks.
+- **`onReorder` reports the visible array plus the move** (`{ from, to, id }`), so a host
+ rendering a filtered view (Flow's per-tool view over an agent-level list) can splice the
+ result back without this component knowing a fuller list exists.
+- **`definitions` is read for three things only**: the provider line, the two BYO notices and
+ the status chip. Rows resolve by validator id, and a BYO row by its validator name alone,
+ which is the rule both products already ship (the name is unique per tenant, so an admin
+ rebinding a configuration to another connection still resolves).
+ `resolveGuardrailListItemState` is exported for hosts needing the same answer outside a row.
+ The type is the minimum the list reads, so `useGuardrailDefinitions`' output feeds it
+ unchanged, and so does a product's own definition type.
+- **The BYO notices keep the "definitions have loaded" guard.** While the array is empty no
+ row claims its configuration is gone, which is what both products already do; without it
+ every BYO row flashes the notice for as long as the catalog takes to load.
+- **Every addition is opt-in**, so adopting the list behind a flag renders what the host
+ renders today and the new UI arrives deliberately:
+
+ | prop | default | why |
+ | --- | --- | --- |
+ | `statusChips` | `false` | Neither product chips definition status or administration today. The two BYO notices are *not* gated: both products already show those, and a guardrail that cannot run is not an opt-in detail. |
+ | `previewChip` | `false` | Product lifecycle, not a package concern. Both hosts pass it today and drop it at GA without a release here. |
+ | `reorderDisabled` | `disabled` | Agents stops reordering in a read-only list and Flow does not, so one flag could not express both. Flow passes `false`. |
+ | `unstyled` | `false` | Agents renders inside its own section accordion, where the card border and padding are extra chrome. |
+ | `hideHeader` | `false` | Agents owns the section title and its add affordance. |
+ | `footer` | none | Agents' add affordance sits *below* the rows and swaps itself for an entitlement line, which the header-only `addSlot` cannot express. |
+ | `emptyState` | the default line | Agents renders nothing when empty: pass `null`. An explicit `null` is honoured, so the check is for an absent prop, not a falsy value. |
+ | `rowActivatesEdit` | `false` | Agents opens the editor by clicking the row body. |
+ | `renderItemActions` | inline buttons | Agents' actions are an overflow menu. The slot receives `defaultActions`, so it can add to them instead of replacing them. |
+ | `renderRowTooltip` | none | Agents hovers a combined description, provider and scopes tooltip over the row body. |
+ | `formatScopes` / `formatAction` | raw values | Scope and action wording is product copy; return `null` to hide either line. |
+ | `getItemId` | `id ?? name` | Flow keys rows by `id`, Agents by `name`. |
+ | `getItemAdministration` | `'local'` | Governance-managed guardrails come from a different endpoint, so nothing on the record identifies them. |
+
+ So Flow passes `previewChip` and `reorderDisabled={false}`; Agents passes `previewChip`,
+ `unstyled`, `hideHeader`, `emptyState={null}`, `footer`, `rowActivatesEdit`,
+ `formatAction={() => null}`, `formatScopes`, `renderItemActions` and `renderRowTooltip`.
+
+- **Reorder is real dnd-kit, and keyboard operable.** Pointer and keyboard sensors, vertical
+ and parent-bound modifiers, and a drag handle that is a real button: Agents' handle today is
+ an `aria-hidden` icon carrying the listeners, so it cannot be reached from the keyboard.
+ That is a fix, not a regression. With reorder off, or with a single row, no drag machinery
+ is mounted at all.
+- `renderRowTooltip` brings its own `TooltipProvider`, because a row renders where none is
+ guaranteed. A tooltip anchored to a row body that is not activatable is pointer-only, so
+ anything a keyboard user needs belongs in the row itself.
+
+### Chips and notices
+
+`GuardrailStatusChip` is `Badge`-based and reuses the chip family's pill geometry. It is
+deliberately **not** `GuardrailChip`, which wraps a Radix `Toggle`: these are read-only
+labels, and rendering them as toggles would put fake buttons in the tab order and misreport
+them to screen readers.
+
+`getGuardrailListChips` is the mapping, exported and pure. An `Available`, locally
+administered row chips nothing, which is the common case and keeps the list quiet.
+`FeatureDisabled` and `Unauthorised` chip as warnings; `Disabled` and the synthetic
+`Unavailable` (a BYO row whose definition stopped resolving) chip as errors.
+
+The administration axis is deliberately **not** called "origin": both products already use
+that word for BYO versus UiPath-managed validators (`GuardrailOriginChip`,
+`CentralizedGuardrailOriginBadge`), which is a different axis and travels on
+`byoValidatorName`. Governance-managed guardrails still render in each product's separate
+centralized section; this ships the chip and the seam, so either layout stays possible.
+
+`GuardrailStatusBanner` and `MixedScopesBanner` are exported for hosts composing their own
+chrome. The list itself takes one `statusBanner` slot above the rows (a definitions load
+failure, say) rather than a typed banner prop: the mixed-scopes banner ends in a "Save as
+new" hint, which is builder copy, and both products render it inside a builder rather than
+over a list.
+
+### Localization
+
+Chrome strings resolve through `useGuardrailListLabels` (lingui, `guardrails.list.*` ids in
+the shared canvas catalog); `labels` overrides any of them and wins over the catalog. Twelve
+of the seventeen are harvested from Flow's canvas catalog into all 12 translated locales, so
+the list adds no translation work. The five with no host equivalent are English-only for now
+and fall back per key: `edit-row`, `status-feature-disabled`, `status-disabled`,
+`status-unavailable`, `administration-governance`.
+
+## GuardrailPalette
+
+The add-guardrail picker: the definitions a user may add, grouped, with an optional
+create-custom entry.
+
+```tsx
+import { GuardrailPalette } from '@uipath/apollo-react/canvas/guardrails';
+
+const { definitions, loading, error } = useGuardrailDefinitions({ baseUrl, tenantId });
+
+ ;
+```
+
+### Contract
+
+- **The host filters, the palette offers.** Feature flags, entitlements, `FeatureDisabled` /
+ `Disabled` removal and Tool-scope filtering never cross this boundary: both products
+ already filter before rendering, and both do it differently (Agents gates five per-validator
+ flags, Flow one `canvas.guardrails` flag plus a hidden-validator list). The palette offers
+ every definition it is given.
+- **Both callbacks are intents.** `onSelectOotb` reports the choice; the builder that opens
+ next, the unique default name it starts with and the telemetry stay host-side. No telemetry
+ ships in the package: the two products' event taxonomies do not overlap
+ (`guardrails.create_ootb_clicked` versus `GUARDRAILS_CREATE_OUT_OF_THE_BOX_CLICKED`) and
+ wrapping the callbacks is what both adapters do anyway.
+- **`onCreateCustom` is opt-in by presence, not by a boolean.** Flow offers custom guardrails
+ only for `scope === 'Tool'`, so it passes the prop conditionally. Agents keeps the affordance
+ in its own palette header, so it omits the prop and the picker renders no second one.
+- **Grouping is the rule both products already ship** (`groupGuardrailsForPalette`, exported
+ and pure): with no bring-your-own definitions, one unheaded group in payload order;
+ otherwise one group per BYO `folderPath ?? byoConnectorName`, sorted by that key, then a
+ trailing UiPath group, with display-name sorting inside every group. One deliberate
+ difference: an empty catalog produces **no** groups rather than one empty group, which is
+ what makes the empty line reachable. Flow's own empty state is guarded on
+ `groups.length === 0` and its implementation can never return that.
+- **Entry identity is `validator`, or `byoValidatorName:byoGuardrailConnectionId`**
+ (`getGuardrailPaletteItemId`). A BYO validator name is unique per *connection*, so two
+ connections can expose the same name; Flow keys BYO entries by name alone today and collides
+ in exactly that case. This is not the same question as `matchesGuardrailListDefinition`,
+ which resolves a *saved* guardrail and matches BYO on the name alone on purpose.
+- **`Unauthorised` is offered, chipped and not choosable.** It is the only non-`Available`
+ status that reaches a correctly filtered palette, and it is how a tenant discovers a
+ validator it is not entitled to. The entry is `aria-disabled`, not `disabled`, so it keeps
+ its place in the tab order and a keyboard user reaches the chip that says why. Agents today
+ lets that entry through to the builder, which then refuses to save; Flow disables it in the
+ select, and that is the behaviour this ships.
+- **`previewChip` defaults to `false`**, the same call as the list: product lifecycle is not a
+ package concern. Both hosts hardcode the chip today and both pass the prop, then drop it at
+ GA without a release here.
+- **`isLoading` renders a polite loading line and `error` a `GuardrailStatusBanner`.** Any
+ definitions that did arrive stay pickable under the banner, which is what a host with a
+ stale cache and a failed revalidation wants. `error` is shaped like
+ `useGuardrailDefinitions`' own `error`, so the hook's result destructures straight in.
+- **Definitions are generic.** `GuardrailPaletteDefinition` is the eight fields the palette
+ reads; `EnrichedGuardrailDefinition` satisfies it, and so does a product's own definition
+ type. The component is generic over it, so `onSelectOotb` hands back the object the host
+ passed in, `parameters` and all, and there is nothing to look up again.
+
+### What the palette is not
+
+Only the picker ships. The shell is host orchestration, because the two products disagree and
+both are right for their surface: Flow opens a 500px dialog, or an inline properties-panel
+overlay that renders the chosen builder underneath the picker; Agents takes over the whole
+sidebar with a back button and its own create affordance. Both are a handful of lines around
+this component (see the `InADialog` and `InAHostSidebar` stories), and a wrapper modelling
+both would be a worse contract than no wrapper. `existingGuardrails` is likewise not a palette
+concern: it exists so the *builder* can propose a unique default name.
+
+### Localization
+
+Chrome strings resolve through `useGuardrailPaletteLabels` (lingui, `guardrails.palette.*` ids
+in the shared canvas catalog); `labels` overrides any of them and wins over the catalog.
+Definition copy is not localized here: display names, descriptions and connector or folder
+names arrive resolved on the definitions, from the canonical copy table or the wire.
+
+All nine strings are harvested from the two products' own catalogs (eight from Flow's
+`addGuardrailPalette_*` i18next keys, `list-aria-label` from Agents' `guardrails.palette.*`
+lingui id), so the palette adds no translation work: 9 of 9 in each of the 12 translated
+locales, asserted by a test. `ru` is deliberately absent, as everywhere else in this catalog.
+One loc review item comes with the harvest: German takes "Leitplanke" from Flow for eight ids
+and "Leitlinien" from Agents for the ninth, because the two products picked different words.
+
+## GuardrailBuilder
+
+The complete Add/Edit screen for an OOTB guardrail validator: status banners, usage note,
+type display (edit mode), name, description, validator parameters, scope selector, action
+(log / block / escalate; filter reserved for the custom-guardrail phase), evaluations
+toggle, mixed-scopes banner, and the Save / Cancel / Save-as-new footer.
+
+```tsx
+import { GuardrailBuilder } from '@uipath/apollo-react/canvas/guardrails';
+
+ ;
+```
+
+Contract highlights:
+
+- **Owns form state.** Initialized from `definition`/`guardrail`/`defaultName` at mount —
+ remount with a new `key` to reset (all known hosts already remount per session).
+- **Owns validation and gates its own Save.** Messages localize through lingui
+ (overridable per string via `labels`); a host that validates externally (e.g. zod) passes
+ `errors` — host messages display immediately, win per field, and gate Save. The pure
+ predicates (`getGuardrailActionErrorFields`, `getGuardrailSelectorErrorFields`,
+ `getRequiredEmptyParameterIds`) are exported.
+- **Escalation is slot-driven.** `renderRecipientSearch` (user/group directory autosuggest)
+ and `renderAppPicker` (escalation app) are host capabilities; `escalateHelp` renders under
+ the escalation grid (e.g. a marketplace link — product URLs never ship in this package).
+ Without slots the form falls back to a plain input / an "unavailable" note.
+- **Layout knobs for both hosts**: `inline`/`hideHeader`/`dialogMaxWidth`, `title` accepts a
+ ReactNode (chips, links), `evalsTogglePlacement: 'form' | 'footer'`.
+- Requires an ancestor `TooltipProvider`.
+
+`GuardrailFormLayout` is exported standalone for hosts composing their own screen: three
+modes (inline+hideHeader / inline with back-button header / modal Dialog), `secondaryAction`,
+`saveDisabled`, and a `footerStart` region.
+
+## GuardrailValidatorForm
+
+Renders one editor per `GuardrailParameterDefinition`, covering the seven wire parameter
+types:
+
+| type | editor |
+| --- | --- |
+| `number` | numeric input with `min`/`max`/`step` |
+| `text` | multiline textarea (`maxLength`) |
+| `boolean` | switch |
+| `enum` | single select (a stored value missing from `options` is kept as a synthetic option) |
+| `enum-list` | toggleable chips inline for ≤8 options, otherwise the wind `MultiSelect` |
+| `text-list` | repeated textarea rows with Add/Remove (`maxItems`, `maxLength`) |
+| `map-enum` | one numeric input per key selected in the sibling `keySource` enum-list |
+
+```tsx
+import {
+ GuardrailValidatorForm,
+ getRequiredEmptyParameterIds,
+ seedGuardrailParameters,
+} from '@uipath/apollo-react/canvas/guardrails';
+
+const [parameters, setParameters] = useState(() =>
+ seedGuardrailParameters(definition.parameters, existingGuardrail?.validatorParameters)
+);
+
+ — host-owned validation
+ onClearError={clearParamError}
+/>;
+```
+
+Requires an ancestor `TooltipProvider` (for the per-parameter info tooltips).
+
+### Contract
+
+- **Fully controlled, validation-free.** The host owns values and validation. Compute
+ required-field errors with `getRequiredEmptyParameterIds(definitions, parameters)` and map
+ the returned ids to your own (localized) messages; the component renders `errors[id]` under
+ the matching editor and calls `onClearError(id)` before `onChange` when that parameter is
+ edited.
+- **Definitions arrive pre-resolved.** `label`, `tooltip` and `optionLabels` are display
+ strings the host already localized; domain copy (PII entity names, validator descriptions)
+ never ships in this package.
+- **No product types cross the boundary.** `GuardrailValidatorParameter` structurally mirrors
+ the wire shape both products persist, so host unions assign cleanly in both directions.
+- **Per-parameter override.** `renderParameter(ctx)` replaces the editor for any parameter
+ (return `undefined` to fall through). `ctx.onValueChange` upserts the parameter;
+ `ctx.onParametersChange` replaces the whole array for overrides that persist sidecar
+ parameters (e.g. a model picker storing connection metadata).
+
+### Save-time companions
+
+The editors never prune or drop values while typing; reconcile at save time:
+
+```ts
+import { dropEmptyOptionalParameters, syncMapEnumParameters } from '@uipath/apollo-react/canvas/guardrails';
+
+const cleaned = dropEmptyOptionalParameters(
+ syncMapEnumParameters(parameters, definition.parameters),
+ definition.parameters
+);
+```
+
+- `syncMapEnumParameters` rebuilds every `map-enum` value so its keys exactly match the
+ current `keySource` selection (preserving user edits, then per-key defaults, then `min`).
+ It mirrors the map-enum editor's key resolution — keeping the two in one package is the
+ point: they must never drift.
+- `dropEmptyOptionalParameters` removes optional parameters left `''`/`[]`, which runtimes
+ reject at publish time.
+- `seedGuardrailParameters` builds the initial value array from definitions (editing passes
+ the stored values through verbatim), coercing `null` defaults to the union's value types.
+
+### Localization
+
+The component's own chrome strings (placeholders, Add, aria labels) localize through the
+package's standard lingui setup: `useSafeLingui` with explicit `guardrails.*` ids and English
+defaults, translations in the shared canvas catalog (`src/canvas/locales/*.json`, 13 locales
+translated; `ru` falls back to English per key). Without a lingui provider the components
+render the English defaults — mount `ApI18nProvider component="canvas"` (from
+`@uipath/apollo-react/i18n`) for translations. `labels` overrides individual strings and wins
+over the catalog. The required-error message is deliberately not included: it arrives through
+`errors`, because validation messages belong to hosts.
+
+Localized template strings that cross into plain-string APIs (dialog titles, the text-list
+remove label consumed by wind's `formatTemplate`) are ICU messages formatted with sentinel
+values, so they come back carrying the `{{token}}` convention — see `TEMPLATE_TOKENS` in
+`i18n.ts`.
+
+### Consuming from a shadow-DOM host (Agents stage 2)
+
+Radix overlays (the enum select, the enum-list popover, tooltips) portal to `document.body`
+by default and escape shadow roots; wrap the form's subtree with `PortalContainerProvider`
+and inject the compiled canvas stylesheet
+(`@uipath/apollo-react/canvas/styles/tailwind.canvas.css?inline` — its Tailwind build scans
+this directory) into the shadow root (see `AgentCanvasEditor` in `frontend-sw` for the
+`?inline` injection precedent). `@uipath/apollo-wind` must resolve to a single copy alongside
+apollo-react's own pin, or Radix contexts and CSS duplicate.
+
+## Built on the forms/ MetadataForm stack
+
+`GuardrailValidatorForm` is not a form renderer of its own: internally it is
+`buildGuardrailFormSchema(definitions, labels)` + the package's `MetadataForm`
+(`components/forms/`: `FormSchema` → `MetadataForm` → `field-renderer`), mounted through the
+controlled-host seam (`values` / `onValuesChange` / `errors` / `disableValidation` /
+`container="div"` / synchronous `components`). The public contract above is the adapter
+boundary — hosts never see the schema.
+
+How each parameter type maps:
+
+| parameter type | rendering |
+| --- | --- |
+| `number` | field type `number` |
+| `text` | field type `textarea` (`minRows`, `maxLength`) |
+| `boolean` | field type `switch` |
+| `enum` | field type `select` (synthetic option appended for a stale stored value) |
+| `enum-list` > 8 options | field type `multiselect` |
+| `enum-list` ≤ 8 options | custom component `guardrail-enum-list-chips` (`GuardrailChip` toggles in a `FieldShell`) |
+| `text-list` | field type `string-list` (added to forms/ for this convergence — generic) |
+| `map-enum` | custom component `guardrail-map-enum` (reads the `keySource` sibling via the form context) |
+| any id claimed by `renderParameter` | custom component `guardrail-render-parameter` (the bridge that mounts the host's node and exposes `onValueChange`/`onParametersChange`) |
+
+Why the three custom components stay guardrail-owned: the chip-toggle UX is a product
+decision (small option sets read better as chips than a dropdown), `map-enum` derives its
+rows from a sibling field's live selection, and `renderParameter` is a host seam — all three
+are exactly what `type: 'custom'` + component registration exists for.
+
+Adapter invariants (guarded by the `controlled contract` tests in
+`guardrail-validator-form.test.tsx`):
+
+- Emissions upsert only the edited parameter into the host's current array — untouched
+ defaults never leak in, and parameters without a matching definition (sidecars written via
+ `onParametersChange`, e.g. `byomConnectionId`) never enter the form and round-trip
+ untouched.
+- A synchronous host echo of the emitted array is a no-op (per-field deep-equal guard): no
+ re-emission, focus and cursor survive. Hosts must echo synchronously from `onChange`.
+- Values are coerced to the wire shape on emit (`coerceGuardrailParameterValue`): a cleared
+ number input persists `0`, never `NaN`; text/enum never persist `null`.
+
+**Rule for new work**: a new parameter editor extends `field-renderer` with a first-class
+field type (when it's generic) or registers a custom component here (when it's
+guardrail-shaped) — never a parallel renderer next to `MetadataForm`.
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 0000000000..534257682b
--- /dev/null
+++ b/packages/apollo-react/src/canvas/components/Guardrails/__fixtures__/definitions-wire.fixtures.ts
@@ -0,0 +1,245 @@
+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; enrichment supplies 0..1 step 0.1.
+ 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__/dnd-geometry.ts b/packages/apollo-react/src/canvas/components/Guardrails/__fixtures__/dnd-geometry.ts
new file mode 100644
index 0000000000..9b35a719b0
--- /dev/null
+++ b/packages/apollo-react/src/canvas/components/Guardrails/__fixtures__/dnd-geometry.ts
@@ -0,0 +1,69 @@
+/**
+ * Synthetic layout for the drag-and-drop suites.
+ *
+ * happy-dom runs no layout, so every element reports a zero rect. dnd-kit's collision
+ * detection then cannot tell two rows apart, and the keyboard sensor finds no candidate in the
+ * arrow direction, so a reorder silently does nothing. Laying the rendered rows out on a
+ * vertical grid is the minimum geometry a reorder needs; it is also all of it, which is why
+ * this is a fixture and not a mock of dnd-kit.
+ */
+
+import { act } from '@testing-library/react';
+
+const ROW_HEIGHT = 48;
+const ROW_WIDTH = 320;
+
+function rectAt(top: number, height: number): DOMRect {
+ const rect = {
+ x: 0,
+ y: top,
+ top,
+ left: 0,
+ right: ROW_WIDTH,
+ bottom: top + height,
+ width: ROW_WIDTH,
+ height,
+ };
+ return { ...rect, toJSON: () => rect } as DOMRect;
+}
+
+function stubRect(element: Element, rect: DOMRect): void {
+ Object.defineProperty(element, 'getBoundingClientRect', {
+ configurable: true,
+ value: () => rect,
+ });
+}
+
+/**
+ * Stack every rendered row into a column, and give their container a rect that spans it (the
+ * `restrictToParentElement` modifier clamps the drag translation to that rect, and a zero-sized
+ * parent clamps it to nothing).
+ */
+export function layoutRowsVertically(
+ container: HTMLElement,
+ selector = '[data-slot="guardrail-list-row"]'
+): void {
+ const rows = Array.from(container.querySelectorAll(selector));
+ rows.forEach((row, index) => stubRect(row, rectAt(index * ROW_HEIGHT, ROW_HEIGHT)));
+
+ const parents = new Set(
+ rows.map((row) => row.parentElement).filter((el): el is HTMLElement => el !== null)
+ );
+ for (const parent of parents) {
+ stubRect(parent, rectAt(0, rows.length * ROW_HEIGHT));
+ }
+}
+
+/**
+ * Let dnd-kit finish a step of a drag.
+ *
+ * It measures droppables and applies coordinate changes inside `requestAnimationFrame`, which
+ * the canvas test setup implements as `setTimeout(cb, 0)`: a macrotask, so it does not flush
+ * with the `act()` around a `fireEvent`. Await this between the keystrokes of a drag, or the
+ * next one is computed against the geometry of the step before.
+ */
+export async function flushDndFrame(): Promise {
+ await act(async () => {
+ await new Promise((resolve) => setTimeout(resolve, 0));
+ });
+}
diff --git a/packages/apollo-react/src/canvas/components/Guardrails/__fixtures__/guardrail-list.fixtures.ts b/packages/apollo-react/src/canvas/components/Guardrails/__fixtures__/guardrail-list.fixtures.ts
new file mode 100644
index 0000000000..2abe04209c
--- /dev/null
+++ b/packages/apollo-react/src/canvas/components/Guardrails/__fixtures__/guardrail-list.fixtures.ts
@@ -0,0 +1,51 @@
+import type { GuardrailListDefinition, GuardrailListItem } from '../list-types';
+
+/**
+ * Rows and definitions shaped like what the two products pass: one UiPath validator, one
+ * bring-your-own validator resolved through a connector, and one custom guardrail with no
+ * definition at all. Between them they cover every branch a row can take.
+ */
+
+export const PII_GUARDRAIL: GuardrailListItem = {
+ id: 'g1',
+ name: 'PII detection 1',
+ description: 'Scans agent output for personal data.',
+ $guardrailType: 'builtInValidator',
+ selector: { scopes: ['Agent', 'Tool'], matchNames: ['Send email'] },
+ action: { $actionType: 'log' },
+ validatorType: 'pii_detection',
+};
+
+export const BYO_GUARDRAIL: GuardrailListItem = {
+ id: 'g2',
+ name: 'Noma prompt shield',
+ $guardrailType: 'builtInValidator',
+ selector: { scopes: ['Llm'] },
+ action: { $actionType: 'block' },
+ validatorType: 'byo',
+ byoValidatorName: 'noma_prompt_injection',
+};
+
+export const CUSTOM_GUARDRAIL: GuardrailListItem = {
+ id: 'g3',
+ name: 'Blocked words',
+ $guardrailType: 'custom',
+ selector: { scopes: ['Tool'], matchNames: ['Send email'] },
+ action: { $actionType: 'filter' },
+};
+
+export const GUARDRAILS: GuardrailListItem[] = [PII_GUARDRAIL, BYO_GUARDRAIL, CUSTOM_GUARDRAIL];
+
+export const PII_DEFINITION: GuardrailListDefinition = {
+ validator: 'pii_detection',
+ status: 'Available',
+};
+
+export const BYO_DEFINITION: GuardrailListDefinition = {
+ validator: 'byo',
+ status: 'Available',
+ byoValidatorName: 'noma_prompt_injection',
+ byoConnectorName: 'Noma Security',
+};
+
+export const DEFINITIONS: GuardrailListDefinition[] = [PII_DEFINITION, BYO_DEFINITION];
diff --git a/packages/apollo-react/src/canvas/components/Guardrails/__fixtures__/guardrail-palette.fixtures.ts b/packages/apollo-react/src/canvas/components/Guardrails/__fixtures__/guardrail-palette.fixtures.ts
new file mode 100644
index 0000000000..32f1cd5854
--- /dev/null
+++ b/packages/apollo-react/src/canvas/components/Guardrails/__fixtures__/guardrail-palette.fixtures.ts
@@ -0,0 +1,69 @@
+import type { GuardrailPaletteDefinition } from '../palette-types';
+
+/**
+ * Definitions shaped like what the two products pass a palette: UiPath validators (one of
+ * them unauthorized), and bring-your-own validators across two connections, one of which has
+ * no folder. Between them they cover every grouping and every chip branch.
+ */
+
+export const PII_DEFINITION: GuardrailPaletteDefinition = {
+ validator: 'pii_detection',
+ displayName: 'PII detection',
+ description: 'Detects personally identifiable information in agent traffic.',
+ status: 'Available',
+};
+
+export const PROMPT_ATTACKS_DEFINITION: GuardrailPaletteDefinition = {
+ validator: 'user_prompt_attacks',
+ displayName: 'Prompt attacks',
+ description: 'Detects attempts to override the agent instructions.',
+ status: 'Available',
+};
+
+export const UNAUTHORIZED_DEFINITION: GuardrailPaletteDefinition = {
+ validator: 'harmful_content',
+ displayName: 'Harmful content',
+ description: 'Detects harmful content categories in agent traffic.',
+ status: 'Unauthorised',
+};
+
+export const BYO_FOLDER_DEFINITION: GuardrailPaletteDefinition = {
+ validator: 'byo',
+ displayName: 'Noma prompt shield',
+ description: 'Vendor-managed prompt injection detection.',
+ status: 'Available',
+ byoValidatorName: 'noma_prompt_injection',
+ byoConnectorName: 'Noma Security',
+ byoGuardrailConnectionId: 'connection-1',
+ folderPath: 'Shared/Security',
+};
+
+/** The same validator name on a second connection: only the connection id keeps them apart. */
+export const BYO_SECOND_CONNECTION_DEFINITION: GuardrailPaletteDefinition = {
+ ...BYO_FOLDER_DEFINITION,
+ displayName: 'Noma prompt shield (EU)',
+ byoGuardrailConnectionId: 'connection-2',
+ folderPath: 'Shared/Security EU',
+};
+
+/** No folder resolved, so the group falls back to the connector name. */
+export const BYO_CONNECTOR_ONLY_DEFINITION: GuardrailPaletteDefinition = {
+ validator: 'byo',
+ displayName: 'Acme policy check',
+ status: 'Available',
+ byoValidatorName: 'acme_policy',
+ byoConnectorName: 'Acme Guard',
+ byoGuardrailConnectionId: 'connection-3',
+};
+
+export const UIPATH_DEFINITIONS: GuardrailPaletteDefinition[] = [
+ PII_DEFINITION,
+ PROMPT_ATTACKS_DEFINITION,
+];
+
+export const MIXED_DEFINITIONS: GuardrailPaletteDefinition[] = [
+ PII_DEFINITION,
+ BYO_FOLDER_DEFINITION,
+ PROMPT_ATTACKS_DEFINITION,
+ BYO_CONNECTOR_ONLY_DEFINITION,
+];
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 0000000000..4c50c3fa00
--- /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
new file mode 100644
index 0000000000..8a9aa89413
--- /dev/null
+++ b/packages/apollo-react/src/canvas/components/Guardrails/builder-types.ts
@@ -0,0 +1,177 @@
+import type * as React from 'react';
+import type { GuardrailParameterDefinition, GuardrailValidatorParameter } from './types';
+
+/**
+ * Structural mirrors of the guardrail wire shapes both consuming products persist. Hosts
+ * pass their own equivalent types without mapping; mutual assignability is asserted on the
+ * host side (see the Flow adapter's type-assertion file).
+ */
+
+export type GuardrailScope = 'Agent' | 'Llm' | 'Tool';
+
+export interface GuardrailSelector {
+ scopes: GuardrailScope[];
+ matchNames?: string[];
+}
+
+export type GuardrailSeverityLevel = 'Info' | 'Warning' | 'Error';
+
+/** Recipient type discriminators (numeric on the wire). */
+export const GuardrailRecipientType = {
+ User: 1,
+ Group: 2,
+ StaticEmail: 3,
+ AssetEmail: 4,
+ StaticGroupName: 5,
+ AssetGroupName: 6,
+} as const;
+export type GuardrailRecipientTypeValue =
+ (typeof GuardrailRecipientType)[keyof typeof GuardrailRecipientType];
+
+/**
+ * Escalation recipient union. The editor offers User/Group/StaticEmail/StaticGroupName;
+ * the asset variants (4/6) exist so values using them round-trip through the form unedited.
+ */
+export type GuardrailEscalateRecipient =
+ | { type: 1; value: string; displayName: string }
+ | { type: 2; value: string; displayName: string }
+ | { type: 3; value: string }
+ | { type: 4; folderPath?: string; assetName: string }
+ | { type: 5; value: string }
+ | { type: 6; folderPath?: string; assetName: string };
+
+export interface GuardrailEscalateApp {
+ id: string;
+ version: string;
+ name: string;
+ folderId?: string;
+ folderName?: string;
+ appProcessKey?: string;
+ runtime?: string;
+}
+
+export type GuardrailAction =
+ | { $actionType: 'log'; severityLevel: GuardrailSeverityLevel }
+ | { $actionType: 'block'; reason: string }
+ // Field references are product-shaped; the OOTB builder never edits them (pass-through
+ // only, the filter option is custom-guardrail territory), so they stay opaque here.
+ | { $actionType: 'filter'; fields: unknown[] }
+ | { $actionType: 'escalate'; app: GuardrailEscalateApp; recipient: GuardrailEscalateRecipient };
+
+export type GuardrailDefinitionStatus =
+ | 'Available'
+ | 'FeatureDisabled'
+ | 'Unauthorised'
+ | 'Disabled';
+
+/**
+ * The display-ready definition of an OOTB guardrail validator. `displayName` and `usageNote`
+ * arrive pre-resolved (host-localized); `parameters` reuses the validator-form definition
+ * type.
+ */
+export interface GuardrailDefinition {
+ validator: string;
+ displayName: string;
+ allowedScopes: GuardrailScope[];
+ parameters: GuardrailParameterDefinition[];
+ status: GuardrailDefinitionStatus;
+ /** Pre-localized informational note rendered above the form. */
+ usageNote?: React.ReactNode;
+ /** Present for bring-your-own guardrail definitions; stamped onto saved values. */
+ byoValidatorName?: string;
+}
+
+/** `validatorType` persisted for bring-your-own guardrail definitions. */
+export const GUARDRAIL_BYO_VALIDATOR_TYPE = 'byo';
+
+/** The builder's in/out value — mirrors the persisted built-in-validator guardrail shape. */
+export interface GuardrailBuilderValue {
+ id: string;
+ $guardrailType: 'builtInValidator';
+ name: string;
+ description?: string;
+ selector: GuardrailSelector;
+ action: GuardrailAction;
+ enabledForEvals: boolean;
+ validatorType: string;
+ validatorParameters: GuardrailValidatorParameter[];
+ byoValidatorName?: string;
+}
+
+/**
+ * Host-supplied validation errors, merged over the builder's internal validation (the host
+ * message wins per field). Any present error gates Save.
+ */
+export interface GuardrailBuilderErrors {
+ name?: string;
+ blockReason?: string;
+ filterFields?: string;
+ recipient?: string;
+ actionApp?: string;
+ scopes?: string;
+ toolNames?: string;
+ /** Per-parameter messages keyed by parameter id. */
+ parameters?: Record;
+}
+
+/** Context handed to the `renderRecipientSearch` slot (user/group directory autosuggest). */
+export interface GuardrailRecipientSearchContext {
+ kind: 'user' | 'group';
+ /** Current display value (displayName, falling back to the raw value). */
+ displayValue: string;
+ /** Localized placeholder for the current kind. */
+ placeholder: string;
+ /** Whether the recipient currently fails validation (style the input accordingly). */
+ invalid: boolean;
+ onSelect: (selection: { value: string; displayName: string }) => void;
+ onClear: () => void;
+}
+
+/** Context handed to the `renderAppPicker` slot (escalation action app). */
+export interface GuardrailAppPickerContext {
+ /** The selected app, or null when unset. */
+ app: GuardrailEscalateApp | null;
+ onChange: (app: GuardrailEscalateApp | null) => void;
+ /** Localized field label. */
+ label: string;
+ /** Validation message to surface, if any. */
+ error?: string;
+}
+
+/**
+ * Context handed to the `renderStaticRecipient` slot (StaticEmail/AssetEmail/StaticGroupName/
+ * AssetGroupName recipients).
+ */
+export interface GuardrailStaticRecipientContext {
+ /** Which recipient family the type select currently shows. */
+ kind: 'email' | 'groupName';
+ /** The current recipient — the static or asset variant of the kind. */
+ recipient: GuardrailEscalateRecipient;
+ /** Localized field label for the kind. */
+ label: string;
+ /** Whether the recipient currently fails validation (style the control accordingly). */
+ invalid: boolean;
+ /** Validation message to surface, if any. */
+ error?: string;
+ /**
+ * Replace the recipient wholesale — lets hosts toggle between the static and asset
+ * variants of the same kind (StaticEmail 3 ↔ AssetEmail 4, StaticGroupName 5 ↔
+ * AssetGroupName 6).
+ */
+ onChange: (recipient: GuardrailEscalateRecipient) => void;
+}
+
+export interface GuardrailBuilderSlots {
+ /** Replace the recipient autosuggest for User/Group recipients. Fallback: a plain input. */
+ renderRecipientSearch?: (ctx: GuardrailRecipientSearchContext) => React.ReactNode;
+ /**
+ * Replace the editor for static/asset recipients (types 3/4/5/6). Return `undefined` to
+ * fall through to the built-in plain input (which edits `value` for static recipients and
+ * `assetName` for asset ones).
+ */
+ renderStaticRecipient?: (ctx: GuardrailStaticRecipientContext) => React.ReactNode | undefined;
+ /** Render the escalation app picker. Fallback: a localized "picker unavailable" note. */
+ renderAppPicker?: (ctx: GuardrailAppPickerContext) => React.ReactNode;
+ /** Rendered under the escalation grid (e.g. a marketplace help line). */
+ escalateHelp?: React.ReactNode;
+}
diff --git a/packages/apollo-react/src/canvas/components/Guardrails/builder-utils.test.ts b/packages/apollo-react/src/canvas/components/Guardrails/builder-utils.test.ts
new file mode 100644
index 0000000000..d6bf971a08
--- /dev/null
+++ b/packages/apollo-react/src/canvas/components/Guardrails/builder-utils.test.ts
@@ -0,0 +1,226 @@
+import { describe, expect, it } from 'vitest';
+import {
+ type GuardrailAction,
+ type GuardrailDefinition,
+ GuardrailRecipientType,
+} from './builder-types';
+import {
+ createDefaultGuardrailAction,
+ generateGuardrailId,
+ getGuardrailActionErrorFields,
+ getGuardrailSelectorErrorFields,
+ initGuardrailBuilderFormData,
+} from './builder-utils';
+
+function makeDef(overrides?: Partial): GuardrailDefinition {
+ return {
+ validator: 'pii_detection',
+ displayName: 'PII Detection',
+ allowedScopes: ['Agent', 'Llm', 'Tool'],
+ parameters: [],
+ status: 'Available',
+ ...overrides,
+ };
+}
+
+describe('generateGuardrailId', () => {
+ it('produces the guardrail-- format', () => {
+ expect(generateGuardrailId()).toMatch(/^guardrail-[a-z0-9]+-[a-z0-9]{5}$/);
+ });
+});
+
+describe('createDefaultGuardrailAction', () => {
+ it('creates the per-type default payloads', () => {
+ expect(createDefaultGuardrailAction('log')).toEqual({
+ $actionType: 'log',
+ severityLevel: 'Info',
+ });
+ expect(createDefaultGuardrailAction('block')).toEqual({ $actionType: 'block', reason: '' });
+ expect(createDefaultGuardrailAction('filter')).toEqual({ $actionType: 'filter', fields: [] });
+ expect(createDefaultGuardrailAction('escalate')).toEqual({
+ $actionType: 'escalate',
+ app: { id: '', version: '', name: '' },
+ recipient: { type: GuardrailRecipientType.User, value: '', displayName: '' },
+ });
+ });
+});
+
+describe('getGuardrailActionErrorFields', () => {
+ it('flags an empty block reason', () => {
+ expect(getGuardrailActionErrorFields({ $actionType: 'block', reason: ' ' })).toEqual([
+ 'blockReason',
+ ]);
+ expect(getGuardrailActionErrorFields({ $actionType: 'block', reason: 'why' })).toEqual([]);
+ });
+
+ it('flags an empty filter field selection', () => {
+ expect(getGuardrailActionErrorFields({ $actionType: 'filter', fields: [] })).toEqual([
+ 'filterFields',
+ ]);
+ expect(getGuardrailActionErrorFields({ $actionType: 'filter', fields: [{}] })).toEqual([]);
+ });
+
+ it('flags missing escalate recipient and app', () => {
+ const empty: GuardrailAction = {
+ $actionType: 'escalate',
+ app: { id: '', version: '', name: '' },
+ recipient: { type: GuardrailRecipientType.User, value: '', displayName: '' },
+ };
+ expect(getGuardrailActionErrorFields(empty)).toEqual(['recipient', 'actionApp']);
+
+ const filled: GuardrailAction = {
+ $actionType: 'escalate',
+ app: { id: 'app-1', version: '1', name: 'Escalation app' },
+ recipient: { type: GuardrailRecipientType.StaticEmail, value: 'a@b.c' },
+ };
+ expect(getGuardrailActionErrorFields(filled)).toEqual([]);
+ });
+
+ it('treats a filled asset recipient as valid (validated on assetName)', () => {
+ const assetRecipient: GuardrailAction = {
+ $actionType: 'escalate',
+ app: { id: 'app-1', version: '1', name: 'App' },
+ recipient: { type: GuardrailRecipientType.AssetEmail, assetName: 'asset' },
+ };
+ expect(getGuardrailActionErrorFields(assetRecipient)).toEqual([]);
+ });
+
+ it('never flags a log action', () => {
+ expect(getGuardrailActionErrorFields({ $actionType: 'log', severityLevel: 'Info' })).toEqual(
+ []
+ );
+ });
+});
+
+describe('getGuardrailSelectorErrorFields', () => {
+ it('requires at least one scope', () => {
+ expect(getGuardrailSelectorErrorFields({ scopes: [] })).toEqual(['scopes']);
+ });
+
+ it('requires tools when the Tool scope is selected', () => {
+ expect(getGuardrailSelectorErrorFields({ scopes: ['Tool'] })).toEqual(['toolNames']);
+ expect(getGuardrailSelectorErrorFields({ scopes: ['Tool'], matchNames: [] })).toEqual([
+ 'toolNames',
+ ]);
+ expect(getGuardrailSelectorErrorFields({ scopes: ['Tool'], matchNames: ['ToolA'] })).toEqual(
+ []
+ );
+ });
+
+ it('accepts non-Tool scopes without matchNames', () => {
+ expect(getGuardrailSelectorErrorFields({ scopes: ['Agent', 'Llm'] })).toEqual([]);
+ });
+});
+
+describe('getGuardrailActionErrorFields — asset recipients', () => {
+ it('treats a filled assetName as a valid recipient and an empty one as invalid', () => {
+ const base = {
+ $actionType: 'escalate' as const,
+ app: { id: 'app1', version: '1', name: 'App' },
+ };
+ expect(
+ getGuardrailActionErrorFields({
+ ...base,
+ recipient: { type: GuardrailRecipientType.AssetEmail, assetName: 'EmailAsset' },
+ })
+ ).toEqual([]);
+ expect(
+ getGuardrailActionErrorFields({
+ ...base,
+ recipient: { type: GuardrailRecipientType.AssetGroupName, assetName: ' ' },
+ })
+ ).toEqual(['recipient']);
+ });
+});
+
+describe('initGuardrailBuilderFormData', () => {
+ it('copies an existing guardrail verbatim', () => {
+ const existing = {
+ id: 'g1',
+ $guardrailType: 'builtInValidator' as const,
+ name: 'Mine',
+ description: 'desc',
+ selector: { scopes: ['Tool' as const], matchNames: ['OtherTool'] },
+ action: { $actionType: 'log' as const, severityLevel: 'Info' as const },
+ enabledForEvals: false,
+ validatorType: 'pii_detection',
+ validatorParameters: [],
+ };
+
+ const form = initGuardrailBuilderFormData(makeDef(), 'Tool', existing, 'MyTool');
+ expect(form.id).toBe('g1');
+ expect(form.selector.matchNames).toEqual(['OtherTool']);
+ expect(form.enabledForEvals).toBe(false);
+ });
+
+ it('sets matchNames when scope is Tool and toolName is provided', () => {
+ const form = initGuardrailBuilderFormData(makeDef(), 'Tool', undefined, 'MyTool');
+ expect(form.selector.scopes).toEqual(['Tool']);
+ expect(form.selector.matchNames).toEqual(['MyTool']);
+ });
+
+ it('does not set matchNames when scope is Tool but no toolName', () => {
+ const form = initGuardrailBuilderFormData(makeDef(), 'Tool');
+ expect(form.selector.scopes).toEqual(['Tool']);
+ expect(form.selector.matchNames).toBeUndefined();
+ });
+
+ it('falls back to the first allowedScope when the opening scope is not allowed (non-Agent too)', () => {
+ // The palette filters by allowedScopes before opening, but a disallowed opening scope
+ // must never seed a selector the user can't fix (the scope selector renders only for Agent).
+ const form = initGuardrailBuilderFormData(
+ makeDef({ allowedScopes: ['Agent', 'Llm'] }),
+ 'Tool',
+ undefined,
+ 'MyTool'
+ );
+ expect(form.selector.scopes).toEqual(['Agent']);
+ expect(form.selector.matchNames).toBeUndefined();
+ });
+
+ it('falls back to first allowedScope when Agent is not allowed', () => {
+ const form = initGuardrailBuilderFormData(
+ makeDef({ allowedScopes: ['Llm', 'Tool'] }),
+ 'Agent',
+ undefined,
+ 'MyTool'
+ );
+ expect(form.selector.scopes).toEqual(['Llm']);
+ expect(form.selector.matchNames).toBeUndefined();
+ });
+
+ it('sets matchNames when Agent scope falls back to Tool', () => {
+ const form = initGuardrailBuilderFormData(
+ makeDef({ allowedScopes: ['Tool'] }),
+ 'Agent',
+ undefined,
+ 'MyTool'
+ );
+ expect(form.selector.scopes).toEqual(['Tool']);
+ expect(form.selector.matchNames).toEqual(['MyTool']);
+ });
+
+ it('seeds defaults with null coercion for create mode', () => {
+ const definition = makeDef({
+ parameters: [
+ { id: 'prompt', type: 'text', label: 'Prompt', required: true, defaultValue: null },
+ {
+ id: 'examples',
+ type: 'text-list',
+ label: 'Examples',
+ required: false,
+ defaultValue: null,
+ },
+ ],
+ });
+ const form = initGuardrailBuilderFormData(definition, 'Agent');
+
+ expect(form.validatorParameters).toEqual([
+ { $parameterType: 'text', id: 'prompt', value: '' },
+ { $parameterType: 'text-list', id: 'examples', value: [] },
+ ]);
+ expect(form.action).toEqual({ $actionType: 'log', severityLevel: 'Info' });
+ expect(form.enabledForEvals).toBe(true);
+ expect(form.name).toBe('PII Detection');
+ });
+});
diff --git a/packages/apollo-react/src/canvas/components/Guardrails/builder-utils.ts b/packages/apollo-react/src/canvas/components/Guardrails/builder-utils.ts
new file mode 100644
index 0000000000..682aa1e011
--- /dev/null
+++ b/packages/apollo-react/src/canvas/components/Guardrails/builder-utils.ts
@@ -0,0 +1,138 @@
+import {
+ type GuardrailAction,
+ type GuardrailBuilderValue,
+ type GuardrailDefinition,
+ GuardrailRecipientType,
+ type GuardrailScope,
+ type GuardrailSelector,
+} from './builder-types';
+import { seedGuardrailParameters } from './utils';
+
+export function generateGuardrailId(): string {
+ return `guardrail-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 7)}`;
+}
+
+/** Default action payload for a freshly selected action type. */
+export function createDefaultGuardrailAction(
+ type: GuardrailAction['$actionType']
+): GuardrailAction {
+ switch (type) {
+ case 'block':
+ return { $actionType: 'block', reason: '' };
+ case 'filter':
+ return { $actionType: 'filter', fields: [] };
+ case 'escalate':
+ return {
+ $actionType: 'escalate',
+ app: { id: '', version: '', name: '' },
+ recipient: { type: GuardrailRecipientType.User, value: '', displayName: '' },
+ };
+ default:
+ return { $actionType: 'log', severityLevel: 'Info' };
+ }
+}
+
+export type GuardrailActionErrorField = 'blockReason' | 'filterFields' | 'recipient' | 'actionApp';
+
+/** Action fields whose current value fails required validation. Message text is caller-owned. */
+export function getGuardrailActionErrorFields(
+ action: GuardrailAction
+): GuardrailActionErrorField[] {
+ const fields: GuardrailActionErrorField[] = [];
+ switch (action.$actionType) {
+ case 'block':
+ if (!action.reason.trim()) fields.push('blockReason');
+ break;
+ case 'filter':
+ if (action.fields.length === 0) fields.push('filterFields');
+ break;
+ case 'escalate': {
+ const recipient = action.recipient;
+ const filled =
+ 'value' in recipient
+ ? recipient.value.trim().length > 0
+ : recipient.assetName.trim().length > 0;
+ if (!filled) fields.push('recipient');
+ if (!action.app.id) fields.push('actionApp');
+ break;
+ }
+ }
+ return fields;
+}
+
+export type GuardrailSelectorErrorField = 'scopes' | 'toolNames';
+
+/**
+ * Selector fields whose current value fails required validation. Unconditional — callers
+ * gate on whether the scope selector is shown at all.
+ */
+export function getGuardrailSelectorErrorFields(
+ selector: GuardrailSelector
+): GuardrailSelectorErrorField[] {
+ const fields: GuardrailSelectorErrorField[] = [];
+ if (selector.scopes.length === 0) fields.push('scopes');
+ if (
+ selector.scopes.includes('Tool') &&
+ (!selector.matchNames || selector.matchNames.length === 0)
+ ) {
+ fields.push('toolNames');
+ }
+ return fields;
+}
+
+export interface GuardrailBuilderFormData {
+ id: string;
+ name: string;
+ description: string;
+ selector: GuardrailSelector;
+ action: GuardrailAction;
+ enabledForEvals: boolean;
+ validatorParameters: GuardrailBuilderValue['validatorParameters'];
+}
+
+/**
+ * Initial form state: an existing guardrail is copied verbatim (edit); otherwise values are
+ * seeded from the definition — parameters via `seedGuardrailParameters`, the scope coerced to
+ * the definition's first allowed scope when `Agent` is not allowed, a log/Info default
+ * action, and evaluations enabled.
+ *
+ * @internal Exported for testing only
+ */
+export function initGuardrailBuilderFormData(
+ definition: GuardrailDefinition,
+ scope: GuardrailScope,
+ existing?: GuardrailBuilderValue,
+ toolName?: string
+): GuardrailBuilderFormData {
+ if (existing) {
+ return {
+ id: existing.id,
+ name: existing.name,
+ description: existing.description ?? '',
+ selector: existing.selector,
+ action: existing.action,
+ enabledForEvals: existing.enabledForEvals,
+ validatorParameters: existing.validatorParameters,
+ };
+ }
+
+ // Coerce for every opening scope, not just Agent: hosts filter definitions by allowedScopes
+ // before opening the builder, but a definition that doesn't allow the opening scope must not
+ // seed an invalid selector the user can't fix (the scope selector only renders for Agent).
+ const initialScope = definition.allowedScopes.includes(scope)
+ ? scope
+ : (definition.allowedScopes[0] ?? scope);
+
+ return {
+ id: generateGuardrailId(),
+ name: definition.displayName,
+ description: '',
+ selector: {
+ scopes: [initialScope],
+ ...(initialScope === 'Tool' && toolName ? { matchNames: [toolName] } : {}),
+ },
+ action: { $actionType: 'log', severityLevel: 'Info' },
+ enabledForEvals: true,
+ validatorParameters: seedGuardrailParameters(definition.parameters),
+ };
+}
diff --git a/packages/apollo-react/src/canvas/components/Guardrails/components/enum-list-chips-field.tsx b/packages/apollo-react/src/canvas/components/Guardrails/components/enum-list-chips-field.tsx
new file mode 100644
index 0000000000..feb53fcbda
--- /dev/null
+++ b/packages/apollo-react/src/canvas/components/Guardrails/components/enum-list-chips-field.tsx
@@ -0,0 +1,49 @@
+import type { CustomFieldComponentProps } from '@uipath/apollo-wind';
+import { FormField, FormFieldError } from '@uipath/apollo-wind';
+import type { GuardrailValidatorFormLabels } from '../i18n';
+import type { GuardrailParameterDefinition } from '../types';
+import { FieldShell } from './field-shell';
+import { GuardrailChip } from './guardrail-chip';
+import { ParameterLabel } from './parameter-label';
+
+/**
+ * Inline chip-toggle editor for enum-list parameters with small option sets (the ≤8 case),
+ * registered as the `guardrail-enum-list-chips` custom component; larger sets map to the
+ * first-class `multiselect` field type instead. `paramDef` and `labels` arrive via
+ * `componentProps` from `buildGuardrailFormSchema`.
+ */
+export function EnumListChipsField(props: CustomFieldComponentProps) {
+ const { value, onChange, error, disabled } = props;
+ const paramDef = props.paramDef as GuardrailParameterDefinition;
+ const labels = props.labels as GuardrailValidatorFormLabels;
+
+ const selected = Array.isArray(value) ? (value as string[]) : [];
+
+ /** Friendly label for an option value, falling back to the raw value when unmapped. */
+ const labelFor = (option: string) => paramDef.optionLabels?.[option] ?? option;
+
+ const handleToggle = (option: string, pressed: boolean) => {
+ onChange(pressed ? [...selected, option] : selected.filter((s) => s !== option));
+ };
+
+ return (
+
+
+
+
+ {(paramDef.options ?? []).map((option) => (
+ handleToggle(option, pressed)}
+ disabled={disabled}
+ >
+ {labelFor(option)}
+
+ ))}
+
+
+ {error}
+
+ );
+}
diff --git a/packages/apollo-react/src/canvas/components/Guardrails/components/escalate-action-fields.test.tsx b/packages/apollo-react/src/canvas/components/Guardrails/components/escalate-action-fields.test.tsx
new file mode 100644
index 0000000000..caab56fb7f
--- /dev/null
+++ b/packages/apollo-react/src/canvas/components/Guardrails/components/escalate-action-fields.test.tsx
@@ -0,0 +1,309 @@
+import { fireEvent, render, screen } from '@testing-library/react';
+import { axe } from 'jest-axe';
+import { describe, expect, it, vi } from 'vitest';
+import { type GuardrailAction, GuardrailRecipientType } from '../builder-types';
+import { GUARDRAIL_BUILDER_EN_LABELS } from '../i18n';
+import { EscalateActionFields } from './escalate-action-fields';
+
+const labels = GUARDRAIL_BUILDER_EN_LABELS;
+type EscalateAction = Extract;
+
+function makeAction(overrides?: Partial): EscalateAction {
+ return {
+ $actionType: 'escalate',
+ app: { id: '', version: '', name: '' },
+ recipient: { type: GuardrailRecipientType.User, value: '', displayName: '' },
+ ...overrides,
+ };
+}
+
+const baseProps = {
+ actionTypeSelect:
,
+ labels,
+};
+
+describe('EscalateActionFields', () => {
+ it('renders the injected action type cell inside its grid', () => {
+ render( );
+ expect(screen.getByTestId('action-type-cell')).toBeInTheDocument();
+ });
+
+ describe('recipient type switching', () => {
+ it.each([
+ ['Group', { type: GuardrailRecipientType.Group, value: '', displayName: '' }],
+ ['Email address', { type: GuardrailRecipientType.StaticEmail, value: '' }],
+ ['Group name', { type: GuardrailRecipientType.StaticGroupName, value: '' }],
+ ] as const)('switching to %s resets the recipient payload', async (optionLabel, expectedRecipient) => {
+ const onChange = vi.fn();
+ render( );
+
+ fireEvent.click(screen.getByRole('combobox'));
+ fireEvent.click(await screen.findByRole('option', { name: optionLabel }));
+
+ expect(onChange).toHaveBeenCalledWith(
+ expect.objectContaining({ recipient: expectedRecipient })
+ );
+ });
+ });
+
+ it('uses the renderRecipientSearch slot for user recipients', () => {
+ const onChange = vi.fn();
+ render(
+ (
+ ctx.onSelect({ value: 'u2', displayName: 'User Two' })}
+ >
+ pick
+
+ )}
+ />
+ );
+
+ const slot = screen.getByTestId('directory-search');
+ expect(slot).toHaveAttribute('data-kind', 'user');
+ expect(slot).toHaveAttribute('data-display', 'User One');
+ expect(slot).toHaveAttribute('data-invalid', 'true');
+
+ fireEvent.click(slot);
+ expect(onChange).toHaveBeenCalledWith(
+ expect.objectContaining({
+ recipient: { type: GuardrailRecipientType.User, value: 'u2', displayName: 'User Two' },
+ })
+ );
+ });
+
+ it('falls back to a plain input for user recipients without the slot', () => {
+ const onChange = vi.fn();
+ render( );
+
+ const input = screen.getByPlaceholderText('Search for a user...');
+ fireEvent.change(input, { target: { value: 'jane@acme.com' } });
+ expect(onChange).toHaveBeenCalledWith(
+ expect.objectContaining({
+ recipient: {
+ type: GuardrailRecipientType.User,
+ value: 'jane@acme.com',
+ displayName: 'jane@acme.com',
+ },
+ })
+ );
+ });
+
+ it('renders a plain input for static email recipients', () => {
+ const onChange = vi.fn();
+ render(
+
+ );
+
+ fireEvent.change(screen.getByPlaceholderText('Enter email address'), {
+ target: { value: 'a@b.c' },
+ });
+ expect(onChange).toHaveBeenCalledWith(
+ expect.objectContaining({
+ recipient: { type: GuardrailRecipientType.StaticEmail, value: 'a@b.c' },
+ })
+ );
+ });
+
+ it('renders the recipient error once, inside the recipient field', () => {
+ render(
+
+ );
+
+ expect(screen.getAllByText('Recipient is required')).toHaveLength(1);
+ });
+
+ it('uses the renderAppPicker slot with app context', () => {
+ const onChange = vi.fn();
+ render(
+ (
+ ctx.onChange(null)}
+ >
+ pick app
+
+ )}
+ />
+ );
+
+ const slot = screen.getByTestId('app-picker');
+ expect(slot).toHaveAttribute('data-label', 'Action App');
+ expect(slot).toHaveAttribute('data-error', 'Action app is required');
+ expect(slot).toHaveAttribute('data-app', 'My app');
+
+ fireEvent.click(slot);
+ expect(onChange).toHaveBeenCalledWith(
+ expect.objectContaining({ app: { id: '', version: '', name: '' } })
+ );
+ });
+
+ it('falls back to the unavailable note without an app picker slot', () => {
+ render( );
+ expect(screen.getByText(/app picker unavailable/i)).toBeInTheDocument();
+ });
+
+ it('renders escalateHelp below the grid', () => {
+ render(
+ See the marketplace.
}
+ />
+ );
+
+ expect(screen.getByTestId('help-line')).toBeInTheDocument();
+ });
+
+ it('has no accessibility violations', async () => {
+ const { container } = render(
+
+ );
+ const results = await axe(container);
+ expect(results).toHaveNoViolations();
+ });
+});
+
+describe('renderStaticRecipient slot', () => {
+ it('mounts the slot for static recipients and lets it replace the recipient wholesale', () => {
+ const onChange = vi.fn();
+ const action = makeAction({
+ recipient: { type: GuardrailRecipientType.StaticEmail, value: 'a@b.c' },
+ });
+
+ render(
+ (
+
+ ctx.onChange({
+ type: GuardrailRecipientType.AssetEmail,
+ assetName: 'EmailAsset',
+ })
+ }
+ >
+ {ctx.label}
+
+ )}
+ />
+ );
+
+ const slot = screen.getByTestId('static-slot');
+ expect(slot).toHaveAttribute('data-kind', 'email');
+ fireEvent.click(slot);
+ expect(onChange).toHaveBeenCalledWith({
+ ...action,
+ recipient: { type: GuardrailRecipientType.AssetEmail, assetName: 'EmailAsset' },
+ });
+ });
+
+ it('falls through to the built-in input when the slot returns undefined', () => {
+ const onChange = vi.fn();
+ const action = makeAction({
+ recipient: { type: GuardrailRecipientType.StaticGroupName, value: '' },
+ });
+
+ render(
+ undefined}
+ />
+ );
+
+ const input = screen.getByPlaceholderText(labels.groupNamePlaceholder);
+ fireEvent.change(input, { target: { value: 'Ops' } });
+ expect(onChange).toHaveBeenCalledWith({
+ ...action,
+ recipient: { type: GuardrailRecipientType.StaticGroupName, value: 'Ops' },
+ });
+ });
+
+ it('is not invoked for searchable (user/group) recipients', () => {
+ const renderStaticRecipient = vi.fn();
+ render(
+ {}}
+ renderStaticRecipient={renderStaticRecipient}
+ />
+ );
+ expect(renderStaticRecipient).not.toHaveBeenCalled();
+ });
+});
+
+describe('asset recipient variants', () => {
+ it('displays an asset recipient as its static sibling in the type select', () => {
+ render(
+ {}}
+ />
+ );
+
+ expect(
+ screen.getByRole('combobox', {
+ name: `${labels.assignToLabel}: ${labels.recipientGroupNameLabel}`,
+ })
+ ).toBeInTheDocument();
+ });
+
+ it('edits assetName through the built-in fallback input', () => {
+ const onChange = vi.fn();
+ const action = makeAction({
+ recipient: { type: GuardrailRecipientType.AssetEmail, assetName: 'Old' },
+ });
+
+ render( );
+
+ const input = screen.getByPlaceholderText(labels.emailPlaceholder);
+ expect(input).toHaveValue('Old');
+ fireEvent.change(input, { target: { value: 'New' } });
+ expect(onChange).toHaveBeenCalledWith({
+ ...action,
+ recipient: { type: GuardrailRecipientType.AssetEmail, assetName: 'New' },
+ });
+ });
+});
diff --git a/packages/apollo-react/src/canvas/components/Guardrails/components/escalate-action-fields.tsx b/packages/apollo-react/src/canvas/components/Guardrails/components/escalate-action-fields.tsx
new file mode 100644
index 0000000000..f7f86d1278
--- /dev/null
+++ b/packages/apollo-react/src/canvas/components/Guardrails/components/escalate-action-fields.tsx
@@ -0,0 +1,273 @@
+import {
+ Alert,
+ AlertDescription,
+ FormField,
+ FormFieldError,
+ Input,
+ Label,
+ RequiredIndicator,
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from '@uipath/apollo-wind';
+import { Info } from 'lucide-react';
+import { type ReactNode, useCallback } from 'react';
+import {
+ type GuardrailAction,
+ type GuardrailAppPickerContext,
+ type GuardrailEscalateRecipient,
+ type GuardrailRecipientSearchContext,
+ GuardrailRecipientType,
+ type GuardrailStaticRecipientContext,
+} from '../builder-types';
+import type { GuardrailBuilderLabels } from '../i18n';
+
+type EscalateAction = Extract;
+
+export interface EscalateActionFieldsProps {
+ action: EscalateAction;
+ onChange: (action: EscalateAction) => void;
+ /** Leading grid cell (the Action type select); this component owns the full escalate layout. */
+ actionTypeSelect: ReactNode;
+ errors?: { recipient?: string; actionApp?: string };
+ labels: GuardrailBuilderLabels;
+ renderRecipientSearch?: (ctx: GuardrailRecipientSearchContext) => ReactNode;
+ renderStaticRecipient?: (ctx: GuardrailStaticRecipientContext) => ReactNode | undefined;
+ renderAppPicker?: (ctx: GuardrailAppPickerContext) => ReactNode;
+ /** Rendered under the escalation grid (e.g. a marketplace help line). */
+ escalateHelp?: ReactNode;
+}
+
+/**
+ * Escalation action fields: recipient type + recipient value + action-app picker. The
+ * recipient autosuggest (User/Group) and the app picker are host capabilities injected via
+ * render props; plain-input / unavailable-note fallbacks keep the form usable without them.
+ */
+export function EscalateActionFields({
+ action,
+ onChange,
+ actionTypeSelect,
+ errors,
+ labels,
+ renderRecipientSearch,
+ renderStaticRecipient,
+ renderAppPicker,
+ escalateHelp,
+}: EscalateActionFieldsProps) {
+ const recipientTypeLabels: Record = {
+ [GuardrailRecipientType.User]: labels.recipientUserLabel,
+ [GuardrailRecipientType.Group]: labels.recipientGroupLabel,
+ [GuardrailRecipientType.StaticEmail]: labels.recipientEmailLabel,
+ [GuardrailRecipientType.StaticGroupName]: labels.recipientGroupNameLabel,
+ };
+
+ const recipientType = action.recipient.type;
+ // The type select offers the four base entries; asset variants (a host-slot concern)
+ // display as their static siblings so the selection never blanks.
+ const displayedRecipientType =
+ recipientType === GuardrailRecipientType.AssetEmail
+ ? GuardrailRecipientType.StaticEmail
+ : recipientType === GuardrailRecipientType.AssetGroupName
+ ? GuardrailRecipientType.StaticGroupName
+ : recipientType;
+
+ const handleRecipientTypeChange = useCallback(
+ (value: string) => {
+ const newType = Number(value) as GuardrailEscalateRecipient['type'];
+ let recipient: GuardrailEscalateRecipient;
+
+ switch (newType) {
+ case GuardrailRecipientType.Group:
+ recipient = { type: GuardrailRecipientType.Group, value: '', displayName: '' };
+ break;
+ case GuardrailRecipientType.StaticEmail:
+ recipient = { type: GuardrailRecipientType.StaticEmail, value: '' };
+ break;
+ case GuardrailRecipientType.StaticGroupName:
+ recipient = { type: GuardrailRecipientType.StaticGroupName, value: '' };
+ break;
+ default:
+ recipient = { type: GuardrailRecipientType.User, value: '', displayName: '' };
+ }
+
+ onChange({ ...action, recipient });
+ },
+ [action, onChange]
+ );
+
+ const handleRecipientSelect = useCallback(
+ (selection: { value: string; displayName: string }) => {
+ const r = action.recipient;
+ if (r.type === GuardrailRecipientType.User || r.type === GuardrailRecipientType.Group) {
+ onChange({
+ ...action,
+ recipient: { ...r, value: selection.value, displayName: selection.displayName },
+ });
+ }
+ },
+ [action, onChange]
+ );
+
+ const handleRecipientClear = useCallback(() => {
+ const r = action.recipient;
+ if (r.type === GuardrailRecipientType.User || r.type === GuardrailRecipientType.Group) {
+ onChange({ ...action, recipient: { ...r, value: '', displayName: '' } });
+ }
+ }, [action, onChange]);
+
+ const handleTextValueChange = useCallback(
+ (value: string) => {
+ const r = action.recipient;
+ if (
+ r.type === GuardrailRecipientType.StaticEmail ||
+ r.type === GuardrailRecipientType.StaticGroupName
+ ) {
+ onChange({ ...action, recipient: { ...r, value } });
+ } else if (
+ r.type === GuardrailRecipientType.AssetEmail ||
+ r.type === GuardrailRecipientType.AssetGroupName
+ ) {
+ onChange({ ...action, recipient: { ...r, assetName: value } });
+ }
+ },
+ [action, onChange]
+ );
+
+ const recipientValue =
+ 'value' in action.recipient ? action.recipient.value : action.recipient.assetName;
+ const recipientDisplayValue =
+ ('displayName' in action.recipient ? action.recipient.displayName : '') || recipientValue;
+ const isSearchable =
+ recipientType === GuardrailRecipientType.User || recipientType === GuardrailRecipientType.Group;
+ const searchKind = recipientType === GuardrailRecipientType.User ? 'user' : 'group';
+ const searchPlaceholder =
+ searchKind === 'user' ? labels.userSearchPlaceholder : labels.groupSearchPlaceholder;
+
+ const appPickerCtx: GuardrailAppPickerContext = {
+ app: action.app.name ? action.app : null,
+ onChange: (app) => onChange({ ...action, app: app ?? { id: '', version: '', name: '' } }),
+ label: labels.actionAppLabel,
+ error: errors?.actionApp,
+ };
+
+ const fields = (
+ <>
+ {/* Recipient type */}
+
+ {labels.assignToLabel}
+
+
+
+
+
+ {Object.entries(recipientTypeLabels).map(([typeValue, label]) => (
+
+ {label}
+
+ ))}
+
+
+
+
+ {/* Recipient value */}
+
+
+ {recipientTypeLabels[displayedRecipientType] ?? labels.recipientFallbackLabel}
+
+
+ {isSearchable ? (
+ <>
+ {renderRecipientSearch ? (
+ renderRecipientSearch({
+ kind: searchKind,
+ displayValue: recipientDisplayValue,
+ placeholder: searchPlaceholder,
+ invalid: Boolean(errors?.recipient),
+ onSelect: handleRecipientSelect,
+ onClear: handleRecipientClear,
+ })
+ ) : (
+ // Fallback without a host directory search: a plain input writing the value directly.
+
+ handleRecipientSelect({ value: e.target.value, displayName: e.target.value })
+ }
+ placeholder={searchPlaceholder}
+ aria-invalid={errors?.recipient ? true : undefined}
+ />
+ )}
+ {errors?.recipient}
+ >
+ ) : (
+ (() => {
+ const staticNode = renderStaticRecipient?.({
+ kind:
+ displayedRecipientType === GuardrailRecipientType.StaticEmail
+ ? 'email'
+ : 'groupName',
+ recipient: action.recipient,
+ label: recipientTypeLabels[displayedRecipientType] ?? labels.recipientFallbackLabel,
+ invalid: Boolean(errors?.recipient),
+ error: errors?.recipient,
+ onChange: (recipient) => onChange({ ...action, recipient }),
+ });
+ if (staticNode !== undefined) {
+ return (
+ <>
+ {staticNode}
+ {errors?.recipient}
+ >
+ );
+ }
+ return (
+ handleTextValueChange(e.target.value)}
+ placeholder={
+ displayedRecipientType === GuardrailRecipientType.StaticEmail
+ ? labels.emailPlaceholder
+ : labels.groupNamePlaceholder
+ }
+ error={errors?.recipient}
+ />
+ );
+ })()
+ )}
+
+
+ {/* Action app picker (host capability) */}
+
+ {renderAppPicker ? (
+ renderAppPicker(appPickerCtx)
+ ) : (
+ <>
+
+ {labels.actionAppLabel}
+
+
+
+
+ {labels.appPickerUnavailable}
+
+ >
+ )}
+
+ >
+ );
+
+ return (
+
+
+ {actionTypeSelect}
+ {fields}
+
+ {escalateHelp}
+
+ );
+}
diff --git a/packages/apollo-react/src/canvas/components/Guardrails/components/field-shell.test.tsx b/packages/apollo-react/src/canvas/components/Guardrails/components/field-shell.test.tsx
new file mode 100644
index 0000000000..eb57307445
--- /dev/null
+++ b/packages/apollo-react/src/canvas/components/Guardrails/components/field-shell.test.tsx
@@ -0,0 +1,13 @@
+import { render, screen } from '@testing-library/react';
+import { describe, expect, it } from 'vitest';
+import { FieldShell } from './field-shell';
+
+describe('FieldShell', () => {
+ it('renders the error border only when invalid', () => {
+ const { rerender } = render( );
+ expect(screen.getByTestId('shell')).not.toHaveClass('border-error');
+
+ rerender( );
+ expect(screen.getByTestId('shell')).toHaveClass('border-error');
+ });
+});
diff --git a/packages/apollo-react/src/canvas/components/Guardrails/components/field-shell.tsx b/packages/apollo-react/src/canvas/components/Guardrails/components/field-shell.tsx
new file mode 100644
index 0000000000..7658c73d11
--- /dev/null
+++ b/packages/apollo-react/src/canvas/components/Guardrails/components/field-shell.tsx
@@ -0,0 +1,28 @@
+import { cn } from '@uipath/apollo-wind';
+import * as React from 'react';
+
+export interface FieldShellProps extends React.ComponentPropsWithoutRef<'div'> {
+ /** Renders the error border (uses the `error` token, matching aria-invalid controls). */
+ invalid?: boolean;
+}
+
+/** Input-look container for non-input controls (chip groups) in the guardrail forms. */
+const FieldShell = React.forwardRef(
+ ({ invalid = false, className, children, ...props }, ref) => (
+
+ {children}
+
+ )
+);
+FieldShell.displayName = 'FieldShell';
+
+export { FieldShell };
diff --git a/packages/apollo-react/src/canvas/components/Guardrails/components/guardrail-action-section.test.tsx b/packages/apollo-react/src/canvas/components/Guardrails/components/guardrail-action-section.test.tsx
new file mode 100644
index 0000000000..b2a8a31283
--- /dev/null
+++ b/packages/apollo-react/src/canvas/components/Guardrails/components/guardrail-action-section.test.tsx
@@ -0,0 +1,121 @@
+import { fireEvent, render, screen } from '@testing-library/react';
+import { axe } from 'jest-axe';
+import { describe, expect, it, vi } from 'vitest';
+import { type GuardrailAction, GuardrailRecipientType } from '../builder-types';
+import { GUARDRAIL_BUILDER_EN_LABELS } from '../i18n';
+import { GuardrailActionSection } from './guardrail-action-section';
+
+const labels = GUARDRAIL_BUILDER_EN_LABELS;
+const logAction: GuardrailAction = { $actionType: 'log', severityLevel: 'Info' };
+
+describe('GuardrailActionSection', () => {
+ it('renders the action type select with the current value', () => {
+ render( );
+
+ expect(screen.getByText('Action type')).toBeInTheDocument();
+ expect(screen.getByRole('combobox', { name: /action type/i })).toHaveTextContent('Log');
+ });
+
+ it('resets the payload when the action type changes', async () => {
+ const onActionChange = vi.fn();
+ render(
+
+ );
+
+ fireEvent.click(screen.getByRole('combobox', { name: /action type/i }));
+ fireEvent.click(await screen.findByRole('option', { name: 'Block' }));
+
+ expect(onActionChange).toHaveBeenCalledWith({ $actionType: 'block', reason: '' });
+ });
+
+ it('offers Filter only when showFilter is set', async () => {
+ const { unmount } = render(
+
+ );
+ fireEvent.click(screen.getByRole('combobox', { name: /action type/i }));
+ expect(await screen.findByRole('option', { name: 'Log' })).toBeInTheDocument();
+ expect(screen.queryByRole('option', { name: 'Filter' })).not.toBeInTheDocument();
+ unmount();
+
+ render(
+
+ );
+ fireEvent.click(screen.getByRole('combobox', { name: /action type/i }));
+ expect(await screen.findByRole('option', { name: 'Filter' })).toBeInTheDocument();
+ });
+
+ it('renders the severity select for log actions and reports changes', async () => {
+ const onActionChange = vi.fn();
+ render(
+
+ );
+
+ fireEvent.click(screen.getByRole('combobox', { name: /severity level/i }));
+ fireEvent.click(await screen.findByRole('option', { name: 'Warning' }));
+
+ expect(onActionChange).toHaveBeenCalledWith({ $actionType: 'log', severityLevel: 'Warning' });
+ });
+
+ it('renders the blocking reason input with its error for block actions', () => {
+ const onActionChange = vi.fn();
+ render(
+
+ );
+
+ const input = screen.getByLabelText(/blocking reason/i);
+ fireEvent.change(input, { target: { value: 'no PII' } });
+ expect(onActionChange).toHaveBeenCalledWith({ $actionType: 'block', reason: 'no PII' });
+ expect(screen.getByText('Block reason is required')).toBeInTheDocument();
+ });
+
+ it('renders filterContent and its error for filter actions', () => {
+ render(
+ }
+ errors={{ filterFields: 'Fields selection is required' }}
+ labels={labels}
+ />
+ );
+
+ expect(screen.getByTestId('filter-slot')).toBeInTheDocument();
+ expect(screen.getByText('Fields selection is required')).toBeInTheDocument();
+ });
+
+ it('delegates escalate actions to the escalation fields', () => {
+ render(
+
+ );
+
+ expect(screen.getByText('Assign to')).toBeInTheDocument();
+ expect(screen.getByText('Action App')).toBeInTheDocument();
+ });
+
+ it('has no accessibility violations', async () => {
+ const { container } = render(
+
+ );
+ const results = await axe(container);
+ expect(results).toHaveNoViolations();
+ });
+});
diff --git a/packages/apollo-react/src/canvas/components/Guardrails/components/guardrail-action-section.tsx b/packages/apollo-react/src/canvas/components/Guardrails/components/guardrail-action-section.tsx
new file mode 100644
index 0000000000..2fff55ac0c
--- /dev/null
+++ b/packages/apollo-react/src/canvas/components/Guardrails/components/guardrail-action-section.tsx
@@ -0,0 +1,153 @@
+import {
+ FormField,
+ FormFieldError,
+ Input,
+ Label,
+ RequiredIndicator,
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from '@uipath/apollo-wind';
+import type { ReactNode } from 'react';
+import type {
+ GuardrailAction,
+ GuardrailAppPickerContext,
+ GuardrailRecipientSearchContext,
+ GuardrailStaticRecipientContext,
+} from '../builder-types';
+import { createDefaultGuardrailAction } from '../builder-utils';
+import type { GuardrailBuilderLabels } from '../i18n';
+import { EscalateActionFields } from './escalate-action-fields';
+
+export interface GuardrailActionSectionProps {
+ action: GuardrailAction;
+ onActionChange: (action: GuardrailAction) => void;
+ /** Whether to include 'filter' as an option (custom guardrails only) */
+ showFilter?: boolean;
+ /** Content rendered as the second grid column when $actionType === 'filter' */
+ filterContent?: ReactNode;
+ errors?: { blockReason?: string; filterFields?: string; recipient?: string; actionApp?: string };
+ labels: GuardrailBuilderLabels;
+ renderRecipientSearch?: (ctx: GuardrailRecipientSearchContext) => ReactNode;
+ renderStaticRecipient?: (ctx: GuardrailStaticRecipientContext) => ReactNode | undefined;
+ renderAppPicker?: (ctx: GuardrailAppPickerContext) => ReactNode;
+ escalateHelp?: ReactNode;
+}
+
+/**
+ * Action section of the guardrail builder: a 2-column grid of action-type select + the
+ * type-dependent secondary field. Switching the type resets the action payload. Escalate
+ * expands into the full escalation layout.
+ */
+export function GuardrailActionSection({
+ action,
+ onActionChange,
+ showFilter = false,
+ filterContent,
+ errors,
+ labels,
+ renderRecipientSearch,
+ renderStaticRecipient,
+ renderAppPicker,
+ escalateHelp,
+}: GuardrailActionSectionProps) {
+ const actionTypeSelect = (
+
+
+ {labels.actionTypeLabel}
+
+
+
+ onActionChange(createDefaultGuardrailAction(type as GuardrailAction['$actionType']))
+ }
+ >
+
+
+
+
+ {labels.actionLogLabel}
+ {labels.actionBlockLabel}
+ {showFilter && {labels.actionFilterLabel} }
+ {labels.actionEscalateLabel}
+
+
+
+ );
+
+ if (action.$actionType === 'escalate') {
+ return (
+
+ );
+ }
+
+ return (
+
+
+ {actionTypeSelect}
+
+ {action.$actionType === 'log' && (
+
+
+ {labels.severityLabel}
+
+
+
+ onActionChange({ ...action, severityLevel: v } as GuardrailAction)
+ }
+ >
+
+
+
+
+ {labels.severityInfoLabel}
+ {labels.severityWarningLabel}
+ {labels.severityErrorLabel}
+
+
+
+ )}
+
+ {action.$actionType === 'block' && (
+
+
+ {labels.blockReasonLabel}
+
+
+
+ onActionChange({ ...action, reason: e.target.value } as GuardrailAction)
+ }
+ placeholder={labels.blockReasonPlaceholder}
+ error={errors?.blockReason}
+ />
+
+ )}
+
+ {action.$actionType === 'filter' && (
+
+ {filterContent}
+ {errors?.filterFields}
+
+ )}
+
+
+ );
+}
diff --git a/packages/apollo-react/src/canvas/components/Guardrails/components/guardrail-chip.test.tsx b/packages/apollo-react/src/canvas/components/Guardrails/components/guardrail-chip.test.tsx
new file mode 100644
index 0000000000..da7948f865
--- /dev/null
+++ b/packages/apollo-react/src/canvas/components/Guardrails/components/guardrail-chip.test.tsx
@@ -0,0 +1,45 @@
+import { fireEvent, render, screen } from '@testing-library/react';
+import { axe } from 'jest-axe';
+import { describe, expect, it, vi } from 'vitest';
+import { GuardrailChip } from './guardrail-chip';
+
+describe('GuardrailChip', () => {
+ it('renders a pressed toggle button', () => {
+ render(Email );
+ expect(screen.getByRole('button', { name: 'Email', pressed: true })).toBeInTheDocument();
+ });
+
+ it('reports presses', () => {
+ const onPressedChange = vi.fn();
+ render(
+
+ Email
+
+ );
+
+ fireEvent.click(screen.getByRole('button', { name: 'Email', pressed: false }));
+ expect(onPressedChange).toHaveBeenCalledWith(true);
+ });
+
+ it('renders the addable appearance with a dashed border', () => {
+ render(
+
+ ToolB
+
+ );
+ expect(screen.getByRole('button', { name: 'ToolB' })).toHaveClass('border-dashed');
+ });
+
+ it('has no accessibility violations', async () => {
+ const { container } = render(
+ <>
+ On
+
+ Addable
+
+ >
+ );
+ const results = await axe(container);
+ expect(results).toHaveNoViolations();
+ });
+});
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
new file mode 100644
index 0000000000..81a35a2465
--- /dev/null
+++ b/packages/apollo-react/src/canvas/components/Guardrails/components/guardrail-chip.tsx
@@ -0,0 +1,47 @@
+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';
+
+const guardrailChipVariants = cva(
+ // Geometry over Toggle's base, which contributes the focus ring, disabled handling and the
+ // data-[state] hooks.
+ `h-6 ${GUARDRAIL_CHIP_GEOMETRY}`,
+ {
+ variants: {
+ appearance: {
+ default:
+ 'bg-background text-foreground border-border hover:bg-muted hover:text-foreground data-[state=on]:bg-brand-subtle data-[state=on]:text-foreground-accent data-[state=on]:border-brand-lighter',
+ // Dashed affordance for items that can be added but are not currently targeted.
+ addable:
+ 'border-dashed bg-background text-foreground-muted border-border hover:bg-muted hover:text-foreground',
+ },
+ },
+ defaultVariants: { appearance: 'default' },
+ }
+);
+
+export interface GuardrailChipProps
+ extends React.ComponentPropsWithoutRef,
+ VariantProps {}
+
+/** Toggleable pill used for scopes, entities, and tool targeting in the guardrail forms. */
+const GuardrailChip = React.forwardRef(
+ ({ appearance, className, ...props }, ref) => (
+
+ )
+);
+GuardrailChip.displayName = 'GuardrailChip';
+
+export { GuardrailChip, guardrailChipVariants };
diff --git a/packages/apollo-react/src/canvas/components/Guardrails/components/guardrail-list-row.test.tsx b/packages/apollo-react/src/canvas/components/Guardrails/components/guardrail-list-row.test.tsx
new file mode 100644
index 0000000000..2af8d9010e
--- /dev/null
+++ b/packages/apollo-react/src/canvas/components/Guardrails/components/guardrail-list-row.test.tsx
@@ -0,0 +1,249 @@
+import { fireEvent, render, screen } from '@testing-library/react';
+import { axe } from 'jest-axe';
+import { describe, expect, it, vi } from 'vitest';
+import {
+ BYO_GUARDRAIL,
+ CUSTOM_GUARDRAIL,
+ DEFINITIONS,
+ PII_GUARDRAIL,
+} from '../__fixtures__/guardrail-list.fixtures';
+import { resolveGuardrailListItemState } from '../guardrail-list-utils';
+import { GUARDRAIL_LIST_EN_LABELS } from '../i18n';
+import type { GuardrailListItem } from '../list-types';
+import { GuardrailListRow, type GuardrailListRowProps } from './guardrail-list-row';
+
+const labels = GUARDRAIL_LIST_EN_LABELS;
+
+function renderRow(
+ item: GuardrailListItem = PII_GUARDRAIL,
+ props: Partial = {},
+ definitions = DEFINITIONS
+) {
+ return render(
+
+ );
+}
+
+describe('GuardrailListRow', () => {
+ it('renders the name, description, action and scopes', () => {
+ renderRow();
+
+ expect(screen.getByText('PII detection 1')).toBeInTheDocument();
+ expect(screen.getByText('Scans agent output for personal data.')).toBeInTheDocument();
+ expect(screen.getByText('log')).toBeInTheDocument();
+ expect(screen.getByText('Agent, Tool')).toBeInTheDocument();
+ });
+
+ it('renders the BYO connector as the provider line', () => {
+ renderRow(BYO_GUARDRAIL);
+
+ expect(screen.getByText('Provider: Noma Security')).toBeInTheDocument();
+ });
+
+ it('falls back to the unknown-action label when a row has no action', () => {
+ renderRow({ name: 'Legacy guardrail' });
+
+ expect(screen.getByText('Unknown')).toBeInTheDocument();
+ });
+
+ it('renders no action or scopes line when both are formatted away', () => {
+ renderRow(PII_GUARDRAIL, { formatAction: () => null, formatScopes: () => null });
+
+ expect(screen.queryByText('log')).not.toBeInTheDocument();
+ expect(screen.queryByText('Agent, Tool')).not.toBeInTheDocument();
+ });
+
+ it('lets the host localize the scopes and the action', () => {
+ renderRow(PII_GUARDRAIL, {
+ formatScopes: (item) => `Scopes: ${item.selector?.scopes?.length}`,
+ formatAction: () => 'Protokollieren',
+ });
+
+ expect(screen.getByText('Scopes: 2')).toBeInTheDocument();
+ expect(screen.getByText('Protokollieren')).toBeInTheDocument();
+ });
+
+ describe('chips', () => {
+ it('renders no chips by default', () => {
+ renderRow(PII_GUARDRAIL, { statusChips: false, administration: 'governance' });
+
+ expect(screen.queryByText('Governance managed')).not.toBeInTheDocument();
+ });
+
+ it('renders the status and administration chips when asked', () => {
+ renderRow(BYO_GUARDRAIL, { statusChips: true, administration: 'governance' }, [
+ { validator: 'byo', status: 'Unauthorised', byoValidatorName: 'noma_prompt_injection' },
+ ]);
+
+ expect(screen.getByText('Unauthorized')).toBeInTheDocument();
+ expect(screen.getByText('Governance managed')).toBeInTheDocument();
+ });
+
+ it('renders the preview badge only for built-in validators, and only when asked', () => {
+ const { unmount } = renderRow(PII_GUARDRAIL, { previewChip: true });
+ expect(screen.getByText('Preview')).toBeInTheDocument();
+ unmount();
+
+ renderRow(CUSTOM_GUARDRAIL, { previewChip: true });
+ expect(screen.queryByText('Preview')).not.toBeInTheDocument();
+ });
+ });
+
+ describe('BYO notices', () => {
+ it('announces a disabled configuration', () => {
+ renderRow(BYO_GUARDRAIL, {}, [
+ { validator: 'byo', status: 'Disabled', byoValidatorName: 'noma_prompt_injection' },
+ ]);
+
+ expect(screen.getByRole('alert')).toHaveTextContent(
+ "This guardrail's configuration has been disabled"
+ );
+ });
+
+ it('announces a configuration that no longer resolves', () => {
+ renderRow(BYO_GUARDRAIL, {}, [{ validator: 'pii_detection', status: 'Available' }]);
+
+ expect(screen.getByRole('alert')).toHaveTextContent(
+ "This guardrail's configuration is no longer available"
+ );
+ });
+
+ it('shows neither notice for a healthy row', () => {
+ renderRow(BYO_GUARDRAIL);
+
+ expect(screen.queryByRole('alert')).not.toBeInTheDocument();
+ });
+ });
+
+ describe('actions', () => {
+ it('renders only the actions the host wired up', () => {
+ renderRow(PII_GUARDRAIL, { onEdit: vi.fn() });
+
+ expect(screen.getByRole('button', { name: 'Edit guardrail' })).toBeInTheDocument();
+ expect(screen.queryByRole('button', { name: 'Remove guardrail' })).not.toBeInTheDocument();
+ });
+
+ it('reports edit and remove intents with the row', () => {
+ const onEdit = vi.fn();
+ const onRemove = vi.fn();
+ renderRow(PII_GUARDRAIL, { onEdit, onRemove });
+
+ fireEvent.click(screen.getByRole('button', { name: 'Edit guardrail' }));
+ fireEvent.click(screen.getByRole('button', { name: 'Remove guardrail' }));
+
+ expect(onEdit).toHaveBeenCalledWith(PII_GUARDRAIL);
+ expect(onRemove).toHaveBeenCalledWith(PII_GUARDRAIL);
+ });
+
+ it('disables both actions when the list is read-only', () => {
+ const onEdit = vi.fn();
+ renderRow(PII_GUARDRAIL, { onEdit, onRemove: vi.fn(), disabled: true });
+
+ const edit = screen.getByRole('button', { name: 'Edit guardrail' });
+ expect(edit).toBeDisabled();
+ fireEvent.click(edit);
+ expect(onEdit).not.toHaveBeenCalled();
+ });
+
+ it('hands the slot the row, its position and the default actions', () => {
+ const renderItemActions = vi.fn(() => More options );
+ renderRow(PII_GUARDRAIL, { onEdit: vi.fn(), index: 2, renderItemActions });
+
+ expect(screen.getByRole('button', { name: 'More options' })).toBeInTheDocument();
+ expect(screen.queryByRole('button', { name: 'Edit guardrail' })).not.toBeInTheDocument();
+ expect(renderItemActions).toHaveBeenCalledWith(
+ expect.objectContaining({ item: PII_GUARDRAIL, id: 'g1', index: 2, disabled: false })
+ );
+ });
+
+ it('lets a slot render the default actions alongside its own', () => {
+ renderRow(PII_GUARDRAIL, {
+ onEdit: vi.fn(),
+ renderItemActions: ({ defaultActions }) => (
+ <>
+ {defaultActions}
+ More options
+ >
+ ),
+ });
+
+ expect(screen.getByRole('button', { name: 'Edit guardrail' })).toBeInTheDocument();
+ expect(screen.getByRole('button', { name: 'More options' })).toBeInTheDocument();
+ });
+ });
+
+ describe('rowActivatesEdit', () => {
+ it('does not make the body activatable by default', () => {
+ renderRow(PII_GUARDRAIL, { onEdit: vi.fn() });
+
+ expect(
+ screen.queryByRole('button', { name: 'Edit PII detection 1' })
+ ).not.toBeInTheDocument();
+ });
+
+ it('opens the editor on click and on Enter or Space', () => {
+ const onEdit = vi.fn();
+ renderRow(PII_GUARDRAIL, { onEdit, rowActivatesEdit: true });
+
+ const body = screen.getByRole('button', { name: 'Edit PII detection 1' });
+ fireEvent.click(body);
+ fireEvent.keyDown(body, { key: 'Enter' });
+ fireEvent.keyDown(body, { key: ' ' });
+
+ expect(onEdit).toHaveBeenCalledTimes(3);
+ });
+
+ it('stays inert while read-only', () => {
+ renderRow(PII_GUARDRAIL, { onEdit: vi.fn(), rowActivatesEdit: true, disabled: true });
+
+ expect(
+ screen.queryByRole('button', { name: 'Edit PII detection 1' })
+ ).not.toBeInTheDocument();
+ });
+
+ it('keeps the row actions out of the activatable body, so no button nests in a button', async () => {
+ const { container } = renderRow(PII_GUARDRAIL, {
+ onEdit: vi.fn(),
+ onRemove: vi.fn(),
+ rowActivatesEdit: true,
+ });
+
+ const body = screen.getByRole('button', { name: 'Edit PII detection 1' });
+ expect(body.querySelector('button')).toBeNull();
+ expect(await axe(container)).toHaveNoViolations();
+ });
+ });
+
+ it('renders the host tooltip over the row body', () => {
+ renderRow(PII_GUARDRAIL, { renderRowTooltip: (item) => `About ${item.name}` });
+
+ // Radix keeps the content unmounted until it opens; what matters here is that a row
+ // brings its own provider, so mounting a tooltip outside one does not throw.
+ expect(screen.getByText('PII detection 1')).toBeInTheDocument();
+ });
+
+ it('renders the drag handle the list passes in', () => {
+ renderRow(PII_GUARDRAIL, { handle: Reorder guardrail });
+
+ expect(screen.getByRole('button', { name: 'Reorder guardrail' })).toBeInTheDocument();
+ });
+
+ it('has no accessibility violations', async () => {
+ const { container } = renderRow(BYO_GUARDRAIL, {
+ onEdit: vi.fn(),
+ onRemove: vi.fn(),
+ statusChips: true,
+ previewChip: true,
+ administration: 'governance',
+ });
+
+ expect(await axe(container)).toHaveNoViolations();
+ });
+});
diff --git a/packages/apollo-react/src/canvas/components/Guardrails/components/guardrail-list-row.tsx b/packages/apollo-react/src/canvas/components/Guardrails/components/guardrail-list-row.tsx
new file mode 100644
index 0000000000..0ee9cfbfd7
--- /dev/null
+++ b/packages/apollo-react/src/canvas/components/Guardrails/components/guardrail-list-row.tsx
@@ -0,0 +1,248 @@
+import {
+ Badge,
+ Button,
+ cn,
+ Tooltip,
+ TooltipContent,
+ TooltipProvider,
+ TooltipTrigger,
+} from '@uipath/apollo-wind';
+import { Pencil, Trash2 } from 'lucide-react';
+import * as React from 'react';
+import { getGuardrailListChips } from '../guardrail-list-utils';
+import { formatGuardrailFormMessage, type GuardrailListLabels } from '../i18n';
+import type {
+ GuardrailListAdministration,
+ GuardrailListItem,
+ GuardrailListItemActionsContext,
+ GuardrailListItemState,
+} from '../list-types';
+import { GuardrailStatusChip } from './guardrail-status-chip';
+
+export interface GuardrailListRowProps
+ extends Omit, 'children'> {
+ item: GuardrailListItem;
+ /** The row's resolved id, and its index in the rendered list. */
+ id: string;
+ index: number;
+ /** What resolving the row against the definitions produced. */
+ state: GuardrailListItemState;
+ labels: GuardrailListLabels;
+ /** Read-only: edit and remove are disabled. Reorder is the list's decision, not the row's. */
+ disabled?: boolean;
+ /** Drag handle, already wired to the sortable. Absent when the list cannot be reordered. */
+ handle?: React.ReactNode;
+ administration?: GuardrailListAdministration;
+ statusChips?: boolean;
+ previewChip?: boolean;
+ /** Makes the row body a button that opens the editor (Agents' behaviour). */
+ rowActivatesEdit?: boolean;
+ onEdit?: (item: GuardrailListItem) => void;
+ onRemove?: (item: GuardrailListItem) => void;
+ renderItemActions?: (ctx: GuardrailListItemActionsContext) => React.ReactNode;
+ /** Hover content for the row body. Return nothing to leave the row untooltipped. */
+ renderRowTooltip?: (item: GuardrailListItem) => React.ReactNode;
+ formatScopes?: (item: GuardrailListItem) => React.ReactNode;
+ formatAction?: (item: GuardrailListItem) => React.ReactNode;
+}
+
+/**
+ * One guardrail row: drag handle, name with its lifecycle and status chips, BYO notices,
+ * description, provider line, action badge and scopes, then the row actions.
+ *
+ * Presentational and reorder-free. The sortable wrapper lives in `guardrail-list.tsx` and
+ * reaches the row through `ref`, `style` and `handle`, so this renders identically whether or
+ * not the list is reorderable, and the a11y suites can mount it without a `DndContext`.
+ */
+const GuardrailListRow = React.forwardRef(
+ (
+ {
+ item,
+ id,
+ index,
+ state,
+ labels,
+ disabled = false,
+ handle,
+ administration,
+ statusChips = false,
+ previewChip = false,
+ rowActivatesEdit = false,
+ onEdit,
+ onRemove,
+ renderItemActions,
+ renderRowTooltip,
+ formatScopes,
+ formatAction,
+ className,
+ ...props
+ },
+ ref
+ ) => {
+ const handleEdit = onEdit && !disabled ? () => onEdit(item) : undefined;
+ const handleRemove = onRemove && !disabled ? () => onRemove(item) : undefined;
+
+ const chips = statusChips
+ ? getGuardrailListChips({ status: state.status, administration }, labels)
+ : [];
+ const isBuiltInValidator = item.$guardrailType === 'builtInValidator';
+
+ const scopes = item.selector?.scopes ?? [];
+ // Presence of the prop decides, never the value: a host hides the line by returning null
+ // (Agents renders scopes in its own tooltip instead), which `??` would swallow.
+ const scopesContent = formatScopes ? formatScopes(item) : scopes.join(', ') || null;
+ const actionContent = formatAction
+ ? formatAction(item)
+ : (item.action?.$actionType ?? labels.actionUnknown);
+
+ const defaultActions = (
+ <>
+ {onEdit && (
+
+
+
+ )}
+ {onRemove && (
+
+
+
+ )}
+ >
+ );
+
+ const actions = renderItemActions
+ ? renderItemActions({
+ item,
+ id,
+ index,
+ disabled,
+ onEdit: handleEdit,
+ onRemove: handleRemove,
+ defaultActions,
+ })
+ : defaultActions;
+
+ // Only the body carries the role: the handle and the actions are siblings, so an
+ // activatable row never nests interactive controls inside a button.
+ const bodyProps =
+ rowActivatesEdit && handleEdit
+ ? {
+ role: 'button',
+ tabIndex: 0,
+ 'aria-label': formatGuardrailFormMessage(labels.editRow, { name: item.name }),
+ onClick: handleEdit,
+ onKeyDown: (event: React.KeyboardEvent) => {
+ if (event.key === 'Enter' || event.key === ' ') {
+ event.preventDefault();
+ handleEdit();
+ }
+ },
+ }
+ : {};
+
+ const body = (
+
+
+ {item.name}
+ {previewChip && isBuiltInValidator && (
+ {labels.preview}
+ )}
+ {chips.map((chip) => (
+
+ {chip.label}
+
+ ))}
+
+ {/* Both notices are ungated: both products already show them, and a guardrail that
+ cannot run is not an opt-in detail. */}
+ {state.byoDisabled && (
+
+ {labels.byoDisabledNotice}
+
+ )}
+ {state.byoUnavailable && (
+
+ {labels.byoUnavailableNotice}
+
+ )}
+ {item.description && (
+
{item.description}
+ )}
+ {state.provider !== undefined && (
+
+ {labels.provider}: {state.provider}
+
+ )}
+ {(actionContent || scopesContent) && (
+
+ {actionContent && (
+
+ {actionContent}
+
+ )}
+ {scopesContent && (
+ {scopesContent}
+ )}
+
+ )}
+
+ );
+
+ const tooltip = renderRowTooltip?.(item);
+
+ return (
+
+ {handle}
+ {tooltip ? (
+ // Own provider: wind's `Tooltip` is a bare Radix root and throws outside one, and a
+ // row renders where none is guaranteed. Nesting inside an existing provider only
+ // rescopes the delays.
+
+
+ {body}
+ {tooltip}
+
+
+ ) : (
+ body
+ )}
+
{actions}
+
+ );
+ }
+);
+GuardrailListRow.displayName = 'GuardrailListRow';
+
+export { GuardrailListRow };
diff --git a/packages/apollo-react/src/canvas/components/Guardrails/components/guardrail-palette-item.test.tsx b/packages/apollo-react/src/canvas/components/Guardrails/components/guardrail-palette-item.test.tsx
new file mode 100644
index 0000000000..4f41cbf841
--- /dev/null
+++ b/packages/apollo-react/src/canvas/components/Guardrails/components/guardrail-palette-item.test.tsx
@@ -0,0 +1,76 @@
+import { fireEvent, render, screen } from '@testing-library/react';
+import { axe } from 'jest-axe';
+import { describe, expect, it, vi } from 'vitest';
+import { GuardrailPaletteItem } from './guardrail-palette-item';
+import { GuardrailStatusChip } from './guardrail-status-chip';
+
+describe('GuardrailPaletteItem', () => {
+ it('renders the name, the description and the chips', () => {
+ render(
+ Preview}
+ onSelect={vi.fn()}
+ />
+ );
+
+ const item = screen.getByRole('button');
+ expect(item).toHaveTextContent('PII detection');
+ expect(item).toHaveTextContent('Detects personally identifiable information.');
+ expect(item).toHaveTextContent('Preview');
+ });
+
+ it('is a button, so Enter and Space select it without a key handler of our own', () => {
+ const onSelect = vi.fn();
+ render( );
+
+ const item = screen.getByRole('button', { name: 'PII detection' });
+ expect(item).toHaveAttribute('type', 'button');
+ fireEvent.click(item);
+ expect(onSelect).toHaveBeenCalledTimes(1);
+ });
+
+ describe('when disabled', () => {
+ it('does not select', () => {
+ const onSelect = vi.fn();
+ render( );
+
+ fireEvent.click(screen.getByRole('button', { name: 'Harmful content' }));
+ expect(onSelect).not.toHaveBeenCalled();
+ });
+
+ it('stays focusable, so the chip saying why is still reachable', () => {
+ render(
+ Unauthorized}
+ disabled
+ onSelect={vi.fn()}
+ />
+ );
+
+ const item = screen.getByRole('button', { name: /Harmful content/ });
+ expect(item).toHaveAttribute('aria-disabled', 'true');
+ expect(item).not.toBeDisabled();
+ item.focus();
+ expect(item).toHaveFocus();
+ });
+ });
+
+ it('has no accessibility violations', async () => {
+ const { container } = render(
+ <>
+
+ Unauthorized}
+ disabled
+ onSelect={vi.fn()}
+ />
+ >
+ );
+
+ expect(await axe(container)).toHaveNoViolations();
+ });
+});
diff --git a/packages/apollo-react/src/canvas/components/Guardrails/components/guardrail-palette-item.tsx b/packages/apollo-react/src/canvas/components/Guardrails/components/guardrail-palette-item.tsx
new file mode 100644
index 0000000000..7dab854ab6
--- /dev/null
+++ b/packages/apollo-react/src/canvas/components/Guardrails/components/guardrail-palette-item.tsx
@@ -0,0 +1,63 @@
+import { cn } from '@uipath/apollo-wind';
+import * as React from 'react';
+
+export interface GuardrailPaletteItemProps
+ extends Omit, 'children' | 'disabled' | 'onSelect'> {
+ /** Primary line: a definition's display name, or the create-custom label. */
+ name: string;
+ /** Secondary line, wrapped rather than truncated: the palette is where it is read. */
+ description?: string;
+ /** Chips rendered after the name (connector, lifecycle, status). */
+ chips?: React.ReactNode;
+ /** Leading glyph. Only the create-custom entry has one; definitions carry no icons. */
+ icon?: React.ReactNode;
+ /**
+ * Visible but not choosable (an `Unauthorised` definition). Implemented as `aria-disabled`
+ * rather than `disabled`, so the entry keeps its place in the tab order and a keyboard user
+ * still reaches the chip that says why it cannot be picked.
+ */
+ disabled?: boolean;
+ onSelect: () => void;
+}
+
+/**
+ * One palette entry: a real button, so it is reachable and operable from the keyboard.
+ *
+ * Presentational, and unaware of grouping. Agents' entry today is a `div` with
+ * `role="listitem"`, `tabIndex={0}` and its own key handler, which both puts a list item
+ * outside a list and reimplements what a button already does.
+ */
+const GuardrailPaletteItem = React.forwardRef(
+ ({ name, description, chips, icon, disabled = false, onSelect, className, ...props }, ref) => (
+
+ {icon && {icon} }
+
+
+ {name}
+ {chips}
+
+ {description && (
+
+ {description}
+
+ )}
+
+
+ )
+);
+GuardrailPaletteItem.displayName = 'GuardrailPaletteItem';
+
+export { GuardrailPaletteItem };
diff --git a/packages/apollo-react/src/canvas/components/Guardrails/components/guardrail-scope-selector.test.tsx b/packages/apollo-react/src/canvas/components/Guardrails/components/guardrail-scope-selector.test.tsx
new file mode 100644
index 0000000000..a6b1440dce
--- /dev/null
+++ b/packages/apollo-react/src/canvas/components/Guardrails/components/guardrail-scope-selector.test.tsx
@@ -0,0 +1,231 @@
+import { fireEvent, render, screen } from '@testing-library/react';
+import { axe } from 'jest-axe';
+import { describe, expect, it, vi } from 'vitest';
+import type { GuardrailSelector } from '../builder-types';
+import { GUARDRAIL_BUILDER_EN_LABELS } from '../i18n';
+import { GuardrailScopeSelector } from './guardrail-scope-selector';
+
+describe('GuardrailScopeSelector', () => {
+ const defaultProps = {
+ selector: { scopes: [] } as GuardrailSelector,
+ onChange: vi.fn(),
+ availableToolNames: ['ToolA', 'ToolB'],
+ labels: GUARDRAIL_BUILDER_EN_LABELS,
+ };
+
+ it('renders all scope buttons by default', () => {
+ render( );
+
+ expect(screen.getByText('Agent')).toBeInTheDocument();
+ expect(screen.getByText('LLM calls')).toBeInTheDocument();
+ expect(screen.getByText('Tools')).toBeInTheDocument();
+ });
+
+ it('renders only allowed scopes when provided', () => {
+ render( );
+
+ expect(screen.queryByText('Agent')).not.toBeInTheDocument();
+ expect(screen.queryByText('LLM calls')).not.toBeInTheDocument();
+ expect(screen.getByText('Tools')).toBeInTheDocument();
+ });
+
+ it('calls onChange when toggling a scope on', () => {
+ const onChange = vi.fn();
+ render( );
+
+ fireEvent.click(screen.getByText('Agent'));
+ expect(onChange).toHaveBeenCalledWith({ scopes: ['Agent'] });
+ });
+
+ it('calls onChange when toggling a scope off', () => {
+ const onChange = vi.fn();
+ render(
+
+ );
+
+ fireEvent.click(screen.getByText('Agent'));
+ expect(onChange).toHaveBeenCalledWith({ scopes: ['Llm'] });
+ });
+
+ it('pre-selects all tools when toggling Tool scope on', () => {
+ const onChange = vi.fn();
+ render( );
+
+ fireEvent.click(screen.getByText('Tools'));
+ expect(onChange).toHaveBeenCalledWith({ scopes: ['Tool'], matchNames: ['ToolA', 'ToolB'] });
+ });
+
+ it('shows tool selectors when Tool scope is selected', () => {
+ render(
+
+ );
+
+ expect(screen.getByText('ToolA')).toBeInTheDocument();
+ expect(screen.getByText('ToolB')).toBeInTheDocument();
+ });
+
+ it('does not show tool selectors when Tool scope is not selected', () => {
+ render( );
+
+ expect(screen.queryByText('ToolA')).not.toBeInTheDocument();
+ expect(screen.queryByText('ToolB')).not.toBeInTheDocument();
+ });
+
+ it('toggles individual tool when clicked', () => {
+ const onChange = vi.fn();
+ render(
+
+ );
+
+ fireEvent.click(screen.getByText('ToolA'));
+ expect(onChange).toHaveBeenCalledWith(expect.objectContaining({ matchNames: ['ToolB'] }));
+ });
+
+ it('marks only tools in matchNames as pressed; newly-added tools render unpressed (AL-428)', () => {
+ // A guardrail targeting only ToolA shouldn't read as "applied" for ToolB once ToolB
+ // joins the agent: ToolA stays pressed (targeted), ToolB renders unpressed (addable).
+ render(
+
+ );
+
+ expect(screen.getByRole('button', { name: 'ToolA', pressed: true })).toBeInTheDocument();
+ expect(screen.getByRole('button', { name: 'ToolB', pressed: false })).toBeInTheDocument();
+ });
+
+ it('clicking an unpressed tool adds it to matchNames', () => {
+ const onChange = vi.fn();
+ render(
+
+ );
+
+ fireEvent.click(screen.getByRole('button', { name: 'ToolB', pressed: false }));
+ expect(onChange).toHaveBeenCalledWith(
+ expect.objectContaining({ matchNames: ['ToolA', 'ToolB'] })
+ );
+ });
+
+ it('shows Scopes label with required indicator', () => {
+ render( );
+
+ expect(screen.getByText('Scopes')).toBeInTheDocument();
+ expect(screen.getAllByText('*').length).toBeGreaterThan(0);
+ });
+
+ it('does not show tool selectors when no tools are available', () => {
+ render(
+
+ );
+
+ expect(screen.queryByText('Tools', { selector: 'label' })).not.toBeInTheDocument();
+ });
+
+ it('hides Tool scope button when no tools are available', () => {
+ render(
+
+ );
+
+ expect(screen.getByText('Agent')).toBeInTheDocument();
+ expect(screen.getByText('LLM calls')).toBeInTheDocument();
+ expect(screen.queryByText('Tools')).not.toBeInTheDocument();
+ });
+
+ it('strips Tool scope via onChange when tools disappear while Tool is selected', () => {
+ const onChange = vi.fn();
+ const { rerender } = render(
+
+ );
+
+ expect(onChange).not.toHaveBeenCalled();
+
+ rerender(
+
+ );
+
+ expect(onChange).toHaveBeenCalledWith({ scopes: ['Agent'] });
+ });
+
+ it('does not call onChange when tools disappear but Tool scope is not selected', () => {
+ const onChange = vi.fn();
+ const { rerender } = render(
+
+ );
+
+ rerender(
+
+ );
+
+ expect(onChange).not.toHaveBeenCalled();
+ });
+
+ it('renders scope and tool errors', () => {
+ render(
+
+ );
+
+ expect(screen.getByText('At least one scope is required')).toBeInTheDocument();
+ expect(screen.getByText('At least one tool is required')).toBeInTheDocument();
+ });
+
+ it('has no accessibility violations', async () => {
+ const { container } = render(
+
+ );
+ const results = await axe(container);
+ expect(results).toHaveNoViolations();
+ });
+});
diff --git a/packages/apollo-react/src/canvas/components/Guardrails/components/guardrail-scope-selector.tsx b/packages/apollo-react/src/canvas/components/Guardrails/components/guardrail-scope-selector.tsx
new file mode 100644
index 0000000000..03cff2dafa
--- /dev/null
+++ b/packages/apollo-react/src/canvas/components/Guardrails/components/guardrail-scope-selector.tsx
@@ -0,0 +1,167 @@
+import { FormField, FormFieldError, Label, RequiredIndicator } from '@uipath/apollo-wind';
+import { Check, Plus } from 'lucide-react';
+import { useCallback, useEffect, useMemo } from 'react';
+import type { GuardrailScope, GuardrailSelector } from '../builder-types';
+import type { GuardrailBuilderLabels } from '../i18n';
+import { FieldShell } from './field-shell';
+import { GuardrailChip } from './guardrail-chip';
+
+const ALL_SCOPES: GuardrailScope[] = ['Agent', 'Llm', 'Tool'];
+
+export interface GuardrailScopeSelectorProps {
+ selector: GuardrailSelector;
+ onChange: (selector: GuardrailSelector) => void;
+ /** Tool names available for targeting (shown when Tool scope is selected) */
+ availableToolNames?: string[];
+ /** When provided, only these scopes are shown as options */
+ allowedScopes?: GuardrailScope[];
+ errors?: { scopes?: string; toolNames?: string };
+ labels: GuardrailBuilderLabels;
+}
+
+/**
+ * Scope selector for guardrails: which scopes (Agent, LLM, Tool) and optionally which tools
+ * a guardrail targets.
+ */
+export function GuardrailScopeSelector({
+ selector,
+ onChange,
+ availableToolNames = [],
+ allowedScopes,
+ errors,
+ labels,
+}: GuardrailScopeSelectorProps) {
+ const scopeLabels = useMemo>(
+ () => ({
+ Agent: labels.scopeAgentLabel,
+ Llm: labels.scopeLlmLabel,
+ Tool: labels.scopeToolLabel,
+ }),
+ [labels]
+ );
+ const selectedScopes = useMemo(() => selector.scopes ?? [], [selector.scopes]);
+ const hasTools = availableToolNames.length > 0;
+ const visibleScopes = useMemo(() => {
+ const scopes = allowedScopes ?? ALL_SCOPES;
+ return hasTools ? scopes : scopes.filter((s) => s !== 'Tool');
+ }, [allowedScopes, hasTools]);
+
+ // When tools disappear while Tool scope is selected, strip it from the selector to avoid
+ // invisible invalid state the user can't fix via the UI. (Deliberate self-healing side
+ // effect, kept for parity with the original implementation.)
+ useEffect(() => {
+ if (!hasTools && selectedScopes.includes('Tool')) {
+ const cleaned = selectedScopes.filter((s) => s !== 'Tool');
+ onChange({ scopes: cleaned });
+ }
+ }, [hasTools, selectedScopes, onChange]);
+
+ const handleToggleScope = useCallback(
+ (scope: GuardrailScope) => {
+ const isSelected = selectedScopes.includes(scope);
+ const newScopes = isSelected
+ ? selectedScopes.filter((s) => s !== scope)
+ : [...selectedScopes, scope];
+ const hasToolScope = newScopes.includes('Tool');
+ if (!hasToolScope) {
+ onChange({ scopes: newScopes });
+ } else if (!isSelected && scope === 'Tool') {
+ // Toggling Tool ON: pre-select all available tools
+ onChange({ scopes: newScopes, matchNames: [...availableToolNames] });
+ } else {
+ onChange({ scopes: newScopes, matchNames: selector.matchNames ?? [...availableToolNames] });
+ }
+ },
+ [selectedScopes, selector.matchNames, availableToolNames, onChange]
+ );
+
+ const handleToggleTool = useCallback(
+ (toolName: string) => {
+ const current = selector.matchNames ?? [];
+ const isSelected = current.includes(toolName);
+ const newMatchNames = isSelected
+ ? current.filter((n) => n !== toolName)
+ : [...current, toolName];
+ onChange({ ...selector, matchNames: newMatchNames });
+ },
+ [selector, onChange]
+ );
+
+ const selectedTools = useMemo(() => selector.matchNames ?? [], [selector.matchNames]);
+ const hasToolScope = selectedScopes.includes('Tool');
+
+ // Split tools into "currently targeted" (in matchNames) and "available to add" (in
+ // availableToolNames but not in matchNames). Distinct groups with Check vs Plus icons
+ // avoid a newly-added tool appearing "selected" merely by rendering alongside targeted
+ // ones with subtle styling differences.
+ const targetedTools = useMemo(
+ () => selectedTools.filter((name) => availableToolNames.includes(name)),
+ [selectedTools, availableToolNames]
+ );
+ const addableTools = useMemo(
+ () => availableToolNames.filter((name) => !selectedTools.includes(name)),
+ [availableToolNames, selectedTools]
+ );
+
+ return (
+
+ {/* Scope multi-select, always visible */}
+
+
+ {labels.scopesLabel}
+
+
+
+
+ {visibleScopes.map((scope) => (
+ handleToggleScope(scope)}
+ >
+ {scopeLabels[scope]}
+
+ ))}
+
+
+ {errors?.scopes}
+
+
+ {/* Tool name multi-select (only when Tool scope is selected) */}
+ {hasToolScope && availableToolNames.length > 0 && (
+
+
+ {labels.toolsLabel}
+
+
+
+
+ {targetedTools.map((name) => (
+
handleToggleTool(name)}
+ >
+
+ {name}
+
+ ))}
+ {addableTools.map((name) => (
+
handleToggleTool(name)}
+ >
+
+ {name}
+
+ ))}
+
+
+ {errors?.toolNames}
+
+ )}
+
+ );
+}
diff --git a/packages/apollo-react/src/canvas/components/Guardrails/components/guardrail-status-banner.test.tsx b/packages/apollo-react/src/canvas/components/Guardrails/components/guardrail-status-banner.test.tsx
new file mode 100644
index 0000000000..e146b457d7
--- /dev/null
+++ b/packages/apollo-react/src/canvas/components/Guardrails/components/guardrail-status-banner.test.tsx
@@ -0,0 +1,28 @@
+import { render, screen } from '@testing-library/react';
+import { axe } from 'jest-axe';
+import { describe, expect, it } from 'vitest';
+import { GuardrailStatusBanner } from './guardrail-status-banner';
+
+describe('GuardrailStatusBanner', () => {
+ it('renders an error banner with role="alert"', () => {
+ render( );
+
+ const banner = screen.getByRole('alert');
+ expect(banner).toHaveTextContent('This configuration has been disabled.');
+ });
+
+ it('renders a warning banner as a polite status region, not an alert', () => {
+ render(
+
+ );
+
+ expect(screen.queryByRole('alert')).not.toBeInTheDocument();
+ expect(screen.getByRole('status')).toHaveTextContent('You are not entitled to use guardrails.');
+ });
+
+ it('has no accessibility violations', async () => {
+ const { container } = render( );
+ const results = await axe(container);
+ expect(results).toHaveNoViolations();
+ });
+});
diff --git a/packages/apollo-react/src/canvas/components/Guardrails/components/guardrail-status-banner.tsx b/packages/apollo-react/src/canvas/components/Guardrails/components/guardrail-status-banner.tsx
new file mode 100644
index 0000000000..79604ef56e
--- /dev/null
+++ b/packages/apollo-react/src/canvas/components/Guardrails/components/guardrail-status-banner.tsx
@@ -0,0 +1,27 @@
+import { Alert, AlertDescription } from '@uipath/apollo-wind';
+import { AlertCircle, AlertTriangle } from 'lucide-react';
+
+export interface GuardrailStatusBannerProps {
+ tone: 'error' | 'warning';
+ message: string;
+}
+
+/** Status banner shown above the guardrail form (definition disabled / unauthorized / feature off). */
+export function GuardrailStatusBanner({ tone, message }: GuardrailStatusBannerProps) {
+ if (tone === 'error') {
+ return (
+
+
+ {message}
+
+ );
+ }
+ return (
+ // A warning is a persistent notice, not an interruption: role="status" (polite live
+ // region) instead of Alert's default role="alert".
+
+
+ {message}
+
+ );
+}
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 0000000000..199e0c70b1
--- /dev/null
+++ b/packages/apollo-react/src/canvas/components/Guardrails/components/guardrail-status-chip.test.tsx
@@ -0,0 +1,40 @@
+import { render, screen } from '@testing-library/react';
+import { axe } from 'jest-axe';
+import { describe, expect, it } from 'vitest';
+import { GuardrailStatusChip } from './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(screen.getByText('Disabled')).toHaveClass('rounded-full');
+ });
+
+ it.each([
+ ['neutral', 'bg-secondary'],
+ ['warning', 'bg-warning-background'],
+ ['error', 'bg-error-background'],
+ ] as const)('maps the %s tone onto the badge variant', (tone, expected) => {
+ render(Status );
+
+ expect(screen.getByText('Status')).toHaveClass(expected);
+ });
+
+ it('has no accessibility violations', async () => {
+ const { container } = render(
+ <>
+ Feature disabled
+ Unavailable
+ >
+ );
+
+ expect(await axe(container)).toHaveNoViolations();
+ });
+});
diff --git a/packages/apollo-react/src/canvas/components/Guardrails/components/guardrail-status-chip.tsx b/packages/apollo-react/src/canvas/components/Guardrails/components/guardrail-status-chip.tsx
new file mode 100644
index 0000000000..e39eb5f9e3
--- /dev/null
+++ b/packages/apollo-react/src/canvas/components/Guardrails/components/guardrail-status-chip.tsx
@@ -0,0 +1,37 @@
+import { Badge, cn } from '@uipath/apollo-wind';
+import * as React from 'react';
+import { GUARDRAIL_CHIP_GEOMETRY } from './guardrail-chip';
+
+export interface GuardrailStatusChipProps extends React.ComponentPropsWithoutRef<'div'> {
+ /** Maps onto the wind `Badge` variants: a neutral fact, a warning, or a blocking error. */
+ tone?: 'neutral' | 'warning' | 'error';
+}
+
+const TONE_VARIANT = {
+ neutral: 'secondary',
+ warning: 'warning',
+ error: 'error',
+} as const;
+
+/**
+ * Read-only status label for a guardrail row (definition status, governance administration).
+ *
+ * Deliberately **not** `GuardrailChip`: that wraps a Radix `Toggle`, so it is a focusable
+ * button carrying pressed state. These are labels, and rendering them as toggles would put
+ * fake buttons in the tab order and misreport them to screen readers. It reuses the chip
+ * family's pill geometry instead, so the two still read as one system.
+ */
+const GuardrailStatusChip = React.forwardRef(
+ ({ tone = 'neutral', className, ...props }, ref) => (
+
+ )
+);
+GuardrailStatusChip.displayName = 'GuardrailStatusChip';
+
+export { GuardrailStatusChip };
diff --git a/packages/apollo-react/src/canvas/components/Guardrails/components/map-enum-field.tsx b/packages/apollo-react/src/canvas/components/Guardrails/components/map-enum-field.tsx
new file mode 100644
index 0000000000..2271dff87a
--- /dev/null
+++ b/packages/apollo-react/src/canvas/components/Guardrails/components/map-enum-field.tsx
@@ -0,0 +1,85 @@
+import type { CustomFieldComponentProps } from '@uipath/apollo-wind';
+import { FormField, FormFieldError, Input, Label, useWatch } from '@uipath/apollo-wind';
+import { useCallback, useMemo } from 'react';
+import type { GuardrailValidatorFormLabels } from '../i18n';
+import type { GuardrailParameterDefinition } from '../types';
+import { ParameterLabel } from './parameter-label';
+
+/**
+ * One numeric input per key selected in the sibling `keySource` enum-list parameter (e.g. a
+ * threshold per selected PII entity), registered as the `guardrail-map-enum` custom
+ * component. Reads the sibling's live selection through the form context (`useWatch`),
+ * falling back to the source definition's default selection before the sibling registers.
+ * Renders nothing while the source has no selection. Edits never prune keys removed from the
+ * source; reconcile at save time with `syncMapEnumParameters`. `paramDef`, `sourceDef` and
+ * `labels` arrive via `componentProps` from `buildGuardrailFormSchema`.
+ */
+export function MapEnumField(props: CustomFieldComponentProps) {
+ const { value, onChange, error } = props;
+ const paramDef = props.paramDef as GuardrailParameterDefinition;
+ const sourceDef = props.sourceDef as GuardrailParameterDefinition | undefined;
+ const labels = props.labels as GuardrailValidatorFormLabels;
+ const sourceSelection = props.sourceSelection as string[] | undefined;
+
+ // Live sibling selection once the sibling emits its first change; until then `useWatch`
+ // returns undefined, so fall back to the mount-time selection from the schema builder,
+ // then to the source definition's default selection.
+ const watchedSelection = useWatch({ name: paramDef.keySource ?? '__guardrail-no-key-source' });
+ const keys =
+ (Array.isArray(watchedSelection) ? (watchedSelection as string[]) : undefined) ??
+ sourceSelection ??
+ (sourceDef?.defaultValue as string[] | undefined) ??
+ [];
+
+ const currentMap = useMemo(
+ () =>
+ ((value && typeof value === 'object' && !Array.isArray(value)
+ ? value
+ : paramDef.defaultValue) as Record | undefined) ?? {},
+ [value, paramDef.defaultValue]
+ );
+
+ // Per-entity defaults shipped by the backend (e.g. every PII entity at 0.5). Used as the
+ // displayed value for a freshly selected entity that isn't in the current map yet, so the
+ // user sees the real default rather than a misleading 0. Mirrors the save-time fallback in
+ // `syncMapEnumParameters`.
+ const defaults = useMemo(
+ () => (paramDef.defaultValue as Record | undefined) ?? {},
+ [paramDef.defaultValue]
+ );
+
+ const handleThresholdChange = useCallback(
+ (key: string, threshold: number) => {
+ onChange({ ...currentMap, [key]: threshold });
+ },
+ [currentMap, onChange]
+ );
+
+ if (keys.length === 0) return null;
+
+ return (
+
+
+
+ {keys.map((key) => (
+
+
+ {sourceDef?.optionLabels?.[key] ?? key}
+
+ handleThresholdChange(key, Number.parseFloat(e.target.value) || 0)}
+ min={paramDef.min}
+ max={paramDef.max}
+ step={paramDef.step}
+ className="flex-1"
+ />
+
+ ))}
+
+ {error}
+
+ );
+}
diff --git a/packages/apollo-react/src/canvas/components/Guardrails/components/mixed-scopes-banner.test.tsx b/packages/apollo-react/src/canvas/components/Guardrails/components/mixed-scopes-banner.test.tsx
new file mode 100644
index 0000000000..7b1299a15a
--- /dev/null
+++ b/packages/apollo-react/src/canvas/components/Guardrails/components/mixed-scopes-banner.test.tsx
@@ -0,0 +1,72 @@
+import { render, screen } from '@testing-library/react';
+import { axe } from 'jest-axe';
+import { describe, expect, it } from 'vitest';
+import { GUARDRAIL_BUILDER_EN_LABELS } from '../i18n';
+import { MixedScopesBanner } from './mixed-scopes-banner';
+
+const labels = GUARDRAIL_BUILDER_EN_LABELS;
+
+describe('MixedScopesBanner', () => {
+ it('renders nothing when otherAppliedScopes is null', () => {
+ const { container } = render( );
+ expect(container).toBeEmptyDOMElement();
+ });
+
+ it('renders the pre-localized scope labels', () => {
+ render(
+
+ );
+
+ expect(screen.getByText('Agent')).toBeInTheDocument();
+ expect(screen.getByText('LLM calls')).toBeInTheDocument();
+ });
+
+ it('renders the tool names', () => {
+ render(
+
+ );
+
+ expect(screen.getByText('Tool B')).toBeInTheDocument();
+ expect(screen.getByText('Tool C')).toBeInTheDocument();
+ });
+
+ it('renders scopes and tools together as list items', () => {
+ render(
+
+ );
+
+ const items = screen.getAllByRole('listitem');
+ expect(items.map((li) => li.textContent)).toEqual(['Agent', 'Tool B']);
+ });
+
+ it('renders the header and the save-as-new hint', () => {
+ render(
+
+ );
+
+ expect(screen.getByText('This guardrail is also applied to:')).toBeInTheDocument();
+ expect(
+ screen.getByText('Use "Save as new" to create a separate copy for this tool only.')
+ ).toBeInTheDocument();
+ });
+
+ it('has no accessibility violations', async () => {
+ const { container } = render(
+
+ );
+ const results = await axe(container);
+ expect(results).toHaveNoViolations();
+ });
+});
diff --git a/packages/apollo-react/src/canvas/components/Guardrails/components/mixed-scopes-banner.tsx b/packages/apollo-react/src/canvas/components/Guardrails/components/mixed-scopes-banner.tsx
new file mode 100644
index 0000000000..cbdf7855cc
--- /dev/null
+++ b/packages/apollo-react/src/canvas/components/Guardrails/components/mixed-scopes-banner.tsx
@@ -0,0 +1,35 @@
+import { Alert, AlertDescription } from '@uipath/apollo-wind';
+import { AlertCircle } from 'lucide-react';
+import type { GuardrailBuilderLabels } from '../i18n';
+
+export interface MixedScopesBannerProps {
+ /** Pre-localized scope labels + tool names the guardrail also applies to; null hides the banner. */
+ otherAppliedScopes: { scopes: string[]; tools: string[] } | null;
+ labels: Pick;
+}
+
+/**
+ * Info banner shown when a guardrail has mixed scopes (e.g. Agent + Tool): lists the other
+ * scopes/tools it applies to and suggests "Save as new".
+ */
+export function MixedScopesBanner({ otherAppliedScopes, labels }: MixedScopesBannerProps) {
+ if (!otherAppliedScopes) return null;
+
+ return (
+
+
+
+ {labels.mixedScopesAlsoApplied}
+
+ {otherAppliedScopes.scopes.map((s) => (
+ {s}
+ ))}
+ {otherAppliedScopes.tools.map((tool) => (
+ {tool}
+ ))}
+
+ {labels.mixedScopesSaveAsNewHint}
+
+
+ );
+}
diff --git a/packages/apollo-react/src/canvas/components/Guardrails/components/parameter-label.tsx b/packages/apollo-react/src/canvas/components/Guardrails/components/parameter-label.tsx
new file mode 100644
index 0000000000..8d24c75257
--- /dev/null
+++ b/packages/apollo-react/src/canvas/components/Guardrails/components/parameter-label.tsx
@@ -0,0 +1,36 @@
+import { InfoTooltip, Label, RequiredIndicator } from '@uipath/apollo-wind';
+import type { GuardrailValidatorFormLabels } from '../i18n';
+import type { GuardrailParameterDefinition } from '../types';
+
+export interface ParameterLabelProps {
+ paramDef: GuardrailParameterDefinition;
+ labels: GuardrailValidatorFormLabels;
+ htmlFor?: string;
+ asTextHeader?: boolean;
+}
+
+/** Parameter display label with required marker and optional info tooltip. */
+export function ParameterLabel({
+ paramDef,
+ labels,
+ htmlFor,
+ asTextHeader = false,
+}: ParameterLabelProps) {
+ const content = (
+ <>
+ {paramDef.label}
+ {paramDef.required && }
+ {paramDef.tooltip && (
+
+ )}
+ >
+ );
+ if (asTextHeader) {
+ return (
+
+ {content}
+
+ );
+ }
+ return {content} ;
+}
diff --git a/packages/apollo-react/src/canvas/components/Guardrails/definitions-copy.test.ts b/packages/apollo-react/src/canvas/components/Guardrails/definitions-copy.test.ts
new file mode 100644
index 0000000000..8e8654eb7a
--- /dev/null
+++ b/packages/apollo-react/src/canvas/components/Guardrails/definitions-copy.test.ts
@@ -0,0 +1,129 @@
+import { readFileSync } from 'node:fs';
+import { dirname, join } from 'node:path';
+import { fileURLToPath } from 'node:url';
+import { renderHook } from '@testing-library/react';
+import { describe, expect, it } from 'vitest';
+import { PII_ENTITY_OPTIONS } from './__fixtures__/definitions-wire.fixtures';
+import {
+ CURATED_GUARDRAIL_VALIDATORS,
+ GUARDRAIL_COPY_EN,
+ GUARDRAIL_COPY_EN_MESSAGES,
+ useGuardrailDefinitionCopy,
+} from './definitions-copy';
+
+const ID_PATTERN =
+ /^guardrails\.definitions\.[a-z0-9_]+\.(display-name|description|usage-note|param\.[A-Za-z0-9]+\.(label|tooltip)|option\.[A-Za-z0-9]+\.[A-Za-z0-9]+)$/;
+
+describe('GUARDRAIL_COPY_EN', () => {
+ it('covers every curated validator', () => {
+ expect(Object.keys(GUARDRAIL_COPY_EN).sort()).toEqual([...CURATED_GUARDRAIL_VALIDATORS].sort());
+ });
+
+ it('labels every PII entity the backend can return', () => {
+ const labels = GUARDRAIL_COPY_EN.pii_detection?.optionLabels?.entities ?? {};
+
+ expect(Object.keys(labels).sort()).toEqual([...PII_ENTITY_OPTIONS].sort());
+ });
+
+ it('resolves to English without a lingui provider', () => {
+ expect(GUARDRAIL_COPY_EN.llm_as_judge?.displayName).toBe('LLM as Judge');
+ expect(GUARDRAIL_COPY_EN.harmful_content?.paramLabels.harmfulContentEntities).toBe(
+ 'Content categories'
+ );
+ });
+});
+
+describe('message ids', () => {
+ it('all follow the documented convention', () => {
+ const offenders = Object.keys(GUARDRAIL_COPY_EN_MESSAGES).filter((id) => !ID_PATTERN.test(id));
+
+ expect(offenders).toEqual([]);
+ });
+
+ it('use the raw wire value for option segments, never a transcribed slug', () => {
+ // Transcribing is how the two products ended up with `finNationalId` and `fiNationalId`
+ // for the same entity; the raw value cannot drift because it is the value we persist.
+ for (const option of PII_ENTITY_OPTIONS) {
+ expect(GUARDRAIL_COPY_EN_MESSAGES).toHaveProperty(
+ `guardrails.definitions.pii_detection.option.entities.${option}`
+ );
+ }
+ });
+
+ it('are unique across the table', () => {
+ const table = GUARDRAIL_COPY_EN;
+ let stringCount = 0;
+ for (const copy of Object.values(table)) {
+ stringCount += 2 + (copy.usageNote === undefined ? 0 : 1);
+ stringCount += Object.keys(copy.paramLabels).length;
+ stringCount += Object.keys(copy.paramTooltips ?? {}).length;
+ for (const options of Object.values(copy.optionLabels ?? {})) {
+ stringCount += Object.keys(options).length;
+ }
+ }
+
+ // A duplicated id would silently collapse two strings into one entry.
+ expect(Object.keys(GUARDRAIL_COPY_EN_MESSAGES)).toHaveLength(stringCount);
+ });
+});
+
+describe('GUARDRAIL_COPY_EN_MESSAGES', () => {
+ it('holds the same English as the table it was built from', () => {
+ expect(GUARDRAIL_COPY_EN_MESSAGES['guardrails.definitions.pii_detection.display-name']).toBe(
+ GUARDRAIL_COPY_EN.pii_detection?.displayName
+ );
+ expect(
+ GUARDRAIL_COPY_EN_MESSAGES['guardrails.definitions.llm_as_judge.param.threshold.tooltip']
+ ).toBe(GUARDRAIL_COPY_EN.llm_as_judge?.paramTooltips?.threshold);
+ });
+
+ it('is frozen, so a host cannot mutate the CI baseline it diffs against', () => {
+ expect(Object.isFrozen(GUARDRAIL_COPY_EN_MESSAGES)).toBe(true);
+ });
+});
+
+describe('the shared canvas catalog', () => {
+ // `src/canvas` uses no lingui macros, so `lingui extract` does not feed this catalog: the
+ // entries are hand-authored. This test is what `extract` would otherwise be doing, and it
+ // is the only thing standing between a new message and an untranslatable string.
+ const catalog: Record = JSON.parse(
+ readFileSync(join(dirname(fileURLToPath(import.meta.url)), '../../locales/en.json'), 'utf8')
+ );
+
+ it('carries every definition message with the same English', () => {
+ const missing: string[] = [];
+ const drifted: string[] = [];
+ for (const [id, message] of Object.entries(GUARDRAIL_COPY_EN_MESSAGES)) {
+ if (!(id in catalog)) missing.push(id);
+ else if (catalog[id] !== message)
+ drifted.push(`${id}\n src: ${message}\n en: ${catalog[id]}`);
+ }
+
+ expect({ missing, drifted }).toEqual({ missing: [], drifted: [] });
+ });
+
+ it('carries no definition message the source no longer declares', () => {
+ const orphans = Object.keys(catalog)
+ .filter((id) => id.startsWith('guardrails.definitions.'))
+ .filter((id) => !(id in GUARDRAIL_COPY_EN_MESSAGES));
+
+ expect(orphans).toEqual([]);
+ });
+});
+
+describe('useGuardrailDefinitionCopy', () => {
+ it('returns the English table when no provider is mounted', () => {
+ const { result } = renderHook(() => useGuardrailDefinitionCopy());
+
+ expect(result.current).toEqual(GUARDRAIL_COPY_EN);
+ });
+
+ it('keeps a stable reference across renders, so enrichment does not rerun', () => {
+ const { result, rerender } = renderHook(() => useGuardrailDefinitionCopy());
+ const first = result.current;
+
+ rerender();
+
+ expect(result.current).toBe(first);
+ });
+});
diff --git a/packages/apollo-react/src/canvas/components/Guardrails/definitions-copy.ts b/packages/apollo-react/src/canvas/components/Guardrails/definitions-copy.ts
new file mode 100644
index 0000000000..2fa37eba03
--- /dev/null
+++ b/packages/apollo-react/src/canvas/components/Guardrails/definitions-copy.ts
@@ -0,0 +1,415 @@
+import { useMemo } from 'react';
+import { useSafeLingui } from '../../../i18n';
+
+/**
+ * The canonical display copy for the built-in guardrail validators.
+ *
+ * Until now this table lived twice, once in each product, and the two drifted. Here it is a
+ * set of lingui messages in the shared canvas catalog, so both hosts get the same wording and
+ * the same translations, and the strings sit in the real localization pipeline instead of a
+ * host-side constant.
+ *
+ * Message ids follow the family's dotted convention:
+ *
+ * ```
+ * guardrails.definitions..display-name
+ * guardrails.definitions..description
+ * guardrails.definitions..usage-note
+ * guardrails.definitions..param..label
+ * guardrails.definitions..param..tooltip
+ * guardrails.definitions..option..
+ * ```
+ *
+ * Validator, parameter and option segments are the **raw wire values**
+ * (`pii_detection`, `entityThresholds`, `USSocialSecurityNumber`), never a transcribed slug.
+ * Transcribing is how the two products ended up keying the same Finland entity as
+ * `finNationalId` and `fiNationalId`.
+ *
+ * One builder holds every message, so the English source, the flat record hosts diff in CI
+ * and the runtime lingui path cannot drift, and `lingui extract` still sees static calls.
+ * Where the two products' English differed, the choice and its reason are recorded in
+ * `definitions-parity.test.ts`.
+ */
+
+/** Curated copy for one validator. Absent entries fall back to the wire, then to the id. */
+export interface GuardrailValidatorCopy {
+ displayName: string;
+ description: string;
+ /** Informational note rendered above the form, e.g. a cost caveat. */
+ usageNote?: string;
+ /** Keyed by raw parameter id. */
+ paramLabels: Record;
+ /** Keyed by raw parameter id. */
+ paramTooltips?: Record;
+ /** Keyed by raw parameter id, then by raw wire option value. */
+ optionLabels?: Record>;
+}
+
+/** Curated copy keyed by raw validator id. */
+export type GuardrailCopyTable = Record;
+
+/** The subset of `useSafeLingui`'s translator this module needs. */
+type CopyTranslate = (descriptor: { id: string; message: string }) => string;
+
+function buildGuardrailCopy(_: CopyTranslate): GuardrailCopyTable {
+ return {
+ pii_detection: {
+ displayName: _({
+ id: 'guardrails.definitions.pii_detection.display-name',
+ message: 'PII detection',
+ }),
+ description: _({
+ id: 'guardrails.definitions.pii_detection.description',
+ message: 'Detect personally identifiable information using Azure Cognitive Services.',
+ }),
+ paramLabels: {
+ entities: _({
+ id: 'guardrails.definitions.pii_detection.param.entities.label',
+ message: 'Entities to detect',
+ }),
+ entityThresholds: _({
+ id: 'guardrails.definitions.pii_detection.param.entityThresholds.label',
+ message: 'Detection thresholds',
+ }),
+ },
+ paramTooltips: {
+ entityThresholds: _({
+ id: 'guardrails.definitions.pii_detection.param.entityThresholds.tooltip',
+ message:
+ '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: {
+ Person: _({
+ id: 'guardrails.definitions.pii_detection.option.entities.Person',
+ message: 'Person',
+ }),
+ Address: _({
+ id: 'guardrails.definitions.pii_detection.option.entities.Address',
+ message: 'Address',
+ }),
+ Date: _({
+ id: 'guardrails.definitions.pii_detection.option.entities.Date',
+ message: 'Date',
+ }),
+ PhoneNumber: _({
+ id: 'guardrails.definitions.pii_detection.option.entities.PhoneNumber',
+ message: 'Phone Number',
+ }),
+ EugpsCoordinates: _({
+ id: 'guardrails.definitions.pii_detection.option.entities.EugpsCoordinates',
+ message: 'EU GPS Coordinates',
+ }),
+ Email: _({
+ id: 'guardrails.definitions.pii_detection.option.entities.Email',
+ message: 'Email',
+ }),
+ CreditCardNumber: _({
+ id: 'guardrails.definitions.pii_detection.option.entities.CreditCardNumber',
+ message: 'Credit Card Number',
+ }),
+ InternationalBankingAccountNumber: _({
+ id: 'guardrails.definitions.pii_detection.option.entities.InternationalBankingAccountNumber',
+ message: 'International Banking Account Number (IBAN)',
+ }),
+ SwiftCode: _({
+ id: 'guardrails.definitions.pii_detection.option.entities.SwiftCode',
+ message: 'SWIFT Code',
+ }),
+ ABARoutingNumber: _({
+ id: 'guardrails.definitions.pii_detection.option.entities.ABARoutingNumber',
+ message: 'ABA Routing Number',
+ }),
+ USDriversLicenseNumber: _({
+ id: 'guardrails.definitions.pii_detection.option.entities.USDriversLicenseNumber',
+ message: "US Driver's License Number",
+ }),
+ UKDriversLicenseNumber: _({
+ id: 'guardrails.definitions.pii_detection.option.entities.UKDriversLicenseNumber',
+ message: "UK Driver's License Number",
+ }),
+ USIndividualTaxpayerIdentification: _({
+ id: 'guardrails.definitions.pii_detection.option.entities.USIndividualTaxpayerIdentification',
+ message: 'US Individual Taxpayer Identification Number (ITIN)',
+ }),
+ UKUniqueTaxpayerNumber: _({
+ id: 'guardrails.definitions.pii_detection.option.entities.UKUniqueTaxpayerNumber',
+ message: 'UK Unique Taxpayer Number (UTR)',
+ }),
+ USBankAccountNumber: _({
+ id: 'guardrails.definitions.pii_detection.option.entities.USBankAccountNumber',
+ message: 'US Bank Account Number',
+ }),
+ USSocialSecurityNumber: _({
+ id: 'guardrails.definitions.pii_detection.option.entities.USSocialSecurityNumber',
+ message: 'US Social Security Number (SSN)',
+ }),
+ UsukPassportNumber: _({
+ id: 'guardrails.definitions.pii_detection.option.entities.UsukPassportNumber',
+ message: 'US/UK Passport Number',
+ }),
+ NOIdentityNumber: _({
+ id: 'guardrails.definitions.pii_detection.option.entities.NOIdentityNumber',
+ message: 'Norway Identity Number',
+ }),
+ FINationalID: _({
+ id: 'guardrails.definitions.pii_detection.option.entities.FINationalID',
+ message: 'Finland National ID',
+ }),
+ FIPassportNumber: _({
+ id: 'guardrails.definitions.pii_detection.option.entities.FIPassportNumber',
+ message: 'Finland Passport Number',
+ }),
+ SENationalID: _({
+ id: 'guardrails.definitions.pii_detection.option.entities.SENationalID',
+ message: 'Sweden National ID',
+ }),
+ DKPersonalIdentificationNumber: _({
+ id: 'guardrails.definitions.pii_detection.option.entities.DKPersonalIdentificationNumber',
+ message: 'Danish Personal Identification Number',
+ }),
+ NLCitizensServiceNumber: _({
+ id: 'guardrails.definitions.pii_detection.option.entities.NLCitizensServiceNumber',
+ message: 'Netherlands Citizens Service Number',
+ }),
+ URL: _({
+ id: 'guardrails.definitions.pii_detection.option.entities.URL',
+ message: 'URL',
+ }),
+ IPAddress: _({
+ id: 'guardrails.definitions.pii_detection.option.entities.IPAddress',
+ message: 'IP Address',
+ }),
+ },
+ },
+ },
+
+ prompt_injection: {
+ displayName: _({
+ id: 'guardrails.definitions.prompt_injection.display-name',
+ message: 'Prompt injection',
+ }),
+ // The one description that keeps its "This validator is provided by..." form: the
+ // third-party vendor attribution is load-bearing and only Agents carries it.
+ description: _({
+ id: 'guardrails.definitions.prompt_injection.description',
+ message:
+ '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: _({
+ id: 'guardrails.definitions.prompt_injection.param.threshold.label',
+ message: 'Detection threshold',
+ }),
+ },
+ paramTooltips: {
+ threshold: _({
+ id: 'guardrails.definitions.prompt_injection.param.threshold.tooltip',
+ message:
+ '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: _({
+ id: 'guardrails.definitions.harmful_content.display-name',
+ message: 'Harmful content',
+ }),
+ description: _({
+ id: 'guardrails.definitions.harmful_content.description',
+ message: 'Detect harmful content (e.g. hate, violence) using Azure AI Content Safety.',
+ }),
+ paramLabels: {
+ harmfulContentEntities: _({
+ id: 'guardrails.definitions.harmful_content.param.harmfulContentEntities.label',
+ message: 'Content categories',
+ }),
+ harmfulContentEntityThresholds: _({
+ id: 'guardrails.definitions.harmful_content.param.harmfulContentEntityThresholds.label',
+ message: 'Severity thresholds',
+ }),
+ },
+ paramTooltips: {
+ harmfulContentEntityThresholds: _({
+ id: 'guardrails.definitions.harmful_content.param.harmfulContentEntityThresholds.tooltip',
+ message:
+ '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: _({
+ id: 'guardrails.definitions.harmful_content.option.harmfulContentEntities.Hate',
+ message: 'Hate',
+ }),
+ SelfHarm: _({
+ id: 'guardrails.definitions.harmful_content.option.harmfulContentEntities.SelfHarm',
+ message: 'Self-harm',
+ }),
+ Sexual: _({
+ id: 'guardrails.definitions.harmful_content.option.harmfulContentEntities.Sexual',
+ message: 'Sexual',
+ }),
+ Violence: _({
+ id: 'guardrails.definitions.harmful_content.option.harmfulContentEntities.Violence',
+ message: 'Violence',
+ }),
+ },
+ },
+ },
+
+ user_prompt_attacks: {
+ displayName: _({
+ id: 'guardrails.definitions.user_prompt_attacks.display-name',
+ message: 'User prompt attacks',
+ }),
+ description: _({
+ id: 'guardrails.definitions.user_prompt_attacks.description',
+ message:
+ 'Detect user prompt attacks that attempt to bypass system instructions using Azure AI Content Safety.',
+ }),
+ paramLabels: {},
+ },
+
+ intellectual_property: {
+ displayName: _({
+ id: 'guardrails.definitions.intellectual_property.display-name',
+ message: 'Intellectual property',
+ }),
+ description: _({
+ id: 'guardrails.definitions.intellectual_property.description',
+ message:
+ 'Detect potential intellectual property violations in text and code using Azure AI Content Safety.',
+ }),
+ paramLabels: {
+ ipEntities: _({
+ id: 'guardrails.definitions.intellectual_property.param.ipEntities.label',
+ message: 'Content types',
+ }),
+ },
+ optionLabels: {
+ ipEntities: {
+ Text: _({
+ id: 'guardrails.definitions.intellectual_property.option.ipEntities.Text',
+ message: 'Text',
+ }),
+ Code: _({
+ id: 'guardrails.definitions.intellectual_property.option.ipEntities.Code',
+ message: 'Code',
+ }),
+ },
+ },
+ },
+
+ llm_as_judge: {
+ displayName: _({
+ id: 'guardrails.definitions.llm_as_judge.display-name',
+ message: 'LLM as Judge',
+ }),
+ description: _({
+ id: 'guardrails.definitions.llm_as_judge.description',
+ message: 'Detect violations of a rule you define, using an LLM as the judge.',
+ }),
+ usageNote: _({
+ id: 'guardrails.definitions.llm_as_judge.usage-note',
+ message:
+ "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: _({
+ id: 'guardrails.definitions.llm_as_judge.param.guardrailText.label',
+ message: 'Rule prompt',
+ }),
+ model: _({
+ id: 'guardrails.definitions.llm_as_judge.param.model.label',
+ message: 'Judge model',
+ }),
+ positiveExamples: _({
+ id: 'guardrails.definitions.llm_as_judge.param.positiveExamples.label',
+ message: 'Positive examples',
+ }),
+ negativeExamples: _({
+ id: 'guardrails.definitions.llm_as_judge.param.negativeExamples.label',
+ message: 'Negative examples',
+ }),
+ threshold: _({
+ id: 'guardrails.definitions.llm_as_judge.param.threshold.label',
+ message: 'Strictness',
+ }),
+ },
+ paramTooltips: {
+ guardrailText: _({
+ id: 'guardrails.definitions.llm_as_judge.param.guardrailText.tooltip',
+ message:
+ 'Describe the rule the judge will enforce. Be specific about what should pass and what should fail.',
+ }),
+ model: _({
+ id: 'guardrails.definitions.llm_as_judge.param.model.tooltip',
+ message: 'The model used to evaluate the policy against each payload.',
+ }),
+ positiveExamples: _({
+ id: 'guardrails.definitions.llm_as_judge.param.positiveExamples.tooltip',
+ message:
+ 'Optional payloads that should pass the policy. Used by the judge as calibration anchors.',
+ }),
+ negativeExamples: _({
+ id: 'guardrails.definitions.llm_as_judge.param.negativeExamples.tooltip',
+ message:
+ 'Optional payloads that should fail the policy. Used by the judge as calibration anchors.',
+ }),
+ threshold: _({
+ id: 'guardrails.definitions.llm_as_judge.param.threshold.tooltip',
+ message:
+ '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.',
+ }),
+ },
+ },
+ };
+}
+
+/** The raw validator ids this package ships curated copy for. */
+export const CURATED_GUARDRAIL_VALIDATORS: readonly string[] = Object.freeze([
+ 'pii_detection',
+ 'prompt_injection',
+ 'harmful_content',
+ 'user_prompt_attacks',
+ 'intellectual_property',
+ 'llm_as_judge',
+]);
+
+/**
+ * The English copy, resolved without a lingui provider. This is the default
+ * `enrichGuardrailDefinitions` uses, so the pure layer stays usable outside React.
+ */
+export const GUARDRAIL_COPY_EN: GuardrailCopyTable = buildGuardrailCopy(
+ (descriptor) => descriptor.message
+);
+
+function collectEnglishMessages(): Record {
+ const messages: Record = {};
+ buildGuardrailCopy((descriptor) => {
+ messages[descriptor.id] = descriptor.message;
+ return descriptor.message;
+ });
+ return messages;
+}
+
+/**
+ * The same copy flattened to message id to English string. Exported so hosts can diff their
+ * remaining local tables against it in CI while they migrate off them.
+ */
+export const GUARDRAIL_COPY_EN_MESSAGES: Readonly> = Object.freeze(
+ collectEnglishMessages()
+);
+
+/**
+ * The curated copy in the active locale. Recomputes when the lingui context changes, which is
+ * what makes a locale switch re-enrich in `useGuardrailDefinitions`. Without a provider every
+ * string resolves to its English default.
+ */
+export function useGuardrailDefinitionCopy(): GuardrailCopyTable {
+ const { _ } = useSafeLingui();
+ return useMemo(() => buildGuardrailCopy(_), [_]);
+}
diff --git a/packages/apollo-react/src/canvas/components/Guardrails/definitions-enrich.test.ts b/packages/apollo-react/src/canvas/components/Guardrails/definitions-enrich.test.ts
new file mode 100644
index 0000000000..dcf1abbf62
--- /dev/null
+++ b/packages/apollo-react/src/canvas/components/Guardrails/definitions-enrich.test.ts
@@ -0,0 +1,318 @@
+import { describe, expect, it } from 'vitest';
+import {
+ BYO_WIRE,
+ HARMFUL_CONTENT_WIRE,
+ LLM_AS_JUDGE_WIRE,
+ PII_DETECTION_WIRE,
+ PROMPT_INJECTION_WIRE,
+ UNCURATED_WIRE,
+} from './__fixtures__/definitions-wire.fixtures';
+import type { GuardrailDefinition } from './builder-types';
+import { GUARDRAIL_COPY_EN, type GuardrailCopyTable } from './definitions-copy';
+import {
+ type EnrichedGuardrailDefinition,
+ enrichGuardrailDefinitions,
+ humanizeGuardrailParameterId,
+ isByoGuardrailDefinition,
+ withGuardrailFolderMetadata,
+} from './definitions-enrich';
+import type { GuardrailDefinitionWire } from './definitions-wire';
+
+function enrichOne(wire: GuardrailDefinitionWire, copy?: GuardrailCopyTable) {
+ const [definition] = enrichGuardrailDefinitions([wire], copy ? { copy } : undefined);
+ if (!definition) throw new Error('expected one enriched definition');
+ return definition;
+}
+
+function param(definition: EnrichedGuardrailDefinition, id: string) {
+ const found = definition.parameters.find((p) => p.id === id);
+ if (!found) throw new Error(`no parameter ${id}`);
+ return found;
+}
+
+describe('enrichGuardrailDefinitions', () => {
+ it('feeds GuardrailBuilder without mapping', () => {
+ // The whole point of extending the family's display type: an enriched definition is a
+ // GuardrailDefinition, so no host adapter has to translate between the two.
+ const definition: GuardrailDefinition = enrichOne(PII_DETECTION_WIRE);
+
+ expect(definition.displayName).toBe('PII detection');
+ expect(definition.status).toBe('Available');
+ expect(definition.allowedScopes).toEqual(['Agent', 'Llm', 'Tool']);
+ });
+
+ it('preserves payload order', () => {
+ const result = enrichGuardrailDefinitions([PROMPT_INJECTION_WIRE, PII_DETECTION_WIRE]);
+
+ expect(result.map((d) => d.validator)).toEqual(['prompt_injection', 'pii_detection']);
+ });
+
+ describe('copy precedence', () => {
+ it('prefers curated over wire at definition level for a UiPath validator', () => {
+ const definition = enrichOne({
+ ...PII_DETECTION_WIRE,
+ displayName: 'Backend name',
+ description: 'Backend description',
+ });
+
+ expect(definition.displayName).toBe('PII detection');
+ expect(definition.description).toBe(
+ 'Detect personally identifiable information using Azure Cognitive Services.'
+ );
+ });
+
+ it('falls back to the wire, then the validator id, when nothing is curated', () => {
+ expect(enrichOne({ ...UNCURATED_WIRE, displayName: 'Topic drift' }).displayName).toBe(
+ 'Topic drift'
+ );
+ expect(enrichOne(UNCURATED_WIRE).displayName).toBe('topic_drift');
+ expect(enrichOne(UNCURATED_WIRE).description).toBe('');
+ });
+
+ it('prefers wire over curated at parameter level', () => {
+ const definition = enrichOne({
+ ...PII_DETECTION_WIRE,
+ parameters: [
+ {
+ id: 'entities',
+ type: 'enum-list',
+ required: true,
+ defaultValue: [],
+ options: ['Email'],
+ displayName: 'Fields to scan',
+ description: 'Chosen by the manifest.',
+ },
+ ],
+ });
+
+ expect(param(definition, 'entities').label).toBe('Fields to scan');
+ expect(param(definition, 'entities').tooltip).toBe('Chosen by the manifest.');
+ });
+
+ it('uses curated parameter copy when the wire says nothing', () => {
+ const definition = enrichOne(PII_DETECTION_WIRE);
+
+ expect(param(definition, 'entities').label).toBe('Entities to detect');
+ expect(param(definition, 'entityThresholds').label).toBe('Detection thresholds');
+ expect(param(definition, 'entityThresholds').tooltip).toMatch(/sensitivity level for PII/);
+ });
+
+ it('humanizes an uncurated parameter id rather than showing it raw', () => {
+ const definition = enrichOne(UNCURATED_WIRE);
+
+ expect(param(definition, 'maxDriftScore').label).toBe('Max drift score');
+ });
+
+ it('omits the tooltip entirely when there is none', () => {
+ expect(param(enrichOne(PII_DETECTION_WIRE), 'entities')).not.toHaveProperty('tooltip');
+ });
+
+ it('attaches the curated usage note', () => {
+ expect(enrichOne(LLM_AS_JUDGE_WIRE).usageNote).toMatch(/consume Agent units/);
+ expect(enrichOne(PII_DETECTION_WIRE)).not.toHaveProperty('usageNote');
+ });
+ });
+
+ describe('option labels', () => {
+ it('resolves curated labels for every wire option', () => {
+ const labels = param(enrichOne(PII_DETECTION_WIRE), 'entities').optionLabels;
+
+ expect(labels?.USSocialSecurityNumber).toBe('US Social Security Number (SSN)');
+ expect(labels?.SwiftCode).toBe('SWIFT Code');
+ });
+
+ it('merges wire labels over curated ones, per option', () => {
+ const definition = enrichOne({
+ ...HARMFUL_CONTENT_WIRE,
+ parameters: [
+ {
+ id: 'harmfulContentEntities',
+ type: 'enum-list',
+ required: true,
+ defaultValue: [],
+ options: ['Hate', 'SelfHarm'],
+ optionLabels: { Hate: 'Hateful content' },
+ },
+ ],
+ });
+
+ const labels = param(definition, 'harmfulContentEntities').optionLabels;
+ expect(labels).toEqual({
+ Hate: 'Hateful content',
+ SelfHarm: 'Self-harm',
+ Sexual: 'Sexual',
+ Violence: 'Violence',
+ });
+ });
+
+ it('leaves unlabelled options out, since the editors fall back to the raw value', () => {
+ const labels = param(enrichOne(UNCURATED_WIRE), 'allowedTopics');
+
+ expect(labels).not.toHaveProperty('optionLabels');
+ expect(labels.options).toEqual(['finance', 'legal']);
+ });
+ });
+
+ describe('bring-your-own definitions', () => {
+ it('takes zero curated copy even when the validator id collides with a UiPath one', () => {
+ // BYO_WIRE deliberately reuses `pii_detection` as its validator id.
+ const definition = enrichOne(BYO_WIRE);
+
+ expect(definition.displayName).toBe('Acme PII scan');
+ expect(definition.description).toBe('Runs the Acme detector over prompts and completions.');
+ expect(definition).not.toHaveProperty('usageNote');
+ });
+
+ it('carries the BYO and folder fields through', () => {
+ const definition = enrichOne({ ...BYO_WIRE, folderPath: 'Shared/AI', folderKey: 'fk' });
+
+ expect(definition).toMatchObject({
+ byoValidatorName: 'acme-pii',
+ byoConnectorName: 'Acme AI Guardrails',
+ byoConnectorKey: 'acme',
+ byoGuardrailConnectionId: 'conn-1',
+ byoConfigurationId: 'cfg-1',
+ folderPath: 'Shared/AI',
+ folderKey: 'fk',
+ });
+ });
+
+ it('omits absent optional fields instead of setting them undefined', () => {
+ const definition = enrichOne(PII_DETECTION_WIRE);
+
+ expect(definition).not.toHaveProperty('byoValidatorName');
+ expect(definition).not.toHaveProperty('folderPath');
+ });
+
+ it('uses manifest parameter copy and merges its partial option labels', () => {
+ const definition = enrichOne(BYO_WIRE);
+
+ expect(param(definition, 'sensitivity').label).toBe('Sensitivity');
+ expect(param(definition, 'sensitivity').optionLabels).toEqual({ low: 'Low', high: 'High' });
+ });
+ });
+
+ describe('parameter constraints', () => {
+ it('gives an unbounded map-enum the 0..1 step 0.1 the thresholds need', () => {
+ const thresholds = param(enrichOne(PII_DETECTION_WIRE), 'entityThresholds');
+
+ expect(thresholds).toMatchObject({ keySource: 'entities', min: 0, max: 1, step: 0.1 });
+ });
+
+ it('keeps map-enum bounds the backend does send', () => {
+ const thresholds = param(enrichOne(HARMFUL_CONTENT_WIRE), 'harmfulContentEntityThresholds');
+
+ expect(thresholds).toMatchObject({ min: 0, max: 6, step: 2 });
+ });
+
+ it('copies number, text and text-list constraints only when present', () => {
+ const judge = enrichOne(LLM_AS_JUDGE_WIRE);
+
+ expect(param(judge, 'threshold')).toMatchObject({ min: 0, max: 6, step: 2 });
+ expect(param(judge, 'guardrailText')).toMatchObject({ maxLength: 4000 });
+ expect(param(judge, 'positiveExamples')).toMatchObject({ maxItems: 5, maxLength: 1000 });
+ expect(param(enrichOne(UNCURATED_WIRE), 'maxDriftScore')).not.toHaveProperty('min');
+ });
+
+ it('passes defaultValue through untouched, nulls included', () => {
+ const judge = enrichOne(LLM_AS_JUDGE_WIRE);
+
+ // `seedGuardrailParameters` is what coerces these into the value union at form time.
+ expect(param(judge, 'guardrailText').defaultValue).toBeNull();
+ expect(param(judge, 'positiveExamples').defaultValue).toBeUndefined();
+ expect(param(judge, 'threshold').defaultValue).toBe(4);
+ });
+ });
+
+ describe('hiddenValidators', () => {
+ it('hides nothing by default', () => {
+ const result = enrichGuardrailDefinitions([PROMPT_INJECTION_WIRE, PII_DETECTION_WIRE]);
+
+ expect(result).toHaveLength(2);
+ });
+
+ it('drops the named UiPath validators', () => {
+ const result = enrichGuardrailDefinitions([PROMPT_INJECTION_WIRE, PII_DETECTION_WIRE], {
+ hiddenValidators: ['prompt_injection'],
+ });
+
+ expect(result.map((d) => d.validator)).toEqual(['pii_detection']);
+ });
+
+ it('never hides a BYO definition, whatever its validator id', () => {
+ const result = enrichGuardrailDefinitions([BYO_WIRE], {
+ hiddenValidators: ['pii_detection'],
+ });
+
+ expect(result).toHaveLength(1);
+ });
+ });
+
+ it('resolves copy from the supplied table, not the English one', () => {
+ const localized: GuardrailCopyTable = {
+ ...GUARDRAIL_COPY_EN,
+ pii_detection: {
+ ...(GUARDRAIL_COPY_EN.pii_detection ?? {
+ displayName: '',
+ description: '',
+ paramLabels: {},
+ }),
+ displayName: 'Erkennung personenbezogener Daten',
+ },
+ };
+
+ expect(enrichOne(PII_DETECTION_WIRE, localized).displayName).toBe(
+ 'Erkennung personenbezogener Daten'
+ );
+ });
+});
+
+describe('isByoGuardrailDefinition', () => {
+ it('is exactly "names a BYO validator"', () => {
+ expect(isByoGuardrailDefinition(BYO_WIRE)).toBe(true);
+ expect(isByoGuardrailDefinition(PII_DETECTION_WIRE)).toBe(false);
+ });
+});
+
+describe('humanizeGuardrailParameterId', () => {
+ it.each([
+ ['entityThresholds', 'Entity thresholds'],
+ ['threshold', 'Threshold'],
+ ['maxDriftScore', 'Max drift score'],
+ ['harmfulContentEntities', 'Harmful content entities'],
+ ])('%s reads as %s', (id, expected) => {
+ expect(humanizeGuardrailParameterId(id)).toBe(expected);
+ });
+});
+
+describe('withGuardrailFolderMetadata', () => {
+ const [byo] = enrichGuardrailDefinitions([BYO_WIRE]);
+ const [builtIn] = enrichGuardrailDefinitions([PII_DETECTION_WIRE]);
+ if (!byo || !builtIn) throw new Error('fixture setup failed');
+
+ it('stamps the resolved placement onto BYO definitions', () => {
+ const [stamped] = withGuardrailFolderMetadata([byo], (id) =>
+ id === 'conn-1' ? { folderPath: 'Shared/AI', folderKey: 'fk-1' } : undefined
+ );
+
+ expect(stamped).toMatchObject({ folderPath: 'Shared/AI', folderKey: 'fk-1' });
+ });
+
+ it('leaves definitions without a connection id alone', () => {
+ const [passed] = withGuardrailFolderMetadata([builtIn], () => ({ folderPath: 'Shared/X' }));
+
+ expect(passed).toBe(builtIn);
+ });
+
+ it('keeps a definition the host could not resolve rather than dropping it', () => {
+ const result = withGuardrailFolderMetadata([byo], () => undefined);
+
+ expect(result).toEqual([byo]);
+ });
+
+ it('does not mutate its input', () => {
+ withGuardrailFolderMetadata([byo], () => ({ folderPath: 'Shared/AI' }));
+
+ expect(byo).not.toHaveProperty('folderPath');
+ });
+});
diff --git a/packages/apollo-react/src/canvas/components/Guardrails/definitions-enrich.ts b/packages/apollo-react/src/canvas/components/Guardrails/definitions-enrich.ts
new file mode 100644
index 0000000000..517bcdf304
--- /dev/null
+++ b/packages/apollo-react/src/canvas/components/Guardrails/definitions-enrich.ts
@@ -0,0 +1,220 @@
+import type { GuardrailDefinition } from './builder-types';
+import {
+ GUARDRAIL_COPY_EN,
+ type GuardrailCopyTable,
+ type GuardrailValidatorCopy,
+} from './definitions-copy';
+import type { GuardrailDefinitionWire, GuardrailParameterDefinitionWire } from './definitions-wire';
+import type { GuardrailParameterDefinition } from './types';
+
+/**
+ * Turn validated wire definitions into the display-ready shape the builder renders.
+ *
+ * Pure and React-free on purpose: hosts outside React (Flow's vsix bridge, node-side tests)
+ * call it directly, and `useGuardrailDefinitions` is only a thin composition over it.
+ */
+
+/**
+ * A `GuardrailDefinition` plus the wire fields the builder does not read but hosts do
+ * (palette grouping, BYO connection notices, folder resolution). Because it extends the
+ * family's display type it feeds `GuardrailBuilder` and `GuardrailValidatorForm` unchanged.
+ */
+export interface EnrichedGuardrailDefinition extends GuardrailDefinition {
+ /** Resolved description, `''` when neither the curated table nor the wire has one. */
+ description: string;
+ /** Narrowed from `GuardrailDefinition`'s `ReactNode`: enrichment only ever yields text. */
+ usageNote?: string;
+ byoConnectorName?: string;
+ byoConnectorKey?: string;
+ byoGuardrailConnectionId?: string;
+ byoConfigurationId?: string;
+ folderPath?: string;
+ folderKey?: string;
+}
+
+export interface EnrichGuardrailDefinitionsOptions {
+ /**
+ * Copy table to resolve curated strings from. Defaults to `GUARDRAIL_COPY_EN`; pass
+ * `useGuardrailDefinitionCopy()`'s table for the active locale.
+ */
+ copy?: GuardrailCopyTable;
+ /**
+ * Non-BYO validators to drop entirely, e.g. Flow hides `prompt_injection`. Defaults to
+ * none: which validators a product exposes is an entitlement decision, not a copy one.
+ * BYO definitions are never hidden, since their validator id is not the product's to judge.
+ */
+ hiddenValidators?: readonly string[];
+}
+
+/**
+ * A definition is bring-your-own exactly when it names a BYO validator. Both products
+ * already use this test; it decides copy resolution, palette grouping and the BYO notices.
+ */
+export function isByoGuardrailDefinition(
+ definition: T
+): definition is T & { byoValidatorName: string } {
+ return definition.byoValidatorName !== undefined;
+}
+
+/**
+ * Fallback label for a parameter no curated table names: `entityThresholds` reads as
+ * "Entity thresholds". Better than surfacing the raw id, which is what a host would
+ * otherwise show for a validator the UI has not learned yet.
+ */
+export function humanizeGuardrailParameterId(id: string): string {
+ const spaced = id.replace(/([A-Z])/g, ' $1').trim();
+ return spaced.charAt(0).toUpperCase() + spaced.slice(1).toLowerCase();
+}
+
+function toParameterDefinition(
+ param: GuardrailParameterDefinitionWire,
+ curated: GuardrailValidatorCopy | undefined
+): GuardrailParameterDefinition {
+ const definition: GuardrailParameterDefinition = {
+ id: param.id,
+ type: param.type,
+ // Wire wins at parameter level: a BYO manifest and a newly shipped backend parameter
+ // both describe themselves, and only they know their own copy.
+ label:
+ param.displayName ?? curated?.paramLabels[param.id] ?? humanizeGuardrailParameterId(param.id),
+ required: param.required,
+ defaultValue: param.defaultValue,
+ };
+
+ const tooltip = param.description ?? curated?.paramTooltips?.[param.id];
+ if (tooltip !== undefined) definition.tooltip = tooltip;
+
+ const curatedOptionLabels = curated?.optionLabels?.[param.id];
+ const wireOptionLabels =
+ param.type === 'enum' || param.type === 'enum-list' ? param.optionLabels : undefined;
+ if (curatedOptionLabels !== undefined || wireOptionLabels !== undefined) {
+ // Merged, wire last: a manifest may relabel a subset of options without restating the
+ // rest. Options nobody labelled are deliberately absent, since the editors already fall
+ // back to the raw wire value.
+ definition.optionLabels = { ...curatedOptionLabels, ...wireOptionLabels };
+ }
+
+ switch (param.type) {
+ case 'enum':
+ case 'enum-list':
+ definition.options = param.options;
+ break;
+ case 'map-enum':
+ definition.keySource = param.keySource;
+ // The threshold maps arrive without bounds from some backends, and an unbounded
+ // numeric editor for a 0..1 confidence score is a data-entry hazard.
+ definition.min = param.min ?? 0;
+ definition.max = param.max ?? 1;
+ definition.step = param.step ?? 0.1;
+ break;
+ case 'number':
+ if (param.min != null) definition.min = param.min;
+ if (param.max != null) definition.max = param.max;
+ if (param.step != null) definition.step = param.step;
+ break;
+ case 'text':
+ if (param.maxLength != null) definition.maxLength = param.maxLength;
+ break;
+ case 'text-list':
+ if (param.maxLength != null) definition.maxLength = param.maxLength;
+ if (param.maxItems != null) definition.maxItems = param.maxItems;
+ break;
+ default:
+ break;
+ }
+
+ return definition;
+}
+
+function enrichOne(
+ wire: GuardrailDefinitionWire,
+ copy: GuardrailCopyTable
+): EnrichedGuardrailDefinition {
+ const isByo = isByoGuardrailDefinition(wire);
+ // A BYO guardrail gets zero curated copy at every level: its validator id is the
+ // customer's, and a collision with a UiPath id must not borrow UiPath's wording.
+ const curated = isByo ? undefined : copy[wire.validator];
+
+ const enriched: EnrichedGuardrailDefinition = {
+ validator: wire.validator,
+ // Curated wins at definition level for non-BYO: the backend ships terse internal names
+ // and this table is what product and localization actually review.
+ displayName:
+ (isByo ? wire.displayName : (curated?.displayName ?? wire.displayName)) ?? wire.validator,
+ description: (isByo ? wire.description : (curated?.description ?? wire.description)) ?? '',
+ allowedScopes: wire.allowedScopes,
+ parameters: wire.parameters.map((param) => toParameterDefinition(param, curated)),
+ status: wire.status,
+ };
+
+ if (curated?.usageNote !== undefined) enriched.usageNote = curated.usageNote;
+ if (wire.byoValidatorName !== undefined) enriched.byoValidatorName = wire.byoValidatorName;
+ if (wire.byoConnectorName !== undefined) enriched.byoConnectorName = wire.byoConnectorName;
+ if (wire.byoConnectorKey !== undefined) enriched.byoConnectorKey = wire.byoConnectorKey;
+ if (wire.byoGuardrailConnectionId !== undefined) {
+ enriched.byoGuardrailConnectionId = wire.byoGuardrailConnectionId;
+ }
+ if (wire.byoConfigurationId !== undefined) enriched.byoConfigurationId = wire.byoConfigurationId;
+ if (wire.folderPath !== undefined) enriched.folderPath = wire.folderPath;
+ if (wire.folderKey !== undefined) enriched.folderKey = wire.folderKey;
+
+ return enriched;
+}
+
+/**
+ * Resolve display copy onto validated wire definitions, in payload order.
+ *
+ * Copy precedence, re-derived from what both products do today:
+ *
+ * | 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 |
+ */
+export function enrichGuardrailDefinitions(
+ definitions: readonly GuardrailDefinitionWire[],
+ options: EnrichGuardrailDefinitionsOptions = {}
+): EnrichedGuardrailDefinition[] {
+ const copy = options.copy ?? GUARDRAIL_COPY_EN;
+ const hidden = new Set(options.hiddenValidators ?? []);
+ const result: EnrichedGuardrailDefinition[] = [];
+ for (const wire of definitions) {
+ if (!isByoGuardrailDefinition(wire) && hidden.has(wire.validator)) continue;
+ result.push(enrichOne(wire, copy));
+ }
+ return result;
+}
+
+/** Folder placement for one BYO guardrail connection. */
+export interface GuardrailFolderMetadata {
+ folderPath?: string;
+ folderKey?: string;
+}
+
+/**
+ * Stamp folder placement onto BYO definitions from a host lookup.
+ *
+ * Resolution stays host-side because it needs each product's connections API, which this
+ * package deliberately does not reach for: Agents pages `fetchResources`, Flow calls
+ * `getConnectionById`. Definitions without a connection id, and connections the host cannot
+ * resolve, pass through untouched rather than being dropped.
+ */
+export function withGuardrailFolderMetadata(
+ definitions: readonly T[],
+ resolve: (connectionId: string) => GuardrailFolderMetadata | undefined
+): T[] {
+ return definitions.map((definition) => {
+ const connectionId = definition.byoGuardrailConnectionId;
+ if (connectionId === undefined) return definition;
+ const metadata = resolve(connectionId);
+ if (metadata === undefined) return definition;
+ const next: T = { ...definition };
+ if (metadata.folderPath !== undefined) next.folderPath = metadata.folderPath;
+ if (metadata.folderKey !== undefined) next.folderKey = metadata.folderKey;
+ return next;
+ });
+}
diff --git a/packages/apollo-react/src/canvas/components/Guardrails/definitions-parity.test.ts b/packages/apollo-react/src/canvas/components/Guardrails/definitions-parity.test.ts
new file mode 100644
index 0000000000..d2cd03d41d
--- /dev/null
+++ b/packages/apollo-react/src/canvas/components/Guardrails/definitions-parity.test.ts
@@ -0,0 +1,206 @@
+import { describe, expect, it } from 'vitest';
+import { AGENTS_COPY_EN, FLOW_COPY_EN } from './__fixtures__/host-copy-baselines';
+import {
+ CURATED_GUARDRAIL_VALIDATORS,
+ GUARDRAIL_COPY_EN,
+ type GuardrailCopyTable,
+} from './definitions-copy';
+
+/**
+ * Pins this package's English copy against what each product ships today.
+ *
+ * Two rules, both enforced mechanically:
+ *
+ * 1. Where the products already agree, this package says the same thing. Inventing a third
+ * wording is how a shared table stops being adoptable.
+ * 2. Where they disagree, the choice is declared below with a reason, and this package says
+ * exactly what the chosen product says, so its existing translations harvest cleanly.
+ *
+ * When a product changes its copy, update `__fixtures__/host-copy-baselines.ts` first: this
+ * suite then says whether the choice still holds.
+ */
+
+type Host = 'agents' | 'flow';
+
+interface CopyDivergence {
+ /** Dotted path into the flattened copy table. */
+ path: string;
+ chosen: Host;
+ reason: string;
+}
+
+const EXPECTED_DIVERGENCES: CopyDivergence[] = [
+ // Descriptions: Flow's are one sentence and say what the validator does. Agents' open with
+ // "This validator is designed to..." / "...is provided by X and is built to...", which
+ // costs a line of panel space to say nothing.
+ {
+ path: 'pii_detection.description',
+ chosen: 'flow',
+ reason: 'Concise, drops the "This validator is designed to" preamble',
+ },
+ {
+ path: 'harmful_content.description',
+ chosen: 'flow',
+ reason: 'Concise; keeps the Azure AI Content Safety attribution',
+ },
+ {
+ path: 'intellectual_property.description',
+ chosen: 'flow',
+ reason: 'Concise; keeps the Azure AI Content Safety attribution',
+ },
+ {
+ path: 'user_prompt_attacks.description',
+ chosen: 'flow',
+ reason: 'Concise; keeps the Azure AI Content Safety attribution',
+ },
+ {
+ path: 'prompt_injection.description',
+ chosen: 'agents',
+ reason:
+ 'The deliberate exception to the concision rule: Flow drops the Noma Security attribution, and naming the third-party vendor behind a validator is load-bearing',
+ },
+
+ // Parameter labels: Flow's name the thing being configured; Agents reuses one generic
+ // label across validators, so two different parameters read identically in one panel.
+ {
+ path: 'pii_detection.paramLabels.entityThresholds',
+ chosen: 'flow',
+ reason: 'Plural: there is one threshold per selected entity',
+ },
+ {
+ path: 'harmful_content.paramLabels.harmfulContentEntities',
+ chosen: 'flow',
+ reason: '"Content categories" beats "Entities to detect", which PII already uses',
+ },
+ {
+ path: 'harmful_content.paramLabels.harmfulContentEntityThresholds',
+ chosen: 'flow',
+ reason: 'It is a severity scale, and there is one per category',
+ },
+ {
+ path: 'intellectual_property.paramLabels.ipEntities',
+ chosen: 'flow',
+ reason: '"Content types" beats "Entities to detect", which PII already uses',
+ },
+ {
+ path: 'llm_as_judge.paramLabels.threshold',
+ chosen: 'flow',
+ reason: '"Strictness" says which direction the number moves; "Threshold" does not',
+ },
+ {
+ path: 'llm_as_judge.paramTooltips.threshold',
+ chosen: 'flow',
+ reason: 'Matches the "Strictness" label it explains',
+ },
+
+ // Option labels.
+ {
+ path: 'harmful_content.optionLabels.harmfulContentEntities.SelfHarm',
+ chosen: 'flow',
+ reason: 'Agents shows the raw enum value "SelfHarm"',
+ },
+ {
+ path: 'pii_detection.optionLabels.entities.FIPassportNumber',
+ chosen: 'agents',
+ reason: 'Flow omits an entity the backend returns, so Flow renders the raw value today',
+ },
+
+ // Strings only one product has. Adopting them makes both products better off.
+ {
+ path: 'pii_detection.paramTooltips.entityThresholds',
+ chosen: 'agents',
+ reason: 'Real guidance on the 0..1 scale; Flow ships no tooltip',
+ },
+ {
+ path: 'prompt_injection.paramTooltips.threshold',
+ chosen: 'agents',
+ reason: 'Real guidance on the 0..1 scale; Flow ships no tooltip',
+ },
+ {
+ path: 'harmful_content.paramTooltips.harmfulContentEntityThresholds',
+ chosen: 'agents',
+ reason: 'Explains the 0..6 step-2 severity scale; Flow ships no tooltip',
+ },
+ {
+ path: 'llm_as_judge.usageNote',
+ chosen: 'flow',
+ reason: 'Agent-unit cost caveat; Agents users need it just as much',
+ },
+];
+
+function flatten(table: GuardrailCopyTable): Map {
+ const flat = new Map();
+ for (const [validator, copy] of Object.entries(table)) {
+ flat.set(`${validator}.displayName`, copy.displayName);
+ flat.set(`${validator}.description`, copy.description);
+ if (copy.usageNote !== undefined) flat.set(`${validator}.usageNote`, copy.usageNote);
+ for (const [id, label] of Object.entries(copy.paramLabels)) {
+ flat.set(`${validator}.paramLabels.${id}`, label);
+ }
+ for (const [id, tooltip] of Object.entries(copy.paramTooltips ?? {})) {
+ flat.set(`${validator}.paramTooltips.${id}`, tooltip);
+ }
+ for (const [paramId, options] of Object.entries(copy.optionLabels ?? {})) {
+ for (const [option, label] of Object.entries(options)) {
+ flat.set(`${validator}.optionLabels.${paramId}.${option}`, label);
+ }
+ }
+ }
+ return flat;
+}
+
+const agents = flatten(AGENTS_COPY_EN);
+const flow = flatten(FLOW_COPY_EN);
+const ours = flatten(GUARDRAIL_COPY_EN);
+const allPaths = [...new Set([...agents.keys(), ...flow.keys(), ...ours.keys()])].sort();
+const divergenceByPath = new Map(EXPECTED_DIVERGENCES.map((d) => [d.path, d]));
+
+describe('canonical copy parity with both products', () => {
+ it('covers the same validators both products curate', () => {
+ expect([...CURATED_GUARDRAIL_VALIDATORS].sort()).toEqual(Object.keys(AGENTS_COPY_EN).sort());
+ expect([...CURATED_GUARDRAIL_VALIDATORS].sort()).toEqual(Object.keys(FLOW_COPY_EN).sort());
+ expect(Object.keys(GUARDRAIL_COPY_EN).sort()).toEqual([...CURATED_GUARDRAIL_VALIDATORS].sort());
+ });
+
+ it('matches both products wherever they already agree', () => {
+ const invented = allPaths
+ .filter((path) => agents.get(path) === flow.get(path))
+ .filter((path) => ours.get(path) !== agents.get(path))
+ .map((path) => `${path}\n ours: ${ours.get(path)}\n hosts: ${agents.get(path)}`);
+
+ expect(invented).toEqual([]);
+ });
+
+ it('declares every disagreement between the products', () => {
+ const undeclared = allPaths
+ .filter((path) => agents.get(path) !== flow.get(path))
+ .filter((path) => !divergenceByPath.has(path))
+ .map((path) => `${path}\n agents: ${agents.get(path)}\n flow: ${flow.get(path)}`);
+
+ expect(undeclared).toEqual([]);
+ });
+
+ it('has no stale divergence entries', () => {
+ const settled = EXPECTED_DIVERGENCES.filter(
+ ({ path }) => agents.get(path) === flow.get(path)
+ ).map(({ path }) => path);
+
+ expect(settled).toEqual([]);
+ });
+
+ it.each(EXPECTED_DIVERGENCES)('$path takes the $chosen wording: $reason', ({ path, chosen }) => {
+ const expected = chosen === 'agents' ? agents.get(path) : flow.get(path);
+
+ expect(expected).toBeDefined();
+ expect(ours.get(path)).toBe(expected);
+ });
+
+ it('takes every string from one product or the other', () => {
+ // A string in neither baseline would be copy nobody has translated and nobody reviewed.
+ const unsourced = allPaths.filter(
+ (path) => ours.has(path) && !agents.has(path) && !flow.has(path)
+ );
+
+ expect(unsourced).toEqual([]);
+ });
+});
diff --git a/packages/apollo-react/src/canvas/components/Guardrails/definitions-parse.test.ts b/packages/apollo-react/src/canvas/components/Guardrails/definitions-parse.test.ts
new file mode 100644
index 0000000000..e41b39ec1e
--- /dev/null
+++ b/packages/apollo-react/src/canvas/components/Guardrails/definitions-parse.test.ts
@@ -0,0 +1,244 @@
+import { readdirSync, readFileSync } from 'node:fs';
+import { dirname, join } from 'node:path';
+import { fileURLToPath } from 'node:url';
+import { describe, expect, it } from 'vitest';
+import {
+ ALL_BUILT_IN_WIRE,
+ LLM_AS_JUDGE_WIRE,
+ PII_DETECTION_WIRE,
+ PROMPT_INJECTION_WIRE,
+ RAW_PAYLOAD_WITH_NOISE,
+} from './__fixtures__/definitions-wire.fixtures';
+import { parseGuardrailDefinitions } from './definitions-parse';
+
+describe('parseGuardrailDefinitions', () => {
+ it('returns every definition of a well-formed payload, in order', () => {
+ const result = parseGuardrailDefinitions(ALL_BUILT_IN_WIRE);
+
+ expect(result.definitions.map((d) => d.validator)).toEqual([
+ 'pii_detection',
+ 'prompt_injection',
+ 'harmful_content',
+ 'user_prompt_attacks',
+ 'intellectual_property',
+ 'llm_as_judge',
+ ]);
+ expect(result.invalid).toEqual([]);
+ expect(result.inputError).toBeUndefined();
+ });
+
+ it('round-trips a definition unchanged', () => {
+ const result = parseGuardrailDefinitions([PII_DETECTION_WIRE]);
+
+ expect(result.definitions[0]).toEqual(PII_DETECTION_WIRE);
+ });
+
+ describe('never throws', () => {
+ // The payload crosses a trust boundary (a backend, or Flow's postMessage bridge), and a
+ // properties panel must not disappear because it arrived malformed.
+ it.each([
+ ['null', null],
+ ['undefined', undefined],
+ ['a string', 'not json'],
+ ['a number', 42],
+ ['an object', { definitions: [] }],
+ ])('reports %s as an input error rather than throwing', (_label, input) => {
+ const result = parseGuardrailDefinitions(input);
+
+ expect(result.definitions).toEqual([]);
+ expect(result.invalid).toEqual([]);
+ expect(result.inputError).toMatch(/Expected an array/);
+ });
+
+ it('survives hostile entries', () => {
+ const hostile = [
+ null,
+ undefined,
+ 'string',
+ [],
+ { __proto__: { validator: 'injected' } },
+ { validator: 'x', allowedScopes: 'not-an-array', status: 'Available' },
+ ];
+
+ const result = parseGuardrailDefinitions(hostile);
+
+ expect(result.definitions).toEqual([]);
+ expect(result.invalid).toHaveLength(hostile.length);
+ // The `__proto__` entry must not have reached the prototype chain.
+ expect(Object.prototype).not.toHaveProperty('validator');
+ });
+ });
+
+ it('drops a bad definition whole and keeps the rest', () => {
+ const result = parseGuardrailDefinitions(RAW_PAYLOAD_WITH_NOISE);
+
+ expect(result.definitions.map((d) => d.validator)).toEqual([
+ 'prompt_injection',
+ 'user_prompt_attacks',
+ ]);
+ expect(result.invalid).toEqual([
+ { index: 1, validator: 'broken_validator', message: expect.stringContaining('status') },
+ ]);
+ });
+
+ it('drops a definition when a single parameter is malformed', () => {
+ const result = parseGuardrailDefinitions([
+ {
+ ...PROMPT_INJECTION_WIRE,
+ parameters: [{ id: 'threshold', type: 'number', required: true, defaultValue: 'high' }],
+ },
+ ]);
+
+ expect(result.definitions).toEqual([]);
+ expect(result.invalid[0]).toMatchObject({ index: 0, validator: 'prompt_injection' });
+ });
+
+ it('omits the validator on an issue when the entry has no readable one', () => {
+ const result = parseGuardrailDefinitions([{ allowedScopes: ['Llm'] }]);
+
+ expect(result.invalid[0]).toEqual({ index: 0, message: expect.any(String) });
+ expect(result.invalid[0]).not.toHaveProperty('validator');
+ });
+
+ it('strips keys this package does not model', () => {
+ const result = parseGuardrailDefinitions(RAW_PAYLOAD_WITH_NOISE);
+
+ expect(result.definitions[0]).not.toHaveProperty('executionStage');
+ expect(result.definitions[0]).not.toHaveProperty('internalRanking');
+ });
+
+ describe('normalization', () => {
+ it('treats an empty display string as absent, so it cannot beat curated copy', () => {
+ const result = parseGuardrailDefinitions(RAW_PAYLOAD_WITH_NOISE);
+
+ expect(result.definitions[0]?.displayName).toBeUndefined();
+ expect(result.definitions[0]?.description).toBeUndefined();
+ });
+
+ it('treats an empty parameter displayName as absent', () => {
+ const result = parseGuardrailDefinitions([
+ {
+ ...PROMPT_INJECTION_WIRE,
+ parameters: [
+ {
+ id: 'threshold',
+ type: 'number',
+ required: true,
+ defaultValue: 0.7,
+ displayName: '',
+ description: '',
+ },
+ ],
+ },
+ ]);
+
+ const param = result.definitions[0]?.parameters[0];
+ expect(param?.displayName).toBeUndefined();
+ expect(param?.description).toBeUndefined();
+ });
+
+ it('treats an empty byoValidatorName as "not bring-your-own"', () => {
+ const result = parseGuardrailDefinitions([
+ { ...PROMPT_INJECTION_WIRE, byoValidatorName: '' },
+ ]);
+
+ expect(result.definitions[0]?.byoValidatorName).toBeUndefined();
+ });
+
+ it('defaults a missing parameters array to empty', () => {
+ const result = parseGuardrailDefinitions([
+ { validator: 'x', allowedScopes: ['Llm'], status: 'Available' },
+ ]);
+
+ expect(result.definitions[0]?.parameters).toEqual([]);
+ });
+ });
+
+ describe('accepts both products nullability variants', () => {
+ it('accepts null text and enum defaults, and a missing text-list default', () => {
+ const result = parseGuardrailDefinitions([LLM_AS_JUDGE_WIRE]);
+
+ const byId = new Map(result.definitions[0]?.parameters.map((p) => [p.id, p]));
+ expect(byId.get('guardrailText')).toMatchObject({ defaultValue: null });
+ expect(byId.get('model')).toMatchObject({ defaultValue: null });
+ expect(byId.get('positiveExamples')).not.toHaveProperty('defaultValue');
+ expect(byId.get('negativeExamples')).toMatchObject({ defaultValue: null });
+ });
+
+ it('accepts null numeric constraints', () => {
+ const result = parseGuardrailDefinitions([
+ {
+ ...PROMPT_INJECTION_WIRE,
+ parameters: [
+ {
+ id: 'threshold',
+ type: 'number',
+ required: true,
+ defaultValue: 0.7,
+ min: null,
+ max: null,
+ step: null,
+ },
+ ],
+ },
+ ]);
+
+ expect(result.definitions).toHaveLength(1);
+ });
+ });
+
+ it('rejects a scope the display layer cannot render', () => {
+ const result = parseGuardrailDefinitions([
+ { ...PROMPT_INJECTION_WIRE, allowedScopes: ['Llm', 'Workflow'] },
+ ]);
+
+ expect(result.definitions).toEqual([]);
+ });
+});
+
+describe('zod boundary', () => {
+ const folder = dirname(fileURLToPath(import.meta.url));
+
+ function sourceFiles(dir: string): string[] {
+ return readdirSync(dir, { withFileTypes: true }).flatMap((entry) => {
+ const full = join(dir, entry.name);
+ if (entry.isDirectory()) return sourceFiles(full);
+ return /\.tsx?$/.test(entry.name) ? [full] : [];
+ });
+ }
+
+ it('is confined to definitions-parse.ts', () => {
+ // Anything else importing zod would put schema types in this folder's emitted
+ // declarations and force the dependency on every consumer of the subpath.
+ const importers = sourceFiles(folder)
+ .filter((file) => !file.includes('.test.'))
+ .filter((file) => /from ['"]zod['"]|require\(['"]zod['"]\)/.test(readFileSync(file, 'utf8')))
+ .map((file) => file.slice(folder.length + 1).replace(/\\/g, '/'));
+
+ expect(importers).toEqual(['definitions-parse.ts']);
+ });
+
+ it('produces exactly the keys the public wire type declares', () => {
+ // Assignability alone would not catch a schema field the public type never named, so
+ // the key set is pinned at runtime too.
+ const everyField = {
+ validator: 'x',
+ allowedScopes: ['Llm'],
+ parameters: [],
+ status: 'Available',
+ displayName: 'X',
+ description: 'D',
+ byoConnectorName: 'C',
+ byoConnectorKey: 'ck',
+ byoValidatorName: 'bv',
+ byoGuardrailConnectionId: 'bc',
+ byoConfigurationId: 'bcfg',
+ folderPath: 'Shared/A',
+ folderKey: 'fk',
+ };
+
+ const result = parseGuardrailDefinitions([everyField]);
+
+ expect(Object.keys(result.definitions[0] ?? {}).sort()).toEqual(Object.keys(everyField).sort());
+ });
+});
diff --git a/packages/apollo-react/src/canvas/components/Guardrails/definitions-parse.ts b/packages/apollo-react/src/canvas/components/Guardrails/definitions-parse.ts
new file mode 100644
index 0000000000..022bc8d31c
--- /dev/null
+++ b/packages/apollo-react/src/canvas/components/Guardrails/definitions-parse.ts
@@ -0,0 +1,225 @@
+import { z } from 'zod';
+import type { GuardrailDefinitionWire } from './definitions-wire';
+
+/**
+ * Runtime validation for the guardrail definitions payload.
+ *
+ * zod is private to this module. Everything exported is annotated with the hand-written
+ * types from `definitions-wire.ts`, so the emitted declarations for this folder carry no
+ * schema types and consumers never take a zod dependency (asserted by the source guard in
+ * `definitions-parse.test.ts` and by grepping the built `.d.ts`).
+ */
+
+/**
+ * Optional display string that treats `''` as absent. The empty string is load-bearing: it
+ * would otherwise win the copy precedence and render a blank label where the curated table
+ * or a humanized id belongs. `.optional()` must wrap the transform, not the reverse, or the
+ * inferred key stops being optional.
+ */
+const emptyToUndefined = z
+ .string()
+ .transform((value) => (value === '' ? undefined : value))
+ .optional();
+
+const parameterDisplayFields = {
+ displayName: emptyToUndefined,
+ description: emptyToUndefined,
+};
+
+const enumParameterSchema = z.object({
+ ...parameterDisplayFields,
+ id: z.string().min(1),
+ type: z.literal('enum'),
+ defaultValue: z.string().nullish(),
+ required: z.boolean(),
+ options: z.array(z.string()),
+ optionLabels: z.record(z.string(), z.string()).optional(),
+});
+
+const enumListParameterSchema = z.object({
+ ...parameterDisplayFields,
+ id: z.string().min(1),
+ type: z.literal('enum-list'),
+ defaultValue: z.array(z.string()),
+ required: z.boolean(),
+ options: z.array(z.string()),
+ optionLabels: z.record(z.string(), z.string()).optional(),
+});
+
+const mapEnumParameterSchema = z.object({
+ ...parameterDisplayFields,
+ id: z.string().min(1),
+ type: z.literal('map-enum'),
+ defaultValue: z.record(z.string(), z.number()),
+ required: z.boolean(),
+ keySource: z.string().min(1),
+ min: z.number().nullish(),
+ max: z.number().nullish(),
+ step: z.number().nullish(),
+});
+
+const numberParameterSchema = z.object({
+ ...parameterDisplayFields,
+ id: z.string().min(1),
+ type: z.literal('number'),
+ defaultValue: z.number(),
+ required: z.boolean(),
+ min: z.number().nullish(),
+ max: z.number().nullish(),
+ step: z.number().nullish(),
+});
+
+const textParameterSchema = z.object({
+ ...parameterDisplayFields,
+ id: z.string().min(1),
+ type: z.literal('text'),
+ defaultValue: z.string().nullish(),
+ required: z.boolean(),
+ maxLength: z.number().nullish(),
+});
+
+const textListParameterSchema = z.object({
+ ...parameterDisplayFields,
+ id: z.string().min(1),
+ type: z.literal('text-list'),
+ defaultValue: z.array(z.string()).nullish(),
+ required: z.boolean(),
+ maxItems: z.number().nullish(),
+ maxLength: z.number().nullish(),
+});
+
+const booleanParameterSchema = z.object({
+ ...parameterDisplayFields,
+ id: z.string().min(1),
+ type: z.literal('boolean'),
+ defaultValue: z.boolean(),
+ required: z.boolean(),
+});
+
+const parameterSchema = z.discriminatedUnion('type', [
+ enumParameterSchema,
+ enumListParameterSchema,
+ mapEnumParameterSchema,
+ numberParameterSchema,
+ textParameterSchema,
+ textListParameterSchema,
+ booleanParameterSchema,
+]);
+
+const definitionSchema = z.object({
+ validator: z.string().min(1),
+ allowedScopes: z.array(z.enum(['Agent', 'Llm', 'Tool'])).min(1),
+ parameters: z.array(parameterSchema).default([]),
+ status: z.enum(['Available', 'FeatureDisabled', 'Unauthorised', 'Disabled']),
+ displayName: emptyToUndefined,
+ description: emptyToUndefined,
+ byoConnectorName: emptyToUndefined,
+ byoConnectorKey: emptyToUndefined,
+ // `.min(1)` rather than `emptyToUndefined`: an empty string here would make a UiPath
+ // validator read as bring-your-own, which changes copy resolution and palette grouping.
+ byoValidatorName: z.string().min(1).optional(),
+ byoGuardrailConnectionId: z.string().min(1).optional(),
+ byoConfigurationId: emptyToUndefined,
+ folderPath: emptyToUndefined,
+ folderKey: emptyToUndefined,
+});
+
+type DefinitionSchemaOutput = z.infer;
+
+/**
+ * The bidirectional pin between the private schema and the public mirror: a field the schema
+ * gained but `GuardrailDefinitionWire` did not, or the reverse, fails to compile here. Both
+ * directions sit on `parseGuardrailDefinitions`' hot path, so neither can be dropped as dead
+ * code, and that function's explicit return type keeps the schema types out of the `.d.ts`.
+ */
+function toWireDefinition(parsed: DefinitionSchemaOutput): GuardrailDefinitionWire {
+ const asWire: GuardrailDefinitionWire = parsed;
+ const asSchemaOutput: DefinitionSchemaOutput = asWire;
+ return asSchemaOutput;
+}
+
+/** One definition the payload carried that could not be validated. */
+export interface GuardrailDefinitionParseIssue {
+ /** Position in the input array, so a host can correlate with its own logging. */
+ index: number;
+ /** The entry's `validator`, when it at least had a readable one. */
+ validator?: string;
+ /** Human-readable summary of what failed, for logs. Not user-facing copy. */
+ message: string;
+}
+
+export interface GuardrailDefinitionsParseResult {
+ /** Every definition that validated, in payload order. */
+ definitions: GuardrailDefinitionWire[];
+ /** Every definition that did not, dropped whole. */
+ invalid: GuardrailDefinitionParseIssue[];
+ /** Set when the payload was not an array at all; `definitions` is then empty. */
+ inputError?: string;
+}
+
+function describeInput(input: unknown): string {
+ if (input === null) return 'null';
+ if (Array.isArray(input)) return 'array';
+ return typeof input;
+}
+
+function readValidator(entry: unknown): string | undefined {
+ if (typeof entry !== 'object' || entry === null) return undefined;
+ const validator = (entry as { validator?: unknown }).validator;
+ return typeof validator === 'string' && validator !== '' ? validator : undefined;
+}
+
+function describeIssues(error: z.ZodError): string {
+ return error.issues
+ .slice(0, 5)
+ .map((issue) => {
+ const path = issue.path.join('.');
+ return path ? `${path}: ${issue.message}` : issue.message;
+ })
+ .join('; ');
+}
+
+/**
+ * Validate an unknown definitions payload.
+ *
+ * **Never throws.** A payload that is not an array yields an empty result plus `inputError`;
+ * an individual definition that fails validation is dropped whole and reported in `invalid`,
+ * which is what both products already do entry by entry today. Unknown keys are stripped.
+ *
+ * The only normalizations applied are the two that would otherwise corrupt display: empty
+ * display strings become `undefined`, and an empty `byoValidatorName` is treated as absent.
+ * Nothing else is reshaped here. Turning the wire shape into something renderable is
+ * `enrichGuardrailDefinitions`' job.
+ */
+export function parseGuardrailDefinitions(input: unknown): GuardrailDefinitionsParseResult {
+ if (!Array.isArray(input)) {
+ return {
+ definitions: [],
+ invalid: [],
+ inputError: `Expected an array of guardrail definitions, received ${describeInput(input)}.`,
+ };
+ }
+
+ const definitions: GuardrailDefinitionWire[] = [];
+ const invalid: GuardrailDefinitionParseIssue[] = [];
+
+ input.forEach((entry: unknown, index: number) => {
+ // A hostile payload must not be able to take a host panel down, so even an unexpected
+ // throw out of zod degrades to a dropped definition.
+ let message: string;
+ try {
+ const result = definitionSchema.safeParse(entry);
+ if (result.success) {
+ definitions.push(toWireDefinition(result.data));
+ return;
+ }
+ message = describeIssues(result.error);
+ } catch (error: unknown) {
+ message = error instanceof Error ? error.message : String(error);
+ }
+ const validator = readValidator(entry);
+ invalid.push(validator === undefined ? { index, message } : { index, validator, message });
+ });
+
+ return { definitions, invalid };
+}
diff --git a/packages/apollo-react/src/canvas/components/Guardrails/definitions-wire.ts b/packages/apollo-react/src/canvas/components/Guardrails/definitions-wire.ts
new file mode 100644
index 0000000000..59ad752b91
--- /dev/null
+++ b/packages/apollo-react/src/canvas/components/Guardrails/definitions-wire.ts
@@ -0,0 +1,101 @@
+import type { GuardrailDefinitionStatus, GuardrailScope } from './builder-types';
+
+/**
+ * Hand-written mirrors of the `GET /api/execution/guardrails/definitions` payload.
+ *
+ * This is the public shape of the transport: `parseGuardrailDefinitions` validates unknown
+ * input into it, and hosts that keep their own transport (Agents' SWR, Flow's react-query,
+ * Flow vsix's postMessage) assign their equivalents straight across. zod stays private to
+ * `definitions-parse.ts` so no schema types reach the emitted declarations; the two are
+ * pinned to each other by a compile-time assignability check there.
+ *
+ * Where the two products' schemas differ in strictness the mirror admits both: Agents marks
+ * the numeric constraints and the `text`/`enum` defaults `nullish`, Flow marks the same
+ * defaults `nullable` and omits `folderPath`. Scope and status are reused from
+ * `builder-types` rather than redeclared, so the wire and display layers cannot drift.
+ */
+
+/** The fields every parameter kind carries. */
+export interface GuardrailParameterWireBase {
+ id: string;
+ required: boolean;
+ /**
+ * Display copy shipped by the manifest. BYO guardrails carry their own; UiPath-managed
+ * validators omit it and resolve from the curated copy table instead.
+ */
+ displayName?: string;
+ description?: string;
+}
+
+/**
+ * One configurable parameter as the backend describes it, discriminated on `type`. The seven
+ * kinds match `GuardrailValidatorParameter`'s `$parameterType` one for one.
+ */
+export type GuardrailParameterDefinitionWire =
+ | (GuardrailParameterWireBase & {
+ type: 'enum';
+ defaultValue?: string | null;
+ options: string[];
+ optionLabels?: Record;
+ })
+ | (GuardrailParameterWireBase & {
+ type: 'enum-list';
+ defaultValue: string[];
+ options: string[];
+ optionLabels?: Record;
+ })
+ | (GuardrailParameterWireBase & {
+ type: 'map-enum';
+ defaultValue: Record;
+ /** Id of the sibling `enum-list` parameter whose selection provides the map's keys. */
+ keySource: string;
+ min?: number | null;
+ max?: number | null;
+ step?: number | null;
+ })
+ | (GuardrailParameterWireBase & {
+ type: 'number';
+ defaultValue: number;
+ min?: number | null;
+ max?: number | null;
+ step?: number | null;
+ })
+ | (GuardrailParameterWireBase & {
+ type: 'text';
+ defaultValue?: string | null;
+ maxLength?: number | null;
+ })
+ | (GuardrailParameterWireBase & {
+ type: 'text-list';
+ /** Absent on some validators (Agents' schema has no such field at all). */
+ defaultValue?: string[] | null;
+ maxItems?: number | null;
+ maxLength?: number | null;
+ })
+ | (GuardrailParameterWireBase & {
+ type: 'boolean';
+ defaultValue: boolean;
+ });
+
+/** One guardrail validator definition as the backend returns it. */
+export interface GuardrailDefinitionWire {
+ validator: string;
+ allowedScopes: GuardrailScope[];
+ parameters: GuardrailParameterDefinitionWire[];
+ status: GuardrailDefinitionStatus;
+ displayName?: string;
+ description?: string;
+ byoConnectorName?: string;
+ byoConnectorKey?: string;
+ /** Present only for bring-your-own guardrails; its presence is what makes one BYO. */
+ byoValidatorName?: string;
+ byoGuardrailConnectionId?: string;
+ byoConfigurationId?: string;
+ /**
+ * Not returned by the definitions endpoint. Both products resolve it from their
+ * connections API and stamp it on afterwards; `withGuardrailFolderMetadata` is the shared
+ * helper for that step.
+ */
+ folderPath?: string;
+ folderKey?: string;
+}
diff --git a/packages/apollo-react/src/canvas/components/Guardrails/form-schema-builder.test.ts b/packages/apollo-react/src/canvas/components/Guardrails/form-schema-builder.test.ts
new file mode 100644
index 0000000000..76732ff97e
--- /dev/null
+++ b/packages/apollo-react/src/canvas/components/Guardrails/form-schema-builder.test.ts
@@ -0,0 +1,233 @@
+import { describe, expect, it } from 'vitest';
+import {
+ buildGuardrailFormSchema,
+ coerceGuardrailParameterValue,
+ GUARDRAIL_ENUM_LIST_CHIPS_COMPONENT,
+ GUARDRAIL_MAP_ENUM_COMPONENT,
+ GUARDRAIL_RENDER_PARAMETER_COMPONENT,
+ MAX_INLINE_ENUM_OPTIONS,
+} from './form-schema-builder';
+import { GUARDRAIL_FORM_EN_LABELS } from './i18n';
+import type { GuardrailParameterDefinition, GuardrailValidatorParameter } from './types';
+
+const LABELS = GUARDRAIL_FORM_EN_LABELS;
+
+function fieldFor(
+ def: GuardrailParameterDefinition,
+ options?: Parameters[2],
+ siblings: GuardrailParameterDefinition[] = []
+) {
+ const schema = buildGuardrailFormSchema([def, ...siblings], LABELS, options);
+ const field = schema.sections?.[0]?.fields.find((f) => f.name === def.id);
+ if (!field) throw new Error(`no field built for ${def.id}`);
+ return field;
+}
+
+describe('buildGuardrailFormSchema', () => {
+ it('builds a submit-less single-section schema', () => {
+ const schema = buildGuardrailFormSchema([], LABELS);
+ expect(schema.actions).toEqual([]);
+ expect(schema.sections).toHaveLength(1);
+ });
+
+ it('maps common metadata: label, tooltip, required asterisk config', () => {
+ const field = fieldFor({
+ id: 'threshold',
+ type: 'number',
+ label: 'Threshold',
+ required: true,
+ tooltip: 'How strict.',
+ defaultValue: 0.5,
+ });
+ expect(field).toMatchObject({
+ name: 'threshold',
+ label: 'Threshold',
+ tooltip: 'How strict.',
+ tooltipAriaLabel: LABELS.moreInformation,
+ validation: { required: true },
+ });
+ });
+
+ it('maps number -> number with constraints and a 0 fallback default', () => {
+ const field = fieldFor({
+ id: 'n',
+ type: 'number',
+ label: 'N',
+ required: false,
+ defaultValue: null,
+ min: 0,
+ max: 1,
+ step: 0.1,
+ });
+ expect(field).toMatchObject({ type: 'number', min: 0, max: 1, step: 0.1, defaultValue: 0 });
+ });
+
+ it('maps text -> textarea with minRows and maxLength', () => {
+ const field = fieldFor({
+ id: 't',
+ type: 'text',
+ label: 'T',
+ required: false,
+ defaultValue: null,
+ maxLength: 100,
+ });
+ expect(field).toMatchObject({ type: 'textarea', minRows: 3, maxLength: 100, defaultValue: '' });
+ });
+
+ it('maps boolean -> switch and text-list -> string-list with localized chrome', () => {
+ expect(
+ fieldFor({ id: 'b', type: 'boolean', label: 'B', required: false, defaultValue: true })
+ ).toMatchObject({ type: 'switch', defaultValue: true });
+
+ expect(
+ fieldFor({
+ id: 'phrases',
+ type: 'text-list',
+ label: 'Phrases',
+ required: false,
+ defaultValue: null,
+ maxItems: 3,
+ maxLength: 50,
+ })
+ ).toMatchObject({
+ type: 'string-list',
+ maxItems: 3,
+ maxLength: 50,
+ minRows: 2,
+ addItemLabel: LABELS.addItem,
+ removeItemAriaLabel: LABELS.removeItem,
+ defaultValue: [],
+ });
+ });
+
+ it('maps enum -> select and appends a synthetic option for a stale stored value', () => {
+ const def: GuardrailParameterDefinition = {
+ id: 'model',
+ type: 'enum',
+ label: 'Model',
+ required: true,
+ defaultValue: 'gpt-4o',
+ options: ['gpt-4o', 'gpt-4o-mini'],
+ optionLabels: { 'gpt-4o': 'GPT-4o' },
+ };
+
+ const plain = fieldFor(def);
+ expect(plain).toMatchObject({ type: 'select', placeholder: LABELS.enumPlaceholder });
+ if (plain.type !== 'select') throw new Error('expected select');
+ expect(plain.options).toEqual([
+ { value: 'gpt-4o', label: 'GPT-4o' },
+ { value: 'gpt-4o-mini', label: 'gpt-4o-mini' },
+ ]);
+
+ const withStale = fieldFor(def, { enumSyntheticValues: { model: 'retired-model' } });
+ if (withStale.type !== 'select') throw new Error('expected select');
+ expect(withStale.options).toContainEqual({ value: 'retired-model', label: 'retired-model' });
+ });
+
+ it('splits enum-list at the inline-chip cap: chips custom component vs multiselect', () => {
+ const small: GuardrailParameterDefinition = {
+ id: 'few',
+ type: 'enum-list',
+ label: 'Few',
+ required: true,
+ defaultValue: [],
+ options: Array.from({ length: MAX_INLINE_ENUM_OPTIONS }, (_, i) => `o${i}`),
+ };
+ const large: GuardrailParameterDefinition = {
+ ...small,
+ id: 'many',
+ options: Array.from({ length: MAX_INLINE_ENUM_OPTIONS + 1 }, (_, i) => `o${i}`),
+ };
+
+ expect(fieldFor(small)).toMatchObject({
+ type: 'custom',
+ component: GUARDRAIL_ENUM_LIST_CHIPS_COMPONENT,
+ });
+ const many = fieldFor(large);
+ expect(many).toMatchObject({ type: 'multiselect', placeholder: LABELS.enumListPlaceholder });
+ });
+
+ it('maps map-enum to its custom component with the source definition and mount-time selection', () => {
+ const entities: GuardrailParameterDefinition = {
+ id: 'entities',
+ type: 'enum-list',
+ label: 'Entities',
+ required: true,
+ defaultValue: ['Email'],
+ options: ['Email', 'Address'],
+ };
+ const thresholds: GuardrailParameterDefinition = {
+ id: 'thresholds',
+ type: 'map-enum',
+ label: 'Thresholds',
+ required: false,
+ defaultValue: { Email: 0.5, Address: 0.5 },
+ keySource: 'entities',
+ };
+ const initialParameters: GuardrailValidatorParameter[] = [
+ { $parameterType: 'enum-list', id: 'entities', value: ['Email', 'Address'] },
+ ];
+
+ const field = fieldFor(thresholds, { initialParameters }, [entities]);
+ expect(field).toMatchObject({ type: 'custom', component: GUARDRAIL_MAP_ENUM_COMPONENT });
+ if (field.type !== 'custom') throw new Error('expected custom');
+ expect(field.componentProps).toMatchObject({
+ sourceDef: entities,
+ sourceSelection: ['Email', 'Address'],
+ });
+ });
+
+ it('routes overridden ids to the renderParameter bridge regardless of type', () => {
+ const field = fieldFor(
+ {
+ id: 'model',
+ type: 'enum',
+ label: 'Model',
+ required: true,
+ defaultValue: 'a',
+ options: ['a'],
+ },
+ { overriddenIds: new Set(['model']) }
+ );
+ expect(field).toMatchObject({
+ type: 'custom',
+ component: GUARDRAIL_RENDER_PARAMETER_COMPONENT,
+ });
+ });
+
+ it('seeds field defaults from initialParameters over definition defaults', () => {
+ const field = fieldFor(
+ { id: 'n', type: 'number', label: 'N', required: false, defaultValue: 0.5 },
+ {
+ initialParameters: [{ $parameterType: 'number', id: 'n', value: 0.8 }],
+ }
+ );
+ expect(field.defaultValue).toBe(0.8);
+ });
+});
+
+describe('coerceGuardrailParameterValue', () => {
+ const def = (type: GuardrailParameterDefinition['type']): GuardrailParameterDefinition => ({
+ id: 'x',
+ type,
+ label: 'X',
+ required: false,
+ defaultValue: null,
+ });
+
+ it('coerces a cleared number input (NaN) to 0, like the pre-convergence editor', () => {
+ expect(coerceGuardrailParameterValue(Number.NaN, def('number'))).toBe(0);
+ expect(coerceGuardrailParameterValue(0.7, def('number'))).toBe(0.7);
+ });
+
+ it('never persists null/undefined for text, enum, or boolean', () => {
+ expect(coerceGuardrailParameterValue(undefined, def('text'))).toBe('');
+ expect(coerceGuardrailParameterValue(undefined, def('enum'))).toBe('');
+ expect(coerceGuardrailParameterValue(undefined, def('boolean'))).toBe(false);
+ });
+
+ it('passes arrays and maps through untouched', () => {
+ expect(coerceGuardrailParameterValue(['a'], def('enum-list'))).toEqual(['a']);
+ expect(coerceGuardrailParameterValue({ a: 1 }, def('map-enum'))).toEqual({ a: 1 });
+ });
+});
diff --git a/packages/apollo-react/src/canvas/components/Guardrails/form-schema-builder.ts b/packages/apollo-react/src/canvas/components/Guardrails/form-schema-builder.ts
new file mode 100644
index 0000000000..6f2e125a9e
--- /dev/null
+++ b/packages/apollo-react/src/canvas/components/Guardrails/form-schema-builder.ts
@@ -0,0 +1,224 @@
+import type { FieldMetadata, FormSchema } from '@uipath/apollo-wind';
+import type { GuardrailValidatorFormLabels } from './i18n';
+import type { GuardrailParameterDefinition, GuardrailValidatorParameter } from './types';
+
+/** Registered custom-component names for the parameter editors that stay guardrail-owned. */
+export const GUARDRAIL_ENUM_LIST_CHIPS_COMPONENT = 'guardrail-enum-list-chips';
+export const GUARDRAIL_MAP_ENUM_COMPONENT = 'guardrail-map-enum';
+export const GUARDRAIL_RENDER_PARAMETER_COMPONENT = 'guardrail-render-parameter';
+
+/** enum-list option sets at or below this render as inline chips; larger sets as MultiSelect. */
+export const MAX_INLINE_ENUM_OPTIONS = 8;
+
+export interface BuildGuardrailFormSchemaOptions {
+ /** Definition ids claimed by the host's `renderParameter` override. */
+ overriddenIds?: ReadonlySet;
+ /**
+ * Mount-time parameter values: they seed each field's `defaultValue` so the first paint is
+ * correct before the controlled `values` sync-in runs. Later external changes arrive
+ * through `values`, never through a schema rebuild.
+ */
+ initialParameters?: GuardrailValidatorParameter[];
+ /**
+ * Per-enum-definition stored values missing from the option catalog (e.g. an option removed
+ * after the guardrail was saved); each is appended as a synthetic option so the select never
+ * silently blanks.
+ */
+ enumSyntheticValues?: Record;
+}
+
+/**
+ * Map guardrail parameter definitions onto the forms/ `MetadataForm` schema. Five of the
+ * seven parameter types map to first-class field types (`number`, `textarea`, `switch`,
+ * `select`, `multiselect`/`string-list`); enum-lists small enough for the chip UX, `map-enum`
+ * (keySource-driven rows), and host `renderParameter` overrides register as custom
+ * components. `required` drives only the asterisk — the host owns validation, so the form is
+ * mounted with `disableValidation`.
+ */
+export function buildGuardrailFormSchema(
+ definitions: GuardrailParameterDefinition[],
+ labels: GuardrailValidatorFormLabels,
+ options?: BuildGuardrailFormSchemaOptions
+): FormSchema {
+ const initialById = new Map((options?.initialParameters ?? []).map((p) => [p.id, p]));
+
+ const fields = definitions.map((def) =>
+ buildGuardrailField(def, definitions, labels, initialById, options)
+ );
+
+ return {
+ id: 'guardrail-validator-form',
+ title: '',
+ actions: [],
+ sections: [{ id: 'parameters', fields }],
+ };
+}
+
+function buildGuardrailField(
+ def: GuardrailParameterDefinition,
+ definitions: GuardrailParameterDefinition[],
+ labels: GuardrailValidatorFormLabels,
+ initialById: Map,
+ options?: BuildGuardrailFormSchemaOptions
+): FieldMetadata {
+ const initial = initialById.get(def.id);
+ const base = {
+ name: def.id,
+ label: def.label,
+ tooltip: def.tooltip,
+ tooltipAriaLabel: labels.moreInformation,
+ validation: def.required ? { required: true } : undefined,
+ };
+
+ if (options?.overriddenIds?.has(def.id)) {
+ return {
+ ...base,
+ type: 'custom',
+ component: GUARDRAIL_RENDER_PARAMETER_COMPONENT,
+ defaultValue: initial?.value,
+ };
+ }
+
+ switch (def.type) {
+ case 'number':
+ return {
+ ...base,
+ type: 'number',
+ min: def.min,
+ max: def.max,
+ step: def.step,
+ defaultValue:
+ (initial?.$parameterType === 'number' ? initial.value : (def.defaultValue as number)) ??
+ 0,
+ };
+
+ case 'text':
+ return {
+ ...base,
+ type: 'textarea',
+ minRows: 3,
+ maxLength: def.maxLength,
+ defaultValue:
+ initial?.$parameterType === 'text'
+ ? initial.value
+ : ((def.defaultValue as string | null) ?? ''),
+ };
+
+ case 'boolean':
+ return {
+ ...base,
+ type: 'switch',
+ defaultValue:
+ initial?.$parameterType === 'boolean'
+ ? initial.value
+ : ((def.defaultValue as boolean | null | undefined) ?? false),
+ };
+
+ case 'enum': {
+ const current =
+ initial?.$parameterType === 'enum'
+ ? initial.value
+ : ((def.defaultValue as string | null) ?? '');
+ const catalog = (def.options ?? []).map((opt) => ({
+ value: opt,
+ label: def.optionLabels?.[opt] ?? opt,
+ }));
+ const synthetic = options?.enumSyntheticValues?.[def.id];
+ const opts =
+ synthetic && !catalog.some((opt) => opt.value === synthetic)
+ ? [...catalog, { value: synthetic, label: def.optionLabels?.[synthetic] ?? synthetic }]
+ : catalog;
+ return {
+ ...base,
+ type: 'select',
+ placeholder: labels.enumPlaceholder,
+ options: opts,
+ defaultValue: current,
+ };
+ }
+
+ case 'enum-list': {
+ const selected =
+ initial?.$parameterType === 'enum-list'
+ ? initial.value
+ : ((def.defaultValue as string[]) ?? []);
+ if ((def.options ?? []).length <= MAX_INLINE_ENUM_OPTIONS) {
+ return {
+ ...base,
+ type: 'custom',
+ component: GUARDRAIL_ENUM_LIST_CHIPS_COMPONENT,
+ defaultValue: selected,
+ componentProps: { paramDef: def, labels },
+ };
+ }
+ return {
+ ...base,
+ type: 'multiselect',
+ placeholder: labels.enumListPlaceholder,
+ options: (def.options ?? []).map((opt) => ({
+ value: opt,
+ label: def.optionLabels?.[opt] ?? opt,
+ })),
+ defaultValue: selected,
+ };
+ }
+
+ case 'text-list':
+ return {
+ ...base,
+ type: 'string-list',
+ maxItems: def.maxItems,
+ maxLength: def.maxLength,
+ minRows: 2,
+ addItemLabel: labels.addItem,
+ removeItemAriaLabel: labels.removeItem,
+ defaultValue:
+ initial?.$parameterType === 'text-list'
+ ? initial.value
+ : ((def.defaultValue as string[] | null | undefined) ?? []),
+ };
+
+ case 'map-enum': {
+ const sourceDef = def.keySource ? definitions.find((d) => d.id === def.keySource) : undefined;
+ // The sibling's mount-time selection: `useWatch` in the editor returns undefined
+ // until the sibling field's first change event, so the initial row keys are seeded
+ // through componentProps.
+ const sourceInitial = def.keySource ? initialById.get(def.keySource) : undefined;
+ const sourceSelection =
+ sourceInitial?.$parameterType === 'enum-list'
+ ? sourceInitial.value
+ : (sourceDef?.defaultValue as string[] | undefined);
+ return {
+ ...base,
+ type: 'custom',
+ component: GUARDRAIL_MAP_ENUM_COMPONENT,
+ // No fallback to def.defaultValue here: the editor falls back at render time (and
+ // never prunes), exactly like the pre-convergence editor.
+ defaultValue: initial?.$parameterType === 'map-enum' ? initial.value : undefined,
+ componentProps: { paramDef: def, sourceDef, labels, sourceSelection },
+ };
+ }
+ }
+}
+
+/**
+ * Coerce a form-emitted value to the parameter's wire shape, mirroring the pre-convergence
+ * editors: number inputs emit NaN when cleared (`parseFloat('')`) which persisted as 0;
+ * text/enum never persist null/undefined.
+ */
+export function coerceGuardrailParameterValue(
+ value: unknown,
+ def: GuardrailParameterDefinition
+): unknown {
+ switch (def.type) {
+ case 'number':
+ return typeof value === 'number' && !Number.isNaN(value) ? value : 0;
+ case 'text':
+ case 'enum':
+ return value ?? '';
+ case 'boolean':
+ return value ?? false;
+ default:
+ return value;
+ }
+}
diff --git a/packages/apollo-react/src/canvas/components/Guardrails/guardrail-builder.stories.tsx b/packages/apollo-react/src/canvas/components/Guardrails/guardrail-builder.stories.tsx
new file mode 100644
index 0000000000..0c8d02fddf
--- /dev/null
+++ b/packages/apollo-react/src/canvas/components/Guardrails/guardrail-builder.stories.tsx
@@ -0,0 +1,345 @@
+import type { Meta, StoryObj } from '@storybook/react-vite';
+import {
+ Button,
+ FormField,
+ FormFieldError,
+ FormFieldLabel,
+ Input,
+ TooltipProvider,
+} from '@uipath/apollo-wind';
+import { useMemo, useState } from 'react';
+import { ApI18nProvider } from '../../../i18n';
+import type { GuardrailBuilderValue, GuardrailDefinition } from './builder-types';
+import { GuardrailBuilder } from './guardrail-builder';
+
+const meta = {
+ title: 'Components/UiPath/Guardrail Builder',
+ component: GuardrailBuilder,
+ parameters: {
+ layout: 'padded',
+ docs: {
+ description: {
+ component: `
+The complete Add/Edit screen for an OOTB guardrail validator: status banners, usage note,
+name, description, validator parameters, scope selector, action (including escalation via
+host slots), evaluations toggle, mixed-scopes banner, and the Save/Cancel footer.
+
+The builder owns its form state (initialized at mount: remount with a new key to reset) and
+its validation (it gates Save). Hosts override any message via the labels prop or any field
+via the errors prop. The escalation recipient search and app picker are host capabilities
+injected through render props.
+ `,
+ },
+ },
+ },
+ tags: ['autodocs'],
+ decorators: [
+ (Story) => (
+
+
+
+
+
+ ),
+ ],
+} satisfies Meta;
+
+export default meta;
+type Story = StoryObj;
+
+const piiDefinition: GuardrailDefinition = {
+ validator: 'pii_detection',
+ displayName: 'PII detection',
+ allowedScopes: ['Agent', 'Llm', 'Tool'],
+ status: 'Available',
+ parameters: [
+ {
+ id: 'entities',
+ type: 'enum-list',
+ label: 'Entities to detect',
+ required: true,
+ defaultValue: ['Email'],
+ options: ['Person', 'Address', 'Email', 'PhoneNumber'],
+ optionLabels: {
+ Person: 'Person name',
+ Address: 'Street address',
+ Email: 'Email address',
+ PhoneNumber: 'Phone number',
+ },
+ },
+ {
+ id: 'entityThresholds',
+ type: 'map-enum',
+ label: 'Detection thresholds',
+ required: false,
+ keySource: 'entities',
+ min: 0,
+ max: 1,
+ step: 0.1,
+ defaultValue: { Person: 0.5, Address: 0.5, Email: 0.5, PhoneNumber: 0.5 },
+ },
+ ],
+};
+
+const existingGuardrail: GuardrailBuilderValue = {
+ id: 'g1',
+ $guardrailType: 'builtInValidator',
+ name: 'PII detection 1',
+ description: 'Scans agent output for personal data.',
+ selector: { scopes: ['Agent', 'Tool'], matchNames: ['Send email'] },
+ action: { $actionType: 'log', severityLevel: 'Warning' },
+ enabledForEvals: true,
+ validatorType: 'pii_detection',
+ validatorParameters: [
+ { $parameterType: 'enum-list', id: 'entities', value: ['Email', 'PhoneNumber'] },
+ { $parameterType: 'map-enum', id: 'entityThresholds', value: { Email: 0.7, PhoneNumber: 0.5 } },
+ ],
+};
+
+/** The Add screen as the palette embeds it: inline, header owned by the host. */
+export const AddInline: Story = {
+ args: {
+ open: true,
+ inline: true,
+ hideHeader: true,
+ definition: piiDefinition,
+ scope: 'Agent',
+ defaultName: 'PII detection 1',
+ availableToolNames: ['Send email', 'Search web'],
+ onSave: () => {},
+ onCancel: () => {},
+ },
+};
+
+/** Edit mode as an inline panel with the back-button header. */
+export const EditInline: Story = {
+ args: {
+ open: true,
+ inline: true,
+ definition: piiDefinition,
+ scope: 'Agent',
+ guardrail: existingGuardrail,
+ availableToolNames: ['Send email', 'Search web'],
+ onSave: () => {},
+ onCancel: () => {},
+ },
+};
+
+/** Edit mode in the modal dialog shell. */
+export const EditModal: Story = {
+ decorators: [
+ (Story) => (
+
+
+
+ ),
+ ],
+ args: {
+ open: true,
+ definition: piiDefinition,
+ scope: 'Agent',
+ guardrail: existingGuardrail,
+ availableToolNames: ['Send email'],
+ onSave: () => {},
+ onCancel: () => {},
+ },
+};
+
+/** A definition-level usage note renders as an informational banner. */
+export const WithUsageNote: Story = {
+ args: {
+ ...AddInline.args,
+ definition: {
+ ...piiDefinition,
+ usageNote: 'Each evaluation of this guardrail is billed like an agent LLM call.',
+ },
+ },
+};
+
+export const StatusUnauthorised: Story = {
+ args: { ...AddInline.args, definition: { ...piiDefinition, status: 'Unauthorised' } },
+};
+
+export const StatusFeatureDisabled: Story = {
+ args: { ...AddInline.args, definition: { ...piiDefinition, status: 'FeatureDisabled' } },
+};
+
+export const StatusByoDisabled: Story = {
+ args: {
+ ...AddInline.args,
+ definition: {
+ ...piiDefinition,
+ displayName: 'Vendor PII',
+ byoValidatorName: 'vendor-pii',
+ status: 'Disabled',
+ },
+ },
+};
+
+/**
+ * The escalation recipient search and app picker are host capabilities. This story injects
+ * demo implementations: a static user list and a fake picker button.
+ */
+export const EscalateWithSlots: Story = {
+ args: { ...AddInline.args },
+ render: (args) => {
+ function EscalateExample() {
+ const users = useMemo(
+ () => [
+ { value: 'u1', displayName: 'Ada Lovelace' },
+ { value: 'u2', displayName: 'Grace Hopper' },
+ ],
+ []
+ );
+ return (
+ (
+
+ {users.map((u) => (
+ ctx.onSelect(u)}
+ >
+ {u.displayName}
+
+ ))}
+
+ )}
+ renderAppPicker={(ctx) => (
+
+ {ctx.label}
+
+ ctx.onChange({ id: 'app-1', version: '1.0', name: 'Escalation app' })
+ }
+ >
+ {ctx.app?.name ?? 'Pick an escalation app'}
+
+ {ctx.error}
+
+ )}
+ escalateHelp={
+
+ Escalation uses apps compatible with the guardrails app template.{' '}
+
+ Import one from Marketplace
+
+
+ }
+ />
+ );
+ }
+ return ;
+ },
+};
+
+/** Mixed scopes: the banner lists other applications and the footer gains Save as new. */
+export const MixedScopesWithSaveAsNew: Story = {
+ args: {
+ open: true,
+ inline: true,
+ hideHeader: true,
+ definition: piiDefinition,
+ scope: 'Tool',
+ guardrail: existingGuardrail,
+ toolName: 'Send email',
+ otherAppliedScopes: { scopes: ['Agent'], tools: ['Search web'] },
+ onSave: () => {},
+ onSaveAsNew: () => {},
+ onCancel: () => {},
+ },
+};
+
+/** Pressing Save with empty required fields surfaces the builder's internal validation. */
+export const ValidationErrors: Story = {
+ args: {
+ open: true,
+ inline: true,
+ hideHeader: true,
+ definition: {
+ ...piiDefinition,
+ parameters: [
+ { id: 'prompt', type: 'text', label: 'Rule prompt', required: true, defaultValue: null },
+ ...piiDefinition.parameters,
+ ],
+ },
+ scope: 'Agent',
+ defaultName: '',
+ guardrail: undefined,
+ onSave: () => {},
+ onCancel: () => {},
+ },
+ play: async ({ canvas, userEvent }) => {
+ await userEvent.click(canvas.getByRole('button', { name: 'Save' }));
+ },
+};
+
+/** Agents-style layout: the evaluations toggle lives in the footer row. */
+export const FooterEvalsToggle: Story = {
+ args: { ...AddInline.args, evalsTogglePlacement: 'footer' },
+};
+
+/** Chrome strings resolved from the canvas lingui catalog (Japanese). */
+export const Localized: Story = {
+ args: { ...AddInline.args },
+ decorators: [
+ (Story) => (
+
+
+
+ ),
+ ],
+};
+
+/** Host errors merge over internal validation and display immediately. */
+export const HostErrorsOverride: Story = {
+ args: { ...EditInline.args, hideHeader: true },
+ render: (args) => {
+ function HostErrorsExample() {
+ const [errors, setErrors] = useState<{ name?: string } | undefined>({
+ name: 'A guardrail with this name exists on the server',
+ });
+ return (
+
+
+
+ setErrors(
+ e.target.value
+ ? undefined
+ : { name: 'A guardrail with this name exists on the server' }
+ )
+ }
+ />
+
+
+
+
+
+ );
+ }
+ return ;
+ },
+};
diff --git a/packages/apollo-react/src/canvas/components/Guardrails/guardrail-builder.test.tsx b/packages/apollo-react/src/canvas/components/Guardrails/guardrail-builder.test.tsx
new file mode 100644
index 0000000000..c9593e6ea1
--- /dev/null
+++ b/packages/apollo-react/src/canvas/components/Guardrails/guardrail-builder.test.tsx
@@ -0,0 +1,1031 @@
+import { fireEvent, render as rtlRender, screen } from '@testing-library/react';
+import { TooltipProvider } from '@uipath/apollo-wind';
+import { axe } from 'jest-axe';
+import type { ReactElement } from 'react';
+import { describe, expect, it, vi } from 'vitest';
+import type { GuardrailBuilderValue, GuardrailDefinition } from './builder-types';
+import { GuardrailBuilder } from './guardrail-builder';
+
+// The form renders a radix tooltip for the "Enable for evaluations" info icon, which
+// requires a TooltipProvider ancestor.
+function render(ui: ReactElement) {
+ return rtlRender({ui} );
+}
+
+function makeDef(overrides?: Partial): GuardrailDefinition {
+ return {
+ validator: 'pii_detection',
+ displayName: 'PII Detection',
+ allowedScopes: ['Agent', 'Llm', 'Tool'],
+ parameters: [],
+ status: 'Available',
+ ...overrides,
+ };
+}
+
+function makeGuardrail(overrides?: Partial): GuardrailBuilderValue {
+ return {
+ id: 'g1',
+ $guardrailType: 'builtInValidator',
+ name: 'My PII guardrail',
+ selector: { scopes: ['Agent'] },
+ action: { $actionType: 'log', severityLevel: 'Info' },
+ enabledForEvals: true,
+ validatorType: 'pii_detection',
+ validatorParameters: [],
+ ...overrides,
+ };
+}
+
+describe('GuardrailBuilder', () => {
+ it('shows the guardrail type as a read-only field when editing', () => {
+ render(
+
+ );
+
+ const typeInput = screen.getByLabelText('Guardrail type');
+ expect(typeInput).toBeInTheDocument();
+ expect(typeInput.value).toBe('PII Detection');
+ expect(typeInput).toBeDisabled();
+ });
+
+ it('does not show the guardrail type field when creating a new guardrail', () => {
+ render(
+
+ );
+
+ expect(screen.queryByLabelText('Guardrail type')).not.toBeInTheDocument();
+ });
+
+ describe('usageNote banner', () => {
+ it('renders the note when the definition declares one', () => {
+ const def = makeDef({ usageNote: 'Heads up: same billing as agent LLM calls.' });
+ render(
+
+ );
+ expect(screen.getByText('Heads up: same billing as agent LLM calls.')).toBeInTheDocument();
+ });
+
+ it('renders nothing when the definition omits the note', () => {
+ render(
+
+ );
+ expect(screen.queryByText(/agent llm calls/i)).not.toBeInTheDocument();
+ });
+ });
+
+ describe('disabled BYOG configuration', () => {
+ const disabledByoDefinition = (): GuardrailDefinition =>
+ makeDef({
+ validator: 'vendor_validator',
+ displayName: 'Vendor PII',
+ byoValidatorName: 'my-pii',
+ status: 'Disabled',
+ });
+
+ it('renders an error and disables Save', () => {
+ render(
+
+ );
+
+ expect(screen.getByRole('alert')).toHaveTextContent(
+ "This guardrail's configuration has been disabled"
+ );
+ expect(screen.getByRole('button', { name: /^save$/i })).toBeDisabled();
+ });
+
+ it('disables Save as new for a mixed-scope guardrail', () => {
+ render(
+
+ );
+
+ expect(screen.getByRole('button', { name: /save as new/i })).toBeDisabled();
+ });
+ });
+
+ describe('status banners', () => {
+ it('warns when the definition is unauthorised', () => {
+ render(
+
+ );
+ expect(screen.getByText(/not entitled to use guardrails/i)).toBeInTheDocument();
+ expect(screen.getByRole('button', { name: /^save$/i })).toBeDisabled();
+ });
+
+ it('warns when the feature is disabled', () => {
+ render(
+
+ );
+ expect(screen.getByText(/guardrail feature is disabled/i)).toBeInTheDocument();
+ });
+ });
+
+ describe('required-param validation blocks Save', () => {
+ const llmJudgeDefinition = (): GuardrailDefinition =>
+ makeDef({
+ validator: 'llm_as_judge',
+ displayName: 'LLM as Judge',
+ parameters: [
+ {
+ id: 'guardrailText',
+ type: 'text',
+ label: 'Rule prompt',
+ required: true,
+ defaultValue: null,
+ },
+ {
+ id: 'model',
+ type: 'enum',
+ label: 'Judge model',
+ required: true,
+ defaultValue: null,
+ options: ['m1'],
+ },
+ {
+ id: 'positiveExamples',
+ type: 'text-list',
+ label: 'Positive examples',
+ required: true,
+ defaultValue: [],
+ maxItems: 3,
+ },
+ ],
+ });
+
+ it('does not call onSave when a required text param is empty', () => {
+ const onSave = vi.fn();
+ render(
+
+ );
+
+ fireEvent.click(screen.getByRole('button', { name: /^save$/i }));
+ expect(onSave).not.toHaveBeenCalled();
+ expect(screen.getAllByText('Value is required').length).toBeGreaterThan(0);
+ });
+
+ it('does not call onSave when a required text param is whitespace-only', () => {
+ const onSave = vi.fn();
+ const existing = makeGuardrail({
+ validatorType: 'llm_as_judge',
+ validatorParameters: [
+ { $parameterType: 'text', id: 'guardrailText', value: ' ' },
+ { $parameterType: 'enum', id: 'model', value: 'm1' },
+ { $parameterType: 'text-list', id: 'positiveExamples', value: ['ok'] },
+ ],
+ });
+
+ render(
+
+ );
+
+ fireEvent.click(screen.getByRole('button', { name: /^save$/i }));
+ expect(onSave).not.toHaveBeenCalled();
+ });
+
+ it('does not call onSave when a required enum param is empty', () => {
+ const onSave = vi.fn();
+ const existing = makeGuardrail({
+ validatorType: 'llm_as_judge',
+ validatorParameters: [
+ { $parameterType: 'text', id: 'guardrailText', value: 'rule' },
+ { $parameterType: 'enum', id: 'model', value: '' },
+ { $parameterType: 'text-list', id: 'positiveExamples', value: ['ok'] },
+ ],
+ });
+
+ render(
+
+ );
+
+ fireEvent.click(screen.getByRole('button', { name: /^save$/i }));
+ expect(onSave).not.toHaveBeenCalled();
+ });
+
+ it('does not call onSave when a required text-list has only whitespace entries', () => {
+ const onSave = vi.fn();
+ const existing = makeGuardrail({
+ validatorType: 'llm_as_judge',
+ validatorParameters: [
+ { $parameterType: 'text', id: 'guardrailText', value: 'rule' },
+ { $parameterType: 'enum', id: 'model', value: 'm1' },
+ { $parameterType: 'text-list', id: 'positiveExamples', value: ['', ' '] },
+ ],
+ });
+
+ render(
+
+ );
+
+ fireEvent.click(screen.getByRole('button', { name: /^save$/i }));
+ expect(onSave).not.toHaveBeenCalled();
+ });
+
+ it('calls onSave when every required param has real content', () => {
+ const onSave = vi.fn();
+ const existing = makeGuardrail({
+ validatorType: 'llm_as_judge',
+ validatorParameters: [
+ { $parameterType: 'text', id: 'guardrailText', value: 'rule' },
+ { $parameterType: 'enum', id: 'model', value: 'm1' },
+ { $parameterType: 'text-list', id: 'positiveExamples', value: ['ok'] },
+ ],
+ });
+
+ render(
+
+ );
+
+ fireEvent.click(screen.getByRole('button', { name: /^save$/i }));
+ expect(onSave).toHaveBeenCalled();
+ });
+
+ it('strips empty / whitespace-only entries from text-list params on save', () => {
+ const onSave = vi.fn();
+ const definitionWithOptionalList = makeDef({
+ validator: 'llm_as_judge',
+ displayName: 'LLM as Judge',
+ parameters: [
+ {
+ id: 'guardrailText',
+ type: 'text',
+ label: 'Rule prompt',
+ required: true,
+ defaultValue: null,
+ },
+ {
+ id: 'model',
+ type: 'enum',
+ label: 'Judge model',
+ required: true,
+ defaultValue: null,
+ options: ['m1'],
+ },
+ {
+ id: 'positiveExamples',
+ type: 'text-list',
+ label: 'Positive examples',
+ required: false,
+ defaultValue: [],
+ maxItems: 3,
+ },
+ ],
+ });
+ const existing = makeGuardrail({
+ validatorType: 'llm_as_judge',
+ validatorParameters: [
+ { $parameterType: 'text', id: 'guardrailText', value: 'rule' },
+ { $parameterType: 'enum', id: 'model', value: 'm1' },
+ { $parameterType: 'text-list', id: 'positiveExamples', value: ['', 'kept', ' '] },
+ ],
+ });
+
+ render(
+
+ );
+
+ fireEvent.click(screen.getByRole('button', { name: /^save$/i }));
+ expect(onSave).toHaveBeenCalledTimes(1);
+ const saved = onSave.mock.calls[0][0] as GuardrailBuilderValue;
+ const examplesParam = saved.validatorParameters.find((p) => p.id === 'positiveExamples');
+ expect(examplesParam).toEqual({
+ $parameterType: 'text-list',
+ id: 'positiveExamples',
+ value: ['kept'],
+ });
+ });
+ });
+
+ describe('name validation', () => {
+ it('blocks Save and shows a message when the name is empty', () => {
+ const onSave = vi.fn();
+ render(
+
+ );
+
+ fireEvent.click(screen.getByRole('button', { name: /^save$/i }));
+ expect(onSave).not.toHaveBeenCalled();
+ expect(screen.getByText('Guardrail name is required')).toBeInTheDocument();
+ });
+
+ it('blocks Save on a case-insensitive duplicate name', () => {
+ const onSave = vi.fn();
+ render(
+
+ );
+
+ fireEvent.click(screen.getByRole('button', { name: /^save$/i }));
+ expect(onSave).not.toHaveBeenCalled();
+ expect(screen.getByText('A guardrail with this name already exists')).toBeInTheDocument();
+ });
+ });
+
+ describe('scope validation', () => {
+ it('blocks Save when no scope is selected (Agent context only)', () => {
+ const onSave = vi.fn();
+ render(
+
+ );
+
+ fireEvent.click(screen.getByRole('button', { name: /^save$/i }));
+ expect(onSave).not.toHaveBeenCalled();
+ expect(screen.getByText('At least one scope is required')).toBeInTheDocument();
+ });
+
+ it('ignores selector validation when opened from a tool node', () => {
+ const onSave = vi.fn();
+ render(
+
+ );
+
+ fireEvent.click(screen.getByRole('button', { name: /^save$/i }));
+ expect(onSave).toHaveBeenCalled();
+ });
+ });
+
+ describe('host errors override', () => {
+ it('displays host errors immediately and gates Save', () => {
+ const onSave = vi.fn();
+ render(
+
+ );
+
+ expect(screen.getByText('Backend rejected this name')).toBeInTheDocument();
+ fireEvent.click(screen.getByRole('button', { name: /^save$/i }));
+ expect(onSave).not.toHaveBeenCalled();
+ });
+
+ it('lets the host message win over the internal one per field', () => {
+ render(
+
+ );
+
+ fireEvent.click(screen.getByRole('button', { name: /^save$/i }));
+ expect(screen.getByText('Host-specific message')).toBeInTheDocument();
+ expect(screen.queryByText('Guardrail name is required')).not.toBeInTheDocument();
+ });
+ });
+
+ describe('BYOG guardrails', () => {
+ const makeByoDef = (overrides?: Partial): GuardrailDefinition =>
+ makeDef({
+ validator: 'vendor_validator',
+ displayName: 'Vendor PII',
+ byoValidatorName: 'my-pii',
+ ...overrides,
+ });
+
+ it('persists the byo sentinel and validator name on save', () => {
+ const onSave = vi.fn();
+ render(
+
+ );
+
+ fireEvent.click(screen.getByRole('button', { name: /^save$/i }));
+
+ expect(onSave).toHaveBeenCalledTimes(1);
+ const saved = onSave.mock.calls[0][0] as GuardrailBuilderValue;
+ expect(saved.validatorType).toBe('byo');
+ expect(saved.byoValidatorName).toBe('my-pii');
+ });
+
+ it('does not stamp byo fields for a UiPath-managed definition', () => {
+ const onSave = vi.fn();
+ render(
+
+ );
+
+ fireEvent.click(screen.getByRole('button', { name: /^save$/i }));
+
+ const saved = onSave.mock.calls[0][0] as GuardrailBuilderValue;
+ expect(saved.validatorType).toBe('pii_detection');
+ expect(saved.byoValidatorName).toBeUndefined();
+ });
+
+ it('drops optional params left empty on save but keeps filled ones', () => {
+ const onSave = vi.fn();
+ const definition = makeByoDef({
+ parameters: [
+ {
+ id: 'optionalText',
+ type: 'text',
+ label: 'Optional text',
+ required: false,
+ defaultValue: '',
+ },
+ {
+ id: 'strictMode',
+ type: 'boolean',
+ label: 'Strict mode',
+ required: false,
+ defaultValue: true,
+ },
+ ],
+ });
+ render(
+
+ );
+
+ fireEvent.click(screen.getByRole('button', { name: /^save$/i }));
+
+ const saved = onSave.mock.calls[0][0] as GuardrailBuilderValue;
+ expect(saved.validatorParameters).toEqual([
+ { $parameterType: 'boolean', id: 'strictMode', value: true },
+ ]);
+ });
+
+ it('renders a toggle for boolean params and persists the flipped value', () => {
+ const onSave = vi.fn();
+ const definition = makeByoDef({
+ parameters: [
+ {
+ id: 'strictMode',
+ type: 'boolean',
+ label: 'Strict mode',
+ required: false,
+ defaultValue: false,
+ },
+ ],
+ });
+ render(
+
+ );
+
+ fireEvent.click(screen.getByRole('switch', { name: /strict mode/i }));
+ fireEvent.click(screen.getByRole('button', { name: /^save$/i }));
+
+ const saved = onSave.mock.calls[0][0] as GuardrailBuilderValue;
+ expect(saved.validatorParameters).toEqual([
+ { $parameterType: 'boolean', id: 'strictMode', value: true },
+ ]);
+ });
+ });
+
+ describe('required map-enum validation', () => {
+ it('does not call onSave when a required map-enum param is empty', () => {
+ const onSave = vi.fn();
+ const definition = makeDef({
+ parameters: [
+ {
+ id: 'thresholds',
+ type: 'map-enum',
+ label: 'Thresholds',
+ required: true,
+ defaultValue: {},
+ keySource: 'entities',
+ min: 0,
+ max: 1,
+ step: 0.1,
+ },
+ ],
+ });
+ render(
+
+ );
+
+ fireEvent.click(screen.getByRole('button', { name: /^save$/i }));
+ expect(onSave).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('layout options', () => {
+ it('renders the evals toggle in the footer when evalsTogglePlacement is footer', () => {
+ render(
+
+ );
+
+ const toggle = screen.getByRole('switch', { name: /enable guardrail for evaluations/i });
+ const footer = toggle.closest('div.border-t');
+ expect(footer).not.toBeNull();
+ });
+
+ it('renders a custom title node when provided', () => {
+ render(
+ Fancy title}
+ onSave={vi.fn()}
+ onCancel={vi.fn()}
+ />
+ );
+
+ expect(screen.getByTestId('custom-title')).toBeInTheDocument();
+ });
+
+ it('computes the Add title from the definition display name', () => {
+ render(
+
+ );
+ expect(screen.getByText('Add PII Detection guardrail')).toBeInTheDocument();
+ });
+ });
+
+ describe('form interactions', () => {
+ it('saves edited name, description, evals flag, and scope selection', () => {
+ const onSave = vi.fn();
+ render(
+
+ );
+
+ fireEvent.change(screen.getByLabelText(/guardrail name/i), { target: { value: 'Renamed' } });
+ fireEvent.change(screen.getByLabelText(/guardrail description/i), {
+ target: { value: 'Why it exists' },
+ });
+ fireEvent.click(screen.getByRole('switch', { name: /enable guardrail for evaluations/i }));
+ fireEvent.click(screen.getByText('LLM calls'));
+ fireEvent.click(screen.getByRole('button', { name: /^save$/i }));
+
+ expect(onSave).toHaveBeenCalledTimes(1);
+ const saved = onSave.mock.calls[0][0] as GuardrailBuilderValue;
+ expect(saved.name).toBe('Renamed');
+ expect(saved.description).toBe('Why it exists');
+ expect(saved.enabledForEvals).toBe(false);
+ expect(saved.selector.scopes).toEqual(['Agent', 'Llm']);
+ expect(saved.id).toMatch(/^guardrail-/);
+ });
+
+ it('calls onSaveAsNew with the current value when valid', () => {
+ const onSaveAsNew = vi.fn();
+ render(
+
+ );
+
+ fireEvent.click(screen.getByRole('button', { name: /save as new/i }));
+ expect(onSaveAsNew).toHaveBeenCalledTimes(1);
+ expect((onSaveAsNew.mock.calls[0][0] as GuardrailBuilderValue).name).toBe('My PII guardrail');
+ });
+
+ it('blocks onSaveAsNew and surfaces errors when invalid', () => {
+ const onSaveAsNew = vi.fn();
+ render(
+
+ );
+
+ fireEvent.click(screen.getByRole('button', { name: /save as new/i }));
+ expect(onSaveAsNew).not.toHaveBeenCalled();
+ expect(screen.getByText('Guardrail name is required')).toBeInTheDocument();
+ });
+
+ it('merges host parameter errors into the validator form', () => {
+ const definition = makeDef({
+ parameters: [
+ { id: 'prompt', type: 'text', label: 'Prompt', required: false, defaultValue: 'ok' },
+ ],
+ });
+ render(
+
+ );
+
+ expect(screen.getByText('Backend rejected this prompt')).toBeInTheDocument();
+ });
+
+ it('renders the mixed-scopes banner from otherAppliedScopes', () => {
+ render(
+
+ );
+
+ expect(screen.getByText('This guardrail is also applied to:')).toBeInTheDocument();
+ expect(screen.getByText('Other tool')).toBeInTheDocument();
+ });
+
+ it('passes the escalate slots through to the action section', () => {
+ render(
+
}
+ renderAppPicker={() =>
}
+ escalateHelp={
}
+ onSave={vi.fn()}
+ onCancel={vi.fn()}
+ />
+ );
+
+ expect(screen.getByTestId('host-directory-search')).toBeInTheDocument();
+ expect(screen.getByTestId('host-app-picker')).toBeInTheDocument();
+ expect(screen.getByTestId('host-help')).toBeInTheDocument();
+ });
+
+ it('renders in modal mode with a dialog', () => {
+ render(
+
+ );
+ expect(screen.getByRole('dialog')).toBeInTheDocument();
+ expect(screen.getByText('Add PII Detection guardrail')).toBeInTheDocument();
+ });
+ });
+
+ describe('accessibility', () => {
+ it('has no accessibility violations on a full definition', async () => {
+ const definition = makeDef({
+ usageNote: 'Runs on every LLM call.',
+ parameters: [
+ {
+ id: 'prompt',
+ type: 'text',
+ label: 'Prompt',
+ required: true,
+ defaultValue: 'check tone',
+ },
+ {
+ id: 'strict',
+ type: 'boolean',
+ label: 'Strict mode',
+ required: false,
+ defaultValue: true,
+ },
+ ],
+ });
+ const { container } = render(
+
+ );
+
+ const results = await axe(container);
+ expect(results).toHaveNoViolations();
+ });
+ });
+});
+
+describe('renderParameter passthrough', () => {
+ it('forwards renderParameter to the internal validator form', () => {
+ const def = makeDef({
+ parameters: [
+ {
+ id: 'model',
+ type: 'enum',
+ label: 'Judge model',
+ required: true,
+ defaultValue: '',
+ options: ['gpt-4o'],
+ },
+ ],
+ });
+
+ render(
+ {}}
+ onCancel={() => {}}
+ renderParameter={(ctx) =>
+ ctx.definition.id === 'model' ?
: undefined
+ }
+ />
+ );
+
+ expect(screen.getByTestId('host-picker')).toBeInTheDocument();
+ // The default enum select for the overridden parameter must not render.
+ expect(screen.queryByRole('combobox', { name: /Judge model/ })).not.toBeInTheDocument();
+ });
+});
+
+describe('saveDisabled passthrough', () => {
+ it('disables Save while the host gate is on, on top of internal validation', () => {
+ const { rerender } = render(
+ {}}
+ onCancel={() => {}}
+ saveDisabled
+ />
+ );
+
+ expect(screen.getByRole('button', { name: 'Save' })).toBeDisabled();
+
+ rerender(
+
+ {}}
+ onCancel={() => {}}
+ saveDisabled={false}
+ />
+
+ );
+ expect(screen.getByRole('button', { name: 'Save' })).toBeEnabled();
+ });
+});
diff --git a/packages/apollo-react/src/canvas/components/Guardrails/guardrail-builder.tsx b/packages/apollo-react/src/canvas/components/Guardrails/guardrail-builder.tsx
new file mode 100644
index 0000000000..8ddb3b2618
--- /dev/null
+++ b/packages/apollo-react/src/canvas/components/Guardrails/guardrail-builder.tsx
@@ -0,0 +1,436 @@
+import {
+ Alert,
+ AlertDescription,
+ cn,
+ FormField,
+ FormFieldLabel,
+ InfoTooltip,
+ Input,
+ Label,
+ Separator,
+ Switch,
+ Textarea,
+} from '@uipath/apollo-wind';
+import { Info } from 'lucide-react';
+import { type ReactNode, useCallback, useMemo, useState } from 'react';
+import {
+ GUARDRAIL_BYO_VALIDATOR_TYPE,
+ type GuardrailAppPickerContext,
+ type GuardrailBuilderErrors,
+ type GuardrailBuilderValue,
+ type GuardrailDefinition,
+ type GuardrailRecipientSearchContext,
+ type GuardrailScope,
+ type GuardrailSelector,
+ type GuardrailStaticRecipientContext,
+} from './builder-types';
+import {
+ type GuardrailBuilderFormData,
+ getGuardrailActionErrorFields,
+ getGuardrailSelectorErrorFields,
+ initGuardrailBuilderFormData,
+} from './builder-utils';
+import { GuardrailActionSection } from './components/guardrail-action-section';
+import { GuardrailScopeSelector } from './components/guardrail-scope-selector';
+import { GuardrailStatusBanner } from './components/guardrail-status-banner';
+import { MixedScopesBanner } from './components/mixed-scopes-banner';
+import { GuardrailFormLayout } from './guardrail-form-layout';
+import { GuardrailValidatorForm } from './guardrail-validator-form';
+import {
+ formatGuardrailFormMessage,
+ type GuardrailBuilderLabels,
+ useGuardrailBuilderLabels,
+} from './i18n';
+import type { GuardrailValidatorFormProps } from './types';
+import {
+ dropEmptyOptionalParameters,
+ getRequiredEmptyParameterIds,
+ syncMapEnumParameters,
+} from './utils';
+
+export interface GuardrailBuilderProps {
+ /**
+ * Drives the Dialog in modal mode only. Form state initializes from
+ * `definition`/`guardrail`/`defaultName` at mount — remount (with a new `key`) to reset.
+ */
+ open: boolean;
+ definition: GuardrailDefinition;
+ /** Where the screen was opened from. The scope selector renders only when 'Agent'. */
+ scope: GuardrailScope;
+ /** Edit mode: initial value, copied verbatim into form state. */
+ guardrail?: GuardrailBuilderValue;
+ /** Default name for new guardrails (host-generated unique name). */
+ defaultName?: string;
+ /** Other guardrail names, for case-insensitive duplicate validation. */
+ existingNames?: string[];
+ /** Current tool node name — pre-selected as matchNames when creating at Tool scope. */
+ toolName?: string;
+ /** Tool names available for targeting (shown when Tool scope is selected). */
+ availableToolNames?: string[];
+ onSave: (guardrail: GuardrailBuilderValue) => void;
+ onCancel: () => void;
+ /** Presence renders the "Save as new" secondary action (same validation gate as Save). */
+ onSaveAsNew?: (guardrail: GuardrailBuilderValue) => void;
+ /** Pre-localized labels; renders the mixed-scopes banner when non-null. */
+ otherAppliedScopes?: { scopes: string[]; tools: string[] } | null;
+ inline?: boolean;
+ hideHeader?: boolean;
+ dialogMaxWidth?: number;
+ /** Overrides the computed "Add/Edit {{name}} guardrail" title. */
+ title?: ReactNode;
+ /** Where the enable-for-evaluations switch renders. Default 'form'. */
+ evalsTogglePlacement?: 'form' | 'footer';
+ renderRecipientSearch?: (ctx: GuardrailRecipientSearchContext) => ReactNode;
+ /**
+ * Replace the editor for static/asset recipients (types 3/4/5/6). Return `undefined` to
+ * fall through to the built-in plain input.
+ */
+ renderStaticRecipient?: (ctx: GuardrailStaticRecipientContext) => ReactNode | undefined;
+ renderAppPicker?: (ctx: GuardrailAppPickerContext) => ReactNode;
+ /** Rendered under the escalation grid (e.g. a marketplace help line). */
+ escalateHelp?: ReactNode;
+ /**
+ * Host-supplied validation errors, merged over internal validation (host wins per field).
+ * Host errors display immediately and gate Save like internal ones.
+ */
+ errors?: Partial;
+ /**
+ * Forwarded to the internal `GuardrailValidatorForm`: replace the editor for individual
+ * validator parameters (e.g. mounting a product model picker for a judge-model parameter).
+ */
+ renderParameter?: GuardrailValidatorFormProps['renderParameter'];
+ /**
+ * Extra host-owned Save gate, OR'd with the builder's own (e.g. while an async resolution
+ * a slot started is still in flight).
+ */
+ saveDisabled?: boolean;
+ labels?: Partial;
+ className?: string;
+}
+
+/**
+ * The complete Add/Edit screen for an OOTB guardrail validator: status banners, usage note,
+ * type display (edit mode), name, description, validator parameters, scope selector, action
+ * (incl. escalation via host slots), evaluations toggle, mixed-scopes banner, and the
+ * Save/Cancel/Save-as-new footer.
+ *
+ * Owns its form state (initialized at mount — remount via `key` to reset) and its validation
+ * (gating Save); hosts can override any message via `labels` or any field via `errors`.
+ * Requires an ancestor `TooltipProvider`.
+ */
+export function GuardrailBuilder({
+ open,
+ definition,
+ scope,
+ guardrail,
+ defaultName,
+ existingNames,
+ toolName,
+ availableToolNames,
+ onSave,
+ onCancel,
+ onSaveAsNew,
+ otherAppliedScopes = null,
+ inline = false,
+ hideHeader = false,
+ dialogMaxWidth,
+ title,
+ evalsTogglePlacement = 'form',
+ renderRecipientSearch,
+ renderStaticRecipient,
+ renderAppPicker,
+ escalateHelp,
+ errors: hostErrors,
+ renderParameter,
+ saveDisabled: hostSaveDisabled = false,
+ labels: labelOverrides,
+ className,
+}: GuardrailBuilderProps) {
+ const labels = useGuardrailBuilderLabels(labelOverrides);
+
+ const [formData, setFormData] = useState(() => {
+ const data = initGuardrailBuilderFormData(definition, scope, guardrail, toolName);
+ if (!guardrail && defaultName) data.name = defaultName;
+ return data;
+ });
+ const [showErrors, setShowErrors] = useState(false);
+
+ const showScopeSelector = scope === 'Agent';
+
+ const updateField = useCallback(
+ (field: K, value: GuardrailBuilderFormData[K]) => {
+ setFormData((prev) => ({ ...prev, [field]: value }));
+ },
+ []
+ );
+
+ const handleSelectorChange = useCallback(
+ (selector: GuardrailSelector) => updateField('selector', selector),
+ [updateField]
+ );
+
+ const guardrailResult = useMemo(() => {
+ // Reconcile map-enum values with their source enum-list selection, then drop empty /
+ // whitespace-only text-list entries — otherwise reopening the guardrail surfaces phantom
+ // rows the author never filled in. Finally drop optional params left empty: runtimes
+ // require persisted values to be non-empty.
+ const trimmed = syncMapEnumParameters(formData.validatorParameters, definition.parameters).map(
+ (param) =>
+ param.$parameterType === 'text-list'
+ ? { ...param, value: param.value.filter((entry) => entry.trim().length > 0) }
+ : param
+ );
+ const validatorParameters = dropEmptyOptionalParameters(trimmed, definition.parameters);
+ const isByo = definition.byoValidatorName !== undefined;
+ return {
+ id: formData.id,
+ $guardrailType: 'builtInValidator',
+ name: formData.name,
+ description: formData.description || undefined,
+ selector: formData.selector,
+ action: formData.action,
+ enabledForEvals: formData.enabledForEvals,
+ validatorType: isByo ? GUARDRAIL_BYO_VALIDATOR_TYPE : definition.validator,
+ validatorParameters,
+ ...(isByo ? { byoValidatorName: definition.byoValidatorName } : {}),
+ };
+ }, [formData, definition]);
+
+ const internalErrors = useMemo(() => {
+ const e: GuardrailBuilderErrors = {};
+ if (!formData.name.trim()) {
+ e.name = labels.nameRequiredError;
+ } else if (existingNames?.some((n) => n.toLowerCase() === formData.name.trim().toLowerCase())) {
+ e.name = labels.nameDuplicateError;
+ }
+
+ if (showScopeSelector) {
+ for (const field of getGuardrailSelectorErrorFields(formData.selector)) {
+ e[field] = field === 'scopes' ? labels.scopesRequiredError : labels.toolsRequiredError;
+ }
+ }
+
+ const emptyParamIds = getRequiredEmptyParameterIds(
+ definition.parameters,
+ formData.validatorParameters
+ );
+ if (emptyParamIds.length > 0) {
+ e.parameters = Object.fromEntries(
+ emptyParamIds.map((id) => [id, labels.parameterRequiredError])
+ );
+ }
+
+ for (const field of getGuardrailActionErrorFields(formData.action)) {
+ e[field] = {
+ blockReason: labels.blockReasonRequiredError,
+ filterFields: labels.filterFieldsRequiredError,
+ recipient: labels.recipientRequiredError,
+ actionApp: labels.actionAppRequiredError,
+ }[field];
+ }
+ return e;
+ }, [formData, showScopeSelector, definition.parameters, existingNames, labels]);
+
+ // Host errors display immediately and win per field; internal errors display after a
+ // failed save attempt. Both gate Save.
+ const displayErrors = useMemo(() => {
+ const base: GuardrailBuilderErrors = showErrors ? { ...internalErrors } : {};
+ if (hostErrors) {
+ for (const [key, value] of Object.entries(hostErrors)) {
+ if (value !== undefined) {
+ (base as Record)[key] =
+ key === 'parameters'
+ ? { ...base.parameters, ...(value as Record) }
+ : value;
+ }
+ }
+ }
+ return base;
+ }, [showErrors, internalErrors, hostErrors]);
+
+ const hasHostErrors = Boolean(
+ hostErrors &&
+ Object.values(hostErrors).some(
+ (v) => v !== undefined && (typeof v !== 'object' || Object.keys(v).length > 0)
+ )
+ );
+ const isValid = Object.keys(internalErrors).length === 0 && !hasHostErrors;
+ const isDefinitionAvailable = definition.status === 'Available';
+
+ const handleSave = useCallback(() => {
+ if (!isDefinitionAvailable) return;
+ if (!isValid) {
+ setShowErrors(true);
+ return;
+ }
+ onSave(guardrailResult);
+ }, [guardrailResult, onSave, isValid, isDefinitionAvailable]);
+
+ const handleSaveAsNew = useCallback(() => {
+ if (!onSaveAsNew) return;
+ if (!isDefinitionAvailable) return;
+ if (!isValid) {
+ setShowErrors(true);
+ return;
+ }
+ onSaveAsNew(guardrailResult);
+ }, [guardrailResult, onSaveAsNew, isValid, isDefinitionAvailable]);
+
+ const isByoConfigurationDisabled =
+ definition.byoValidatorName !== undefined && definition.status === 'Disabled';
+
+ const computedTitle = guardrail
+ ? formatGuardrailFormMessage(labels.editTitle, { name: definition.displayName })
+ : formatGuardrailFormMessage(labels.addTitle, { name: definition.displayName });
+
+ const statusBanner = isByoConfigurationDisabled ? (
+
+ ) : definition.status === 'Unauthorised' ? (
+
+ ) : definition.status === 'FeatureDisabled' ? (
+
+ ) : null;
+
+ const usageNote = definition.usageNote ? (
+
+
+ {definition.usageNote}
+
+ ) : null;
+
+ const evalsToggle = (
+
+ updateField('enabledForEvals', checked)}
+ />
+
+ {labels.evalsLabel}
+
+
+
+ );
+
+ const validatorFormErrors = displayErrors.parameters;
+
+ const formBody = (
+
+ {statusBanner}
+ {usageNote}
+
+ {/* Guardrail type (read-only in edit mode — matches the type selector in the create flow) */}
+ {guardrail && (
+
+ {labels.typeLabel}
+
+
+ )}
+
+ {/* Name */}
+
+
+ {labels.nameLabel}
+
+ updateField('name', e.target.value)}
+ placeholder={labels.namePlaceholder}
+ error={displayErrors.name}
+ />
+
+
+ {/* Description */}
+
+ {labels.descriptionLabel}
+
+
+ {/* Validator parameters */}
+ {definition.parameters.length > 0 && (
+
+ updateField('validatorParameters', params)}
+ errors={validatorFormErrors}
+ renderParameter={renderParameter}
+ />
+
+ )}
+
+ {/* Scope selector (when opened from an agent node) */}
+ {showScopeSelector && (
+
+ )}
+
+
+
+ {/* Action section */}
+
+ updateField('action', action)}
+ errors={{
+ blockReason: displayErrors.blockReason,
+ filterFields: displayErrors.filterFields,
+ recipient: displayErrors.recipient,
+ actionApp: displayErrors.actionApp,
+ }}
+ labels={labels}
+ renderRecipientSearch={renderRecipientSearch}
+ renderStaticRecipient={renderStaticRecipient}
+ renderAppPicker={renderAppPicker}
+ escalateHelp={escalateHelp}
+ />
+
+
+ {/* Enable for evaluations */}
+ {evalsTogglePlacement === 'form' && evalsToggle}
+
+ {/* Mixed scopes info banner */}
+
+
+ );
+
+ const secondaryAction = onSaveAsNew
+ ? {
+ label: labels.saveAsNew,
+ onClick: handleSaveAsNew,
+ disabled: !isDefinitionAvailable,
+ }
+ : undefined;
+
+ return (
+
+ {formBody}
+
+ );
+}
+GuardrailBuilder.displayName = 'GuardrailBuilder';
diff --git a/packages/apollo-react/src/canvas/components/Guardrails/guardrail-form-layout.stories.tsx b/packages/apollo-react/src/canvas/components/Guardrails/guardrail-form-layout.stories.tsx
new file mode 100644
index 0000000000..495201a7ec
--- /dev/null
+++ b/packages/apollo-react/src/canvas/components/Guardrails/guardrail-form-layout.stories.tsx
@@ -0,0 +1,90 @@
+import type { Meta, StoryObj } from '@storybook/react-vite';
+import { Label, Switch } from '@uipath/apollo-wind';
+import { GuardrailFormLayout } from './guardrail-form-layout';
+
+const meta = {
+ title: 'Components/UiPath/Guardrail Form Layout',
+ component: GuardrailFormLayout,
+ parameters: {
+ layout: 'padded',
+ docs: {
+ description: {
+ component: `
+Layout shell for guardrail builder forms with three modes: inline + hideHeader (scrollable
+region with a footer), inline (adds a back-button header), and modal (a Dialog). The footer
+carries Cancel, an optional secondary action, and Save; footerStart renders a left-aligned
+region, e.g. an evaluations toggle.
+ `,
+ },
+ },
+ },
+ tags: ['autodocs'],
+ decorators: [
+ (Story) => (
+
+
+
+ ),
+ ],
+} satisfies Meta;
+
+export default meta;
+type Story = StoryObj;
+
+const body = (
+
+
Form body content goes here.
+
The region scrolls when it overflows.
+
+);
+
+export const InlineWithHeader: Story = {
+ args: {
+ open: true,
+ inline: true,
+ title: 'Edit PII detection guardrail',
+ children: body,
+ onSave: () => {},
+ onCancel: () => {},
+ },
+};
+
+export const InlineHiddenHeader: Story = {
+ args: { ...InlineWithHeader.args, hideHeader: true },
+};
+
+export const Modal: Story = {
+ decorators: [(Story) => ],
+ args: {
+ open: true,
+ title: 'Edit PII detection guardrail',
+ children: body,
+ onSave: () => {},
+ onCancel: () => {},
+ },
+};
+
+export const WithSecondaryAction: Story = {
+ args: {
+ ...InlineHiddenHeader.args,
+ secondaryAction: { label: 'Save as new', onClick: () => {} },
+ },
+};
+
+export const WithFooterStart: Story = {
+ args: {
+ ...InlineHiddenHeader.args,
+ footerStart: (
+
+
+
+ Enable guardrail for evaluations
+
+
+ ),
+ },
+};
+
+export const SaveDisabled: Story = {
+ args: { ...InlineHiddenHeader.args, saveDisabled: true },
+};
diff --git a/packages/apollo-react/src/canvas/components/Guardrails/guardrail-form-layout.test.tsx b/packages/apollo-react/src/canvas/components/Guardrails/guardrail-form-layout.test.tsx
new file mode 100644
index 0000000000..5ea1cb638d
--- /dev/null
+++ b/packages/apollo-react/src/canvas/components/Guardrails/guardrail-form-layout.test.tsx
@@ -0,0 +1,139 @@
+import { fireEvent, render, screen } from '@testing-library/react';
+import { axe } from 'jest-axe';
+import { describe, expect, it, vi } from 'vitest';
+import { GuardrailFormLayout } from './guardrail-form-layout';
+
+describe('GuardrailFormLayout', () => {
+ const baseProps = {
+ open: true,
+ title: 'Add guardrail',
+ onSave: vi.fn(),
+ onCancel: vi.fn(),
+ };
+
+ it('inline + hideHeader renders body and footer without a header', () => {
+ render(
+
+
+
+ );
+
+ expect(screen.getByTestId('body')).toBeInTheDocument();
+ expect(screen.queryByText('Add guardrail')).not.toBeInTheDocument();
+ expect(screen.getByRole('button', { name: 'Save' })).toBeInTheDocument();
+ expect(screen.getByRole('button', { name: 'Cancel' })).toBeInTheDocument();
+ });
+
+ it('inline renders a back-button header wired to onCancel', () => {
+ const onCancel = vi.fn();
+ render(
+
+
+
+ );
+
+ expect(screen.getByText('Add guardrail')).toBeInTheDocument();
+ const buttons = screen.getAllByRole('button');
+ fireEvent.click(buttons[0]);
+ expect(onCancel).toHaveBeenCalled();
+ });
+
+ it('modal renders a dialog with title and footer buttons', () => {
+ render(
+
+
+
+ );
+
+ expect(screen.getByRole('dialog')).toBeInTheDocument();
+ expect(screen.getByText('Add guardrail')).toBeInTheDocument();
+ expect(screen.getByTestId('body')).toBeInTheDocument();
+ });
+
+ it('modal calls onCancel when the dialog is dismissed', () => {
+ const onCancel = vi.fn();
+ render(
+
+
+
+ );
+
+ fireEvent.keyDown(screen.getByRole('dialog'), { key: 'Escape' });
+ expect(onCancel).toHaveBeenCalled();
+ });
+
+ it('renders the secondary action and honors its disabled state', () => {
+ const onSecondary = vi.fn();
+ render(
+
+
+
+ );
+
+ const secondary = screen.getByRole('button', { name: 'Save as new' });
+ expect(secondary).toBeDisabled();
+ });
+
+ it('disables Save when saveDisabled is set', () => {
+ render(
+
+
+
+ );
+
+ expect(screen.getByRole('button', { name: 'Save' })).toBeDisabled();
+ });
+
+ it('renders footerStart content in the footer (inline and modal)', () => {
+ const { unmount } = render(
+ }
+ >
+
+
+ );
+ expect(screen.getByTestId('footer-start')).toBeInTheDocument();
+ unmount();
+
+ render(
+ }>
+
+
+ );
+ expect(screen.getByTestId('footer-start-modal')).toBeInTheDocument();
+ });
+
+ it('uses label overrides for the buttons', () => {
+ render(
+
+
+
+ );
+
+ expect(screen.getByRole('button', { name: 'Abbrechen' })).toBeInTheDocument();
+ expect(screen.getByRole('button', { name: 'Speichern' })).toBeInTheDocument();
+ });
+
+ it('has no accessibility violations (inline)', async () => {
+ const { container } = render(
+
+ Body content
+
+ );
+ const results = await axe(container);
+ expect(results).toHaveNoViolations();
+ });
+});
diff --git a/packages/apollo-react/src/canvas/components/Guardrails/guardrail-form-layout.tsx b/packages/apollo-react/src/canvas/components/Guardrails/guardrail-form-layout.tsx
new file mode 100644
index 0000000000..97a9fa3032
--- /dev/null
+++ b/packages/apollo-react/src/canvas/components/Guardrails/guardrail-form-layout.tsx
@@ -0,0 +1,129 @@
+import {
+ Button,
+ Dialog,
+ DialogContent,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+} from '@uipath/apollo-wind';
+import { ArrowLeft } from 'lucide-react';
+import type { ReactNode } from 'react';
+import { useGuardrailBuilderLabels } from './i18n';
+
+export interface GuardrailFormLayoutProps {
+ /** Drives the Dialog in modal mode; inline modes render regardless. */
+ open: boolean;
+ title: ReactNode;
+ children: ReactNode;
+ onSave: () => void;
+ onCancel: () => void;
+ /** Render as an inline panel instead of a modal dialog. */
+ inline?: boolean;
+ /** When inline, hide the back-button header (used when embedded under a host-owned header). */
+ hideHeader?: boolean;
+ /** Max width of the modal dialog in pixels (default: 700) */
+ dialogMaxWidth?: number;
+ /** Optional secondary save action (e.g. "Save as new" for mixed-scope guardrails) */
+ secondaryAction?: { label: ReactNode; onClick: () => void; disabled?: boolean };
+ /** Disable the primary save action when the current configuration cannot be persisted. */
+ saveDisabled?: boolean;
+ /** Left-aligned footer region (e.g. an evaluations toggle). */
+ footerStart?: ReactNode;
+ /** Button labels; English defaults. Hosts with i18n pass translated strings. */
+ labels?: { cancel?: string; save?: string };
+}
+
+/**
+ * Shared layout wrapper for guardrail builder forms, with three rendering modes:
+ * - inline + hideHeader: plain scrollable region with a footer
+ * - inline: full-height flex column with a back-button header and footer
+ * - modal: Dialog with header and footer
+ */
+export function GuardrailFormLayout({
+ open,
+ title,
+ children,
+ onSave,
+ onCancel,
+ inline = false,
+ hideHeader = false,
+ dialogMaxWidth = 700,
+ secondaryAction,
+ saveDisabled = false,
+ footerStart,
+ labels,
+}: GuardrailFormLayoutProps) {
+ const builderLabels = useGuardrailBuilderLabels();
+ const cancelLabel = labels?.cancel ?? builderLabels.cancel;
+ const saveLabel = labels?.save ?? builderLabels.save;
+
+ const footerButtons = (size?: 'sm') => (
+ <>
+
+ {cancelLabel}
+
+ {secondaryAction && (
+
+ {secondaryAction.label}
+
+ )}
+
+ {saveLabel}
+
+ >
+ );
+
+ if (inline) {
+ return (
+
+ {!hideHeader && (
+
+ )}
+
+ {children}
+
+
+ {footerStart &&
{footerStart}
}
+
{footerButtons('sm')}
+
+
+ );
+ }
+
+ return (
+ !isOpen && onCancel()}>
+
+
+ {title}
+
+ {children}
+
+ {footerStart && {footerStart}
}
+ {footerButtons()}
+
+
+
+ );
+}
diff --git a/packages/apollo-react/src/canvas/components/Guardrails/guardrail-list-utils.test.ts b/packages/apollo-react/src/canvas/components/Guardrails/guardrail-list-utils.test.ts
new file mode 100644
index 0000000000..7bf6478e00
--- /dev/null
+++ b/packages/apollo-react/src/canvas/components/Guardrails/guardrail-list-utils.test.ts
@@ -0,0 +1,166 @@
+import { describe, expect, it } from 'vitest';
+import {
+ BYO_DEFINITION,
+ BYO_GUARDRAIL,
+ CUSTOM_GUARDRAIL,
+ DEFINITIONS,
+ PII_DEFINITION,
+ PII_GUARDRAIL,
+} from './__fixtures__/guardrail-list.fixtures';
+import {
+ getGuardrailListChips,
+ getGuardrailListItemId,
+ matchesGuardrailListDefinition,
+ resolveGuardrailListItemState,
+} from './guardrail-list-utils';
+import { GUARDRAIL_LIST_EN_LABELS } from './i18n';
+
+const labels = GUARDRAIL_LIST_EN_LABELS;
+
+describe('getGuardrailListItemId', () => {
+ it('prefers the product id', () => {
+ expect(getGuardrailListItemId(PII_GUARDRAIL)).toBe('g1');
+ });
+
+ it('falls back to the name when there is no id', () => {
+ expect(getGuardrailListItemId({ name: 'PII detection 1' })).toBe('PII detection 1');
+ });
+});
+
+describe('matchesGuardrailListDefinition', () => {
+ it('matches a UiPath validator on the validator id', () => {
+ expect(matchesGuardrailListDefinition(PII_DEFINITION, PII_GUARDRAIL)).toBe(true);
+ });
+
+ it('matches a BYO guardrail on the validator name alone', () => {
+ // Unique per tenant, so an admin rebinding the configuration to another connection still
+ // resolves. The definition's `validator` deliberately does not have to agree.
+ expect(
+ matchesGuardrailListDefinition(
+ { ...BYO_DEFINITION, validator: 'something_else' },
+ BYO_GUARDRAIL
+ )
+ ).toBe(true);
+ });
+
+ it('never matches a UiPath validator against a BYO definition with the same validator id', () => {
+ expect(
+ matchesGuardrailListDefinition(
+ { ...BYO_DEFINITION, validator: 'pii_detection' },
+ PII_GUARDRAIL
+ )
+ ).toBe(false);
+ });
+
+ it('never matches a custom guardrail, which has no validator', () => {
+ expect(matchesGuardrailListDefinition(PII_DEFINITION, CUSTOM_GUARDRAIL)).toBe(false);
+ });
+});
+
+describe('resolveGuardrailListItemState', () => {
+ it('resolves a UiPath validator to its definition and status', () => {
+ expect(resolveGuardrailListItemState(PII_GUARDRAIL, DEFINITIONS)).toEqual({
+ definition: PII_DEFINITION,
+ status: 'Available',
+ isByo: false,
+ provider: undefined,
+ byoDisabled: false,
+ byoUnavailable: false,
+ });
+ });
+
+ it('exposes the BYO connector as the provider', () => {
+ expect(resolveGuardrailListItemState(BYO_GUARDRAIL, DEFINITIONS)).toMatchObject({
+ isByo: true,
+ provider: 'Noma Security',
+ byoDisabled: false,
+ byoUnavailable: false,
+ });
+ });
+
+ it('reports a disabled BYO configuration', () => {
+ const state = resolveGuardrailListItemState(BYO_GUARDRAIL, [
+ PII_DEFINITION,
+ { ...BYO_DEFINITION, status: 'Disabled' },
+ ]);
+
+ expect(state).toMatchObject({ status: 'Disabled', byoDisabled: true, byoUnavailable: false });
+ });
+
+ it('reports a BYO configuration that no longer resolves', () => {
+ const state = resolveGuardrailListItemState(BYO_GUARDRAIL, [PII_DEFINITION]);
+
+ expect(state).toMatchObject({
+ definition: undefined,
+ status: 'Unavailable',
+ byoUnavailable: true,
+ });
+ });
+
+ it('stays quiet about a BYO row while the definitions are still in flight', () => {
+ // Without the empty-array guard every BYO row would claim its configuration is gone for
+ // as long as the catalog takes to load. Both products already guard this.
+ const state = resolveGuardrailListItemState(BYO_GUARDRAIL, []);
+
+ expect(state).toMatchObject({ status: undefined, byoUnavailable: false, byoDisabled: false });
+ });
+
+ it('treats a missing definitions array as still in flight', () => {
+ expect(resolveGuardrailListItemState(BYO_GUARDRAIL)).toMatchObject({ byoUnavailable: false });
+ });
+
+ it('resolves a custom guardrail to nothing without claiming it is broken', () => {
+ expect(resolveGuardrailListItemState(CUSTOM_GUARDRAIL, DEFINITIONS)).toEqual({
+ definition: undefined,
+ status: undefined,
+ isByo: false,
+ provider: undefined,
+ byoDisabled: false,
+ byoUnavailable: false,
+ });
+ });
+
+ it('does not confuse a disabled UiPath validator for a disabled BYO configuration', () => {
+ const state = resolveGuardrailListItemState(PII_GUARDRAIL, [
+ { ...PII_DEFINITION, status: 'Disabled' },
+ ]);
+
+ expect(state).toMatchObject({ status: 'Disabled', byoDisabled: false });
+ });
+});
+
+describe('getGuardrailListChips', () => {
+ it('chips nothing for a locally administered, available row', () => {
+ expect(getGuardrailListChips({ status: 'Available', administration: 'local' }, labels)).toEqual(
+ []
+ );
+ });
+
+ it('chips nothing when nothing resolved', () => {
+ expect(getGuardrailListChips({}, labels)).toEqual([]);
+ });
+
+ it.each([
+ ['FeatureDisabled', 'warning', labels.statusFeatureDisabled],
+ ['Unauthorised', 'warning', labels.statusUnauthorized],
+ ['Disabled', 'error', labels.statusDisabled],
+ ['Unavailable', 'error', labels.statusUnavailable],
+ ] as const)('chips %s as a %s', (status, tone, label) => {
+ expect(getGuardrailListChips({ status }, labels)).toEqual([{ id: 'status', tone, label }]);
+ });
+
+ it('chips governance administration', () => {
+ expect(getGuardrailListChips({ administration: 'governance' }, labels)).toEqual([
+ { id: 'administration', tone: 'neutral', label: 'Governance managed' },
+ ]);
+ });
+
+ it('chips the status before the administration', () => {
+ const chips = getGuardrailListChips(
+ { status: 'Disabled', administration: 'governance' },
+ labels
+ );
+
+ expect(chips.map((chip) => chip.id)).toEqual(['status', 'administration']);
+ });
+});
diff --git a/packages/apollo-react/src/canvas/components/Guardrails/guardrail-list-utils.ts b/packages/apollo-react/src/canvas/components/Guardrails/guardrail-list-utils.ts
new file mode 100644
index 0000000000..bcc69c8a25
--- /dev/null
+++ b/packages/apollo-react/src/canvas/components/Guardrails/guardrail-list-utils.ts
@@ -0,0 +1,109 @@
+import type { GuardrailListLabels } from './i18n';
+import type {
+ GuardrailListAdministration,
+ GuardrailListChip,
+ GuardrailListDefinition,
+ GuardrailListItem,
+ GuardrailListItemState,
+} from './list-types';
+
+/**
+ * Pure helpers behind `GuardrailList`. Exported because hosts need the same answers outside a
+ * row (a remove dialog naming the provider, a section deciding whether to render at all), and
+ * because this is where both products' duplicated logic converges.
+ */
+
+/**
+ * A row's identity: the product's own id where it has one, else the name.
+ *
+ * Agents keys rows by `name` (unique per agent by construction) and Flow by `id`, so one
+ * default covers both. Override with `getItemId` if a product grows a third answer.
+ */
+export function getGuardrailListItemId(item: GuardrailListItem): string {
+ return item.id ?? item.name;
+}
+
+/**
+ * Whether a definition is the one a row was configured from.
+ *
+ * Byte-for-byte the rule both products already ship (`matchesGuardrailDefinition`): a BYO
+ * guardrail matches on the validator name alone, because those names are unique per tenant
+ * and an admin rebinding a configuration to another connection must still resolve.
+ */
+export function matchesGuardrailListDefinition(
+ definition: GuardrailListDefinition,
+ item: GuardrailListItem
+): boolean {
+ return item.byoValidatorName === undefined
+ ? definition.byoValidatorName === undefined && definition.validator === item.validatorType
+ : definition.byoValidatorName === item.byoValidatorName;
+}
+
+/**
+ * Resolve one row against the (host-filtered) definitions: which definition it came from, its
+ * status, and whether its BYO configuration is disabled or gone.
+ *
+ * The two BYO notices keep both products' `definitions.length > 0` guard. Without it every BYO
+ * row claims its configuration is gone while the catalog is still in flight.
+ */
+export function resolveGuardrailListItemState(
+ item: GuardrailListItem,
+ definitions: readonly GuardrailListDefinition[] = []
+): GuardrailListItemState {
+ // Only built-in-validator guardrails carry a BYO validator name in either product, so the
+ // field alone is the test; no `$guardrailType` branch is needed.
+ const isByo = item.byoValidatorName !== undefined;
+ const definition = definitions.find((candidate) =>
+ matchesGuardrailListDefinition(candidate, item)
+ );
+ const byoUnavailable = isByo && definitions.length > 0 && definition === undefined;
+ const byoDisabled = isByo && definition?.status === 'Disabled';
+
+ return {
+ definition,
+ status: byoUnavailable ? 'Unavailable' : definition?.status,
+ isByo,
+ provider: definition?.byoConnectorName,
+ byoDisabled,
+ byoUnavailable,
+ };
+}
+
+/**
+ * The chips a row shows: at most one status and one administration chip.
+ *
+ * A locally administered, `Available` row produces none, which is the common case and keeps
+ * the list quiet. `Available` is deliberately not chipped: "working as configured" is not
+ * news, and both products render nothing for it today.
+ */
+export function getGuardrailListChips(
+ input: {
+ status?: GuardrailListItemState['status'];
+ administration?: GuardrailListAdministration;
+ },
+ labels: GuardrailListLabels
+): GuardrailListChip[] {
+ const chips: GuardrailListChip[] = [];
+
+ const statusChip = ((): Omit | undefined => {
+ switch (input.status) {
+ case 'FeatureDisabled':
+ return { tone: 'warning', label: labels.statusFeatureDisabled };
+ case 'Unauthorised':
+ return { tone: 'warning', label: labels.statusUnauthorized };
+ case 'Disabled':
+ return { tone: 'error', label: labels.statusDisabled };
+ case 'Unavailable':
+ return { tone: 'error', label: labels.statusUnavailable };
+ default:
+ return undefined;
+ }
+ })();
+ if (statusChip) chips.push({ id: 'status', ...statusChip });
+
+ if (input.administration === 'governance') {
+ chips.push({ id: 'administration', tone: 'neutral', label: labels.administrationGovernance });
+ }
+
+ return chips;
+}
diff --git a/packages/apollo-react/src/canvas/components/Guardrails/guardrail-list.stories.tsx b/packages/apollo-react/src/canvas/components/Guardrails/guardrail-list.stories.tsx
new file mode 100644
index 0000000000..e8ca5098d1
--- /dev/null
+++ b/packages/apollo-react/src/canvas/components/Guardrails/guardrail-list.stories.tsx
@@ -0,0 +1,259 @@
+import type { Meta, StoryObj } from '@storybook/react-vite';
+import {
+ Button,
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuTrigger,
+ TooltipProvider,
+} from '@uipath/apollo-wind';
+import { MoreVertical } from 'lucide-react';
+import { useState } from 'react';
+import { ApI18nProvider } from '../../../i18n';
+import type { GuardrailScope } from './builder-types';
+import { GuardrailStatusBanner } from './components/guardrail-status-banner';
+import { GuardrailList } from './guardrail-list';
+import type { GuardrailListDefinition, GuardrailListItem } from './list-types';
+
+const meta = {
+ title: 'Components/UiPath/Guardrail List',
+ component: GuardrailList,
+ parameters: {
+ layout: 'padded',
+ docs: {
+ description: {
+ component: `
+The guardrails applied to an agent or a tool: an ordered, reorderable list of rows with add,
+edit and remove affordances, the two bring-your-own configuration notices, and optional
+status chips.
+
+The host filters and this renders. Rows and definitions arrive already filtered by feature
+flags, entitlements and scope, and every callback is an intent: confirmation dialogs,
+persistence and telemetry stay with the product. Every addition beyond what both products
+already show is opt-in, so adopting it behind a flag renders what the host renders today.
+ `,
+ },
+ },
+ },
+ tags: ['autodocs'],
+ decorators: [
+ (Story) => (
+
+
+
+
+
+ ),
+ ],
+} satisfies Meta;
+
+export default meta;
+type Story = StoryObj;
+
+const AGENT_AND_TOOL: GuardrailScope[] = ['Agent', 'Tool'];
+
+const guardrails: GuardrailListItem[] = [
+ {
+ id: 'g1',
+ name: 'PII detection 1',
+ description: 'Scans agent output for personal data.',
+ $guardrailType: 'builtInValidator',
+ selector: { scopes: AGENT_AND_TOOL, matchNames: ['Send email'] },
+ action: { $actionType: 'log' },
+ validatorType: 'pii_detection',
+ },
+ {
+ id: 'g2',
+ name: 'Noma prompt shield',
+ description: 'Blocks prompt injection attempts on LLM calls.',
+ $guardrailType: 'builtInValidator',
+ selector: { scopes: ['Llm'] },
+ action: { $actionType: 'block' },
+ validatorType: 'byo',
+ byoValidatorName: 'noma_prompt_injection',
+ },
+ {
+ id: 'g3',
+ name: 'Blocked words',
+ $guardrailType: 'custom',
+ selector: { scopes: ['Tool'], matchNames: ['Send email'] },
+ action: { $actionType: 'filter' },
+ },
+];
+
+const definitions: GuardrailListDefinition[] = [
+ { validator: 'pii_detection', status: 'Available' },
+ {
+ validator: 'byo',
+ status: 'Available',
+ byoValidatorName: 'noma_prompt_injection',
+ byoConnectorName: 'Noma Security',
+ },
+];
+
+const noop = () => {};
+
+/** Flow's shape: the card, its own header with an Add button, and reorderable rows. */
+export const Default: Story = {
+ args: {
+ guardrails,
+ definitions,
+ previewChip: true,
+ onAdd: noop,
+ onEdit: noop,
+ onRemove: noop,
+ onReorder: noop,
+ },
+};
+
+/** Read-only, but still reorderable: what Flow passes when the canvas is locked. */
+export const ReadOnlyButReorderable: Story = {
+ args: { ...Default.args, disabled: true, reorderDisabled: false },
+};
+
+export const Empty: Story = {
+ args: { guardrails: [], definitions, onAdd: noop },
+};
+
+/** A bring-your-own row whose configuration was disabled, and one that no longer resolves. */
+export const ByoNotices: Story = {
+ args: {
+ guardrails,
+ definitions: [
+ { validator: 'pii_detection', status: 'Available' },
+ {
+ validator: 'byo',
+ status: 'Disabled',
+ byoValidatorName: 'noma_prompt_injection',
+ byoConnectorName: 'Noma Security',
+ },
+ ],
+ onEdit: noop,
+ onRemove: noop,
+ },
+};
+
+/** Status and administration chips, both off by default. */
+export const StatusChips: Story = {
+ args: {
+ guardrails,
+ definitions: [
+ { validator: 'pii_detection', status: 'Unauthorised' },
+ { validator: 'byo', status: 'Available', byoValidatorName: 'noma_prompt_injection' },
+ ],
+ statusChips: true,
+ previewChip: true,
+ getItemAdministration: (item) => (item.id === 'g1' ? 'governance' : 'local'),
+ onEdit: noop,
+ },
+};
+
+/** A definitions load failure is a host-owned banner rendered above the rows. */
+export const WithStatusBanner: Story = {
+ args: {
+ ...Default.args,
+ statusBanner: (
+
+ ),
+ },
+};
+
+/**
+ * Agents' shape: the section chrome and the add affordance belong to the host, rows open the
+ * editor on click, the actions are an overflow menu, and the description, provider and scopes
+ * move into a hover tooltip.
+ */
+export const EmbeddedInHostSection: Story = {
+ args: {
+ guardrails,
+ definitions,
+ unstyled: true,
+ hideHeader: true,
+ rowActivatesEdit: true,
+ previewChip: true,
+ emptyState: null,
+ onEdit: noop,
+ onRemove: noop,
+ onReorder: noop,
+ formatAction: () => null,
+ formatScopes: (item) => `Scopes: ${(item.selector?.scopes ?? []).join(', ')}`,
+ renderRowTooltip: (item) => (
+
+ {item.description &&
{item.description}
}
+
Scopes: {(item.selector?.scopes ?? []).join(', ')}
+
+ ),
+ renderItemActions: ({ item, onEdit, onRemove }) => (
+
+
+
+
+
+
+
+ onEdit?.()}>Edit
+ onRemove?.()}>Remove
+
+
+ ),
+ footer: (
+
+ Add another guardrail
+
+ ),
+ },
+ decorators: [
+ (Story) => (
+
+
+
Guardrails (host section)
+
+
+
+ ),
+ ],
+};
+
+/** Reordering reports the visible array plus the move, and the host owns the write. */
+export const ControlledReorder: Story = {
+ args: { guardrails, definitions, onReorder: noop },
+ render: (args) => {
+ function ReorderExample() {
+ const [rows, setRows] = useState(args.guardrails);
+ const [lastMove, setLastMove] = useState('none yet');
+ return (
+
+
{
+ setRows(reordered);
+ setLastMove(`${move.id}: ${move.from} to ${move.to}`);
+ }}
+ />
+ Last move: {lastMove}
+
+ );
+ }
+ return ;
+ },
+};
+
+/** Chrome strings resolved from the canvas lingui catalog (Japanese). */
+export const Localized: Story = {
+ args: { ...Default.args },
+ render: (args) => (
+
+
+
+ ),
+};
diff --git a/packages/apollo-react/src/canvas/components/Guardrails/guardrail-list.test.tsx b/packages/apollo-react/src/canvas/components/Guardrails/guardrail-list.test.tsx
new file mode 100644
index 0000000000..d5c855bb6c
--- /dev/null
+++ b/packages/apollo-react/src/canvas/components/Guardrails/guardrail-list.test.tsx
@@ -0,0 +1,274 @@
+import { fireEvent, render, screen } from '@testing-library/react';
+import { axe } from 'jest-axe';
+import { describe, expect, it, vi } from 'vitest';
+import { flushDndFrame, layoutRowsVertically } from './__fixtures__/dnd-geometry';
+import {
+ BYO_GUARDRAIL,
+ CUSTOM_GUARDRAIL,
+ DEFINITIONS,
+ GUARDRAILS,
+ PII_GUARDRAIL,
+} from './__fixtures__/guardrail-list.fixtures';
+import { GuardrailStatusBanner } from './components/guardrail-status-banner';
+import { GuardrailList, type GuardrailListProps } from './guardrail-list';
+
+function renderList(props: Partial = {}) {
+ return render( );
+}
+
+const reorderHandle = (name: string) =>
+ screen.getByRole('button', { name: `Reorder guardrail ${name}` });
+
+describe('GuardrailList', () => {
+ it('renders every row it is given, in order', () => {
+ renderList();
+
+ const rows = screen.getAllByText(/PII detection 1|Noma prompt shield|Blocked words/);
+ expect(rows.map((row) => row.textContent)).toEqual([
+ 'PII detection 1',
+ 'Noma prompt shield',
+ 'Blocked words',
+ ]);
+ });
+
+ it('renders a row whose definition never arrived', () => {
+ renderList({ definitions: [] });
+
+ expect(screen.getByText('Noma prompt shield')).toBeInTheDocument();
+ });
+
+ describe('header', () => {
+ it('renders the title and the add affordance', () => {
+ const onAdd = vi.fn();
+ renderList({ onAdd });
+
+ expect(screen.getByText('Guardrails')).toBeInTheDocument();
+ fireEvent.click(screen.getByRole('button', { name: 'Add' }));
+ expect(onAdd).toHaveBeenCalledTimes(1);
+ });
+
+ it('omits the add button when the host wired no intent', () => {
+ renderList();
+
+ expect(screen.queryByRole('button', { name: 'Add' })).not.toBeInTheDocument();
+ });
+
+ it('disables the add button while read-only', () => {
+ renderList({ onAdd: vi.fn(), disabled: true });
+
+ expect(screen.getByRole('button', { name: 'Add' })).toBeDisabled();
+ });
+
+ it('lets a slot replace the add button', () => {
+ renderList({ onAdd: vi.fn(), addSlot: Requires entitlement });
+
+ expect(screen.getByText('Requires entitlement')).toBeInTheDocument();
+ expect(screen.queryByRole('button', { name: 'Add' })).not.toBeInTheDocument();
+ });
+
+ it('can be hidden for hosts that own the section chrome', () => {
+ renderList({ hideHeader: true, onAdd: vi.fn() });
+
+ expect(screen.queryByText('Guardrails')).not.toBeInTheDocument();
+ expect(screen.queryByRole('button', { name: 'Add' })).not.toBeInTheDocument();
+ });
+ });
+
+ describe('chrome', () => {
+ it('renders a card by default and drops it when unstyled', () => {
+ const { container, unmount } = renderList();
+ expect(container.querySelector('[data-slot="guardrail-list"]')).toHaveClass('border');
+ unmount();
+
+ const { container: bare } = renderList({ unstyled: true });
+ expect(bare.querySelector('[data-slot="guardrail-list"]')).not.toHaveClass('border');
+ });
+
+ it('renders the status banner above the rows', () => {
+ renderList({
+ statusBanner: ,
+ });
+
+ expect(screen.getByRole('alert')).toHaveTextContent('Failed to load validators');
+ });
+
+ it('renders the footer below the rows', () => {
+ renderList({ footer: Add another guardrail });
+
+ expect(screen.getByRole('button', { name: 'Add another guardrail' })).toBeInTheDocument();
+ });
+ });
+
+ describe('empty state', () => {
+ it('explains itself by default', () => {
+ renderList({ guardrails: [] });
+
+ expect(screen.getByText('No guardrails configured')).toBeInTheDocument();
+ });
+
+ it('renders nothing when the host passes null', () => {
+ renderList({ guardrails: [], emptyState: null });
+
+ expect(screen.queryByText('No guardrails configured')).not.toBeInTheDocument();
+ });
+
+ it('renders a host-supplied replacement', () => {
+ renderList({ guardrails: [], emptyState: Nothing here yet });
+
+ expect(screen.getByText('Nothing here yet')).toBeInTheDocument();
+ expect(screen.queryByText('No guardrails configured')).not.toBeInTheDocument();
+ });
+ });
+
+ describe('rows', () => {
+ it('identifies rows by id, and by name when the host has no ids', () => {
+ const { container, unmount } = renderList();
+ expect(
+ Array.from(container.querySelectorAll('[data-guardrail-id]')).map((row) =>
+ row.getAttribute('data-guardrail-id')
+ )
+ ).toEqual(['g1', 'g2', 'g3']);
+ unmount();
+
+ const { container: byName } = renderList({
+ guardrails: [{ name: 'PII detection 1' }],
+ });
+ expect(byName.querySelector('[data-guardrail-id]')).toHaveAttribute(
+ 'data-guardrail-id',
+ 'PII detection 1'
+ );
+ });
+
+ it('lets the host override row identity', () => {
+ const { container } = renderList({ getItemId: (item) => `custom-${item.name}` });
+
+ expect(container.querySelector('[data-guardrail-id]')).toHaveAttribute(
+ 'data-guardrail-id',
+ 'custom-PII detection 1'
+ );
+ });
+
+ it('tags governance-administered rows when the chips are on', () => {
+ renderList({
+ statusChips: true,
+ getItemAdministration: (item) => (item.id === 'g2' ? 'governance' : 'local'),
+ });
+
+ expect(screen.getAllByText('Governance managed')).toHaveLength(1);
+ });
+
+ it('forwards the row intents', () => {
+ const onEdit = vi.fn();
+ const onRemove = vi.fn();
+ renderList({ onEdit, onRemove });
+
+ fireEvent.click(screen.getAllByRole('button', { name: 'Edit guardrail' })[0] as HTMLElement);
+ fireEvent.click(
+ screen.getAllByRole('button', { name: 'Remove guardrail' })[1] as HTMLElement
+ );
+
+ expect(onEdit).toHaveBeenCalledWith(PII_GUARDRAIL);
+ expect(onRemove).toHaveBeenCalledWith(BYO_GUARDRAIL);
+ });
+ });
+
+ describe('reorder', () => {
+ it('mounts no drag affordance without an onReorder intent', () => {
+ renderList();
+
+ expect(screen.queryByRole('button', { name: /^Reorder guardrail/ })).not.toBeInTheDocument();
+ });
+
+ it('mounts no drag affordance for a single row', () => {
+ renderList({ guardrails: [PII_GUARDRAIL], onReorder: vi.fn() });
+
+ expect(screen.queryByRole('button', { name: /^Reorder guardrail/ })).not.toBeInTheDocument();
+ });
+
+ it('follows `disabled` by default', () => {
+ renderList({ onReorder: vi.fn(), disabled: true });
+
+ expect(screen.queryByRole('button', { name: /^Reorder guardrail/ })).not.toBeInTheDocument();
+ });
+
+ it('stays reorderable in a read-only list when the host asks', () => {
+ renderList({ onReorder: vi.fn(), disabled: true, reorderDisabled: false });
+
+ expect(reorderHandle('PII detection 1')).toBeInTheDocument();
+ });
+
+ it('names each handle after its row, and keeps it keyboard reachable', () => {
+ renderList({ onReorder: vi.fn() });
+
+ const handle = reorderHandle('Noma prompt shield');
+ expect(handle.tagName).toBe('BUTTON');
+ expect(handle).not.toHaveAttribute('aria-hidden');
+ });
+
+ it('reports the reordered rows and the move after a keyboard drag', async () => {
+ const onReorder = vi.fn();
+ const { container } = renderList({ onReorder });
+ layoutRowsVertically(container);
+
+ const handle = reorderHandle('PII detection 1');
+ fireEvent.keyDown(handle, { key: ' ', code: 'Space' });
+ await flushDndFrame();
+ fireEvent.keyDown(handle, { key: 'ArrowDown', code: 'ArrowDown' });
+ await flushDndFrame();
+ fireEvent.keyDown(handle, { key: ' ', code: 'Space' });
+
+ expect(onReorder).toHaveBeenCalledWith([BYO_GUARDRAIL, PII_GUARDRAIL, CUSTOM_GUARDRAIL], {
+ from: 0,
+ to: 1,
+ id: 'g1',
+ });
+ });
+
+ it('reports nothing when a drag is cancelled', async () => {
+ const onReorder = vi.fn();
+ const { container } = renderList({ onReorder });
+ layoutRowsVertically(container);
+
+ const handle = reorderHandle('PII detection 1');
+ fireEvent.keyDown(handle, { key: ' ', code: 'Space' });
+ await flushDndFrame();
+ fireEvent.keyDown(handle, { key: 'ArrowDown', code: 'ArrowDown' });
+ await flushDndFrame();
+ fireEvent.keyDown(handle, { key: 'Escape', code: 'Escape' });
+
+ expect(onReorder).not.toHaveBeenCalled();
+ });
+
+ it('reports nothing when a row is dropped where it started', async () => {
+ const onReorder = vi.fn();
+ const { container } = renderList({ onReorder });
+ layoutRowsVertically(container);
+
+ const handle = reorderHandle('PII detection 1');
+ fireEvent.keyDown(handle, { key: ' ', code: 'Space' });
+ await flushDndFrame();
+ fireEvent.keyDown(handle, { key: ' ', code: 'Space' });
+
+ expect(onReorder).not.toHaveBeenCalled();
+ });
+ });
+
+ it('has no accessibility violations', async () => {
+ const { container } = renderList({
+ onAdd: vi.fn(),
+ onEdit: vi.fn(),
+ onRemove: vi.fn(),
+ onReorder: vi.fn(),
+ statusChips: true,
+ previewChip: true,
+ });
+
+ expect(await axe(container)).toHaveNoViolations();
+ });
+
+ it('has no accessibility violations while empty and read-only', async () => {
+ const { container } = renderList({ guardrails: [], disabled: true, onAdd: vi.fn() });
+
+ expect(await axe(container)).toHaveNoViolations();
+ });
+});
diff --git a/packages/apollo-react/src/canvas/components/Guardrails/guardrail-list.tsx b/packages/apollo-react/src/canvas/components/Guardrails/guardrail-list.tsx
new file mode 100644
index 0000000000..2deae046cf
--- /dev/null
+++ b/packages/apollo-react/src/canvas/components/Guardrails/guardrail-list.tsx
@@ -0,0 +1,326 @@
+import {
+ closestCenter,
+ DndContext,
+ type DragEndEvent,
+ KeyboardSensor,
+ PointerSensor,
+ useSensor,
+ useSensors,
+} from '@dnd-kit/core';
+import { restrictToParentElement, restrictToVerticalAxis } from '@dnd-kit/modifiers';
+import {
+ arrayMove,
+ SortableContext,
+ sortableKeyboardCoordinates,
+ useSortable,
+ verticalListSortingStrategy,
+} from '@dnd-kit/sortable';
+import { CSS } from '@dnd-kit/utilities';
+import { Button, cn } from '@uipath/apollo-wind';
+import { GripVertical, Plus, Shield } from 'lucide-react';
+import { type ReactNode, useCallback, useMemo } from 'react';
+import { GuardrailListRow, type GuardrailListRowProps } from './components/guardrail-list-row';
+import { getGuardrailListItemId, resolveGuardrailListItemState } from './guardrail-list-utils';
+import {
+ formatGuardrailFormMessage,
+ type GuardrailListLabels,
+ useGuardrailListLabels,
+} from './i18n';
+import type {
+ GuardrailListAdministration,
+ GuardrailListDefinition,
+ GuardrailListItem,
+ GuardrailListItemActionsContext,
+ GuardrailReorderMove,
+} from './list-types';
+
+const DND_MODIFIERS = [restrictToVerticalAxis, restrictToParentElement];
+// A pointer has to travel before a press becomes a drag, or clicking a row's body to edit it
+// registers as a nudge instead. Both products already use 8px.
+const POINTER_SENSOR_OPTIONS = { activationConstraint: { distance: 8 } };
+
+export interface GuardrailListProps {
+ /**
+ * The rows to render, in order, already filtered by the host: feature flags, entitlements
+ * and scope filtering never cross this boundary. The list renders every item it is given.
+ */
+ guardrails: GuardrailListItem[];
+ /**
+ * Definitions the rows resolve against, already filtered by the host. Used only to derive
+ * the provider line, the two BYO notices and the status chip; rows with no matching
+ * definition still render.
+ */
+ definitions?: GuardrailListDefinition[];
+ /** Read-only: suppresses add, edit and remove. Reorder follows `reorderDisabled`. */
+ disabled?: boolean;
+ /**
+ * Whether reordering is off. Defaults to `disabled`, which is Agents' behaviour; Flow keeps
+ * its read-only list reorderable and passes `false`. With reorder off no drag machinery is
+ * mounted at all.
+ */
+ reorderDisabled?: boolean;
+
+ /** Open the editor for a row. The row body activates it too when `rowActivatesEdit` is set. */
+ onEdit?: (item: GuardrailListItem) => void;
+ /**
+ * Request removal of a row. An intent, not a mutation: the confirmation dialog, the
+ * scoped-removal unwind and the write stay host-side, because the two products confirm with
+ * different copy and unwind tool-scoped guardrails differently.
+ */
+ onRemove?: (item: GuardrailListItem) => void;
+ /** Add affordance intent (header button, or whatever `addSlot` / `footer` render). */
+ onAdd?: () => void;
+ /**
+ * Reorder intent. Receives the reordered **visible** array plus the move, so a host
+ * rendering a filtered view (Flow's per-tool view over an agent-level list) can splice the
+ * result back without this component knowing a fuller list exists.
+ */
+ onReorder?: (guardrails: GuardrailListItem[], move: GuardrailReorderMove) => void;
+
+ /** Row identity. Defaults to `id ?? name`; must be stable and unique. */
+ getItemId?: (item: GuardrailListItem) => string;
+ /**
+ * Who administers a row. Defaults to `'local'`, which renders no chip. Governance-managed
+ * guardrails come from a different endpoint, so nothing on the record identifies them.
+ */
+ getItemAdministration?: (item: GuardrailListItem) => GuardrailListAdministration | undefined;
+
+ /** Render the status and administration chips. Off: neither product shows them today. */
+ statusChips?: boolean;
+ /** Render the "Preview" badge on built-in-validator rows. Product lifecycle, not a package concern. */
+ previewChip?: boolean;
+ /** Drop the card border and padding, for hosts that already own the section chrome. */
+ unstyled?: boolean;
+ /** Hide the header (title and add affordance) when the host renders its own. */
+ hideHeader?: boolean;
+ /** Make the row body a button that opens the editor. */
+ rowActivatesEdit?: boolean;
+ /** Replaces the default empty line. Pass `null` to render nothing when the list is empty. */
+ emptyState?: ReactNode;
+ /** Rendered under the rows, e.g. an add affordance that doubles as an entitlement notice. */
+ footer?: ReactNode;
+ /** Replaces the header's add button, e.g. with an entitlement lock. */
+ addSlot?: ReactNode;
+ /** Rendered above the rows, e.g. `GuardrailStatusBanner` for a definitions load failure. */
+ statusBanner?: ReactNode;
+
+ /** Hover content for a row's body. Return nothing to leave that row untooltipped. */
+ renderRowTooltip?: GuardrailListRowProps['renderRowTooltip'];
+ /** Replace a row's inline actions, e.g. with an overflow menu. */
+ renderItemActions?: (ctx: GuardrailListItemActionsContext) => ReactNode;
+ /** Localize a row's scopes line. Return `null` to hide it. Default: raw scopes, comma-joined. */
+ formatScopes?: (item: GuardrailListItem) => ReactNode;
+ /** Localize a row's action badge. Return `null` to hide it. Default: the raw action type. */
+ formatAction?: (item: GuardrailListItem) => ReactNode;
+
+ labels?: Partial;
+ className?: string;
+}
+
+type SortableRowProps = Omit;
+
+/**
+ * A row inside a `DndContext`. Mounted only when the list is reorderable, so `useSortable`
+ * never runs outside its provider, and a non-reorderable list carries no drag state at all.
+ */
+function SortableGuardrailRow({ id, item, labels, ...rowProps }: SortableRowProps) {
+ const {
+ attributes,
+ listeners,
+ setNodeRef,
+ setActivatorNodeRef,
+ transform,
+ transition,
+ isDragging,
+ } = useSortable({ id });
+
+ const handle = (
+
+
+
+ );
+
+ return (
+
+ );
+}
+
+/**
+ * The guardrails applied to an agent or a tool: an ordered, reorderable list of rows with
+ * add, edit and remove affordances.
+ *
+ * The host filters, this renders. Every callback is an intent, so dialogs, persistence and
+ * telemetry stay with the product, and every addition beyond what both products already show
+ * is opt-in, so adopting this behind a flag renders what the host renders today.
+ */
+export function GuardrailList({
+ guardrails,
+ definitions,
+ disabled = false,
+ reorderDisabled,
+ onEdit,
+ onRemove,
+ onAdd,
+ onReorder,
+ getItemId = getGuardrailListItemId,
+ getItemAdministration,
+ statusChips = false,
+ previewChip = false,
+ unstyled = false,
+ hideHeader = false,
+ rowActivatesEdit = false,
+ emptyState,
+ footer,
+ addSlot,
+ statusBanner,
+ renderRowTooltip,
+ renderItemActions,
+ formatScopes,
+ formatAction,
+ labels: labelOverrides,
+ className,
+}: GuardrailListProps) {
+ const labels = useGuardrailListLabels(labelOverrides);
+
+ const sensors = useSensors(
+ useSensor(PointerSensor, POINTER_SENSOR_OPTIONS),
+ useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates })
+ );
+
+ const rows = useMemo(
+ () => guardrails.map((item) => ({ item, id: getItemId(item) })),
+ [guardrails, getItemId]
+ );
+ const ids = useMemo(() => rows.map((row) => row.id), [rows]);
+ const isReorderable =
+ !(reorderDisabled ?? disabled) && onReorder !== undefined && guardrails.length > 1;
+
+ const handleDragEnd = useCallback(
+ ({ active, over }: DragEndEvent) => {
+ if (!onReorder || !over || active.id === over.id) return;
+ const from = ids.indexOf(String(active.id));
+ const to = ids.indexOf(String(over.id));
+ if (from === -1 || to === -1) return;
+ // `active.id` is the row id `getItemId` produced, so the move needs no index lookup.
+ onReorder(arrayMove(guardrails, from, to), { from, to, id: String(active.id) });
+ },
+ [guardrails, ids, onReorder]
+ );
+
+ const renderedRows = rows.map(({ item, id }, index) => {
+ const rowProps: Omit = {
+ item,
+ id,
+ index,
+ state: resolveGuardrailListItemState(item, definitions),
+ labels,
+ disabled,
+ administration: getItemAdministration?.(item),
+ statusChips,
+ previewChip,
+ rowActivatesEdit,
+ onEdit,
+ onRemove,
+ renderRowTooltip,
+ renderItemActions,
+ formatScopes,
+ formatAction,
+ };
+ return isReorderable ? (
+
+ ) : (
+
+ );
+ });
+
+ const content =
+ guardrails.length > 0 ? (
+ {renderedRows}
+ ) : // An explicit `null` hides the empty line; only an absent prop takes the default.
+ emptyState === undefined ? (
+ {labels.empty}
+ ) : (
+ emptyState
+ );
+
+ return (
+
+ {!hideHeader && (
+
+
+
+ {labels.title}
+
+ {addSlot !== undefined
+ ? addSlot
+ : onAdd && (
+
+
+ {labels.add}
+
+ )}
+
+ )}
+
+ {statusBanner}
+ {isReorderable ? (
+
+
+ {content}
+
+
+ ) : (
+ content
+ )}
+ {footer}
+
+
+ );
+}
diff --git a/packages/apollo-react/src/canvas/components/Guardrails/guardrail-palette-utils.test.ts b/packages/apollo-react/src/canvas/components/Guardrails/guardrail-palette-utils.test.ts
new file mode 100644
index 0000000000..87ba022bf8
--- /dev/null
+++ b/packages/apollo-react/src/canvas/components/Guardrails/guardrail-palette-utils.test.ts
@@ -0,0 +1,120 @@
+import { describe, expect, it } from 'vitest';
+import {
+ BYO_CONNECTOR_ONLY_DEFINITION,
+ BYO_FOLDER_DEFINITION,
+ BYO_SECOND_CONNECTION_DEFINITION,
+ MIXED_DEFINITIONS,
+ PII_DEFINITION,
+ PROMPT_ATTACKS_DEFINITION,
+ UIPATH_DEFINITIONS,
+} from './__fixtures__/guardrail-palette.fixtures';
+import { getGuardrailPaletteItemId, groupGuardrailsForPalette } from './guardrail-palette-utils';
+import type { GuardrailPaletteDefinition } from './palette-types';
+
+describe('getGuardrailPaletteItemId', () => {
+ it('identifies a UiPath validator by its validator id', () => {
+ expect(getGuardrailPaletteItemId(PII_DEFINITION)).toBe('pii_detection');
+ });
+
+ it('joins the connection id for a bring-your-own validator', () => {
+ expect(getGuardrailPaletteItemId(BYO_FOLDER_DEFINITION)).toBe(
+ 'noma_prompt_injection:connection-1'
+ );
+ });
+
+ it('keeps one validator name on two connections apart', () => {
+ expect(getGuardrailPaletteItemId(BYO_FOLDER_DEFINITION)).not.toBe(
+ getGuardrailPaletteItemId(BYO_SECOND_CONNECTION_DEFINITION)
+ );
+ });
+
+ it('still produces an id for a bring-your-own definition with no connection', () => {
+ const orphan: GuardrailPaletteDefinition = {
+ validator: 'byo',
+ displayName: 'Orphan',
+ status: 'Available',
+ byoValidatorName: 'orphan_validator',
+ };
+
+ expect(getGuardrailPaletteItemId(orphan)).toBe('orphan_validator:');
+ });
+});
+
+describe('groupGuardrailsForPalette', () => {
+ it('returns one unheaded group, in payload order, with no bring-your-own definitions', () => {
+ const groups = groupGuardrailsForPalette(UIPATH_DEFINITIONS, 'UiPath guardrails');
+
+ expect(groups).toEqual([
+ { key: '__all__', header: null, isByo: false, definitions: UIPATH_DEFINITIONS },
+ ]);
+ });
+
+ it('returns no groups at all for an empty catalog', () => {
+ // Deliberately not the hosts' `[{ key: '__all__', definitions: [] }]`: Flow's own empty
+ // state is guarded on `groups.length === 0`, which its implementation can never reach.
+ expect(groupGuardrailsForPalette([], 'UiPath guardrails')).toEqual([]);
+ });
+
+ it('groups bring-your-own definitions by folder and trails the UiPath group', () => {
+ const groups = groupGuardrailsForPalette(MIXED_DEFINITIONS, 'UiPath guardrails');
+
+ expect(groups.map((group) => [group.key, group.header, group.isByo])).toEqual([
+ ['Acme Guard', 'Acme Guard', true],
+ ['Shared/Security', 'Shared/Security', true],
+ ['__uipath__', 'UiPath guardrails', false],
+ ]);
+ });
+
+ it('prefers the folder path over the connector name as the group key', () => {
+ const groups = groupGuardrailsForPalette(
+ [BYO_FOLDER_DEFINITION, BYO_CONNECTOR_ONLY_DEFINITION],
+ 'UiPath guardrails'
+ );
+
+ expect(groups.map((group) => group.key)).toEqual(['Acme Guard', 'Shared/Security']);
+ });
+
+ it('sorts definitions inside a group by display name', () => {
+ const groups = groupGuardrailsForPalette(
+ [PROMPT_ATTACKS_DEFINITION, PII_DEFINITION, BYO_FOLDER_DEFINITION],
+ 'UiPath guardrails'
+ );
+ const uipath = groups.at(-1);
+
+ expect(uipath?.definitions.map((definition) => definition.displayName)).toEqual([
+ 'PII detection',
+ 'Prompt attacks',
+ ]);
+ });
+
+ it('heads a bring-your-own group with nothing when it has neither folder nor connector', () => {
+ const nameless: GuardrailPaletteDefinition = {
+ validator: 'byo',
+ displayName: 'Nameless',
+ status: 'Available',
+ byoValidatorName: 'nameless',
+ };
+ const groups = groupGuardrailsForPalette([nameless, PII_DEFINITION], 'UiPath guardrails');
+
+ expect(groups[0]).toEqual({
+ key: '__byo__',
+ header: null,
+ isByo: true,
+ definitions: [nameless],
+ });
+ });
+
+ it('omits the UiPath group when every definition is bring-your-own', () => {
+ const groups = groupGuardrailsForPalette([BYO_FOLDER_DEFINITION], 'UiPath guardrails');
+
+ expect(groups).toHaveLength(1);
+ expect(groups[0]?.isByo).toBe(true);
+ });
+
+ it('never mutates the input array', () => {
+ const input = [PROMPT_ATTACKS_DEFINITION, PII_DEFINITION];
+ groupGuardrailsForPalette(input, 'UiPath guardrails');
+
+ expect(input).toEqual([PROMPT_ATTACKS_DEFINITION, PII_DEFINITION]);
+ });
+});
diff --git a/packages/apollo-react/src/canvas/components/Guardrails/guardrail-palette-utils.ts b/packages/apollo-react/src/canvas/components/Guardrails/guardrail-palette-utils.ts
new file mode 100644
index 0000000000..a3765d0d1b
--- /dev/null
+++ b/packages/apollo-react/src/canvas/components/Guardrails/guardrail-palette-utils.ts
@@ -0,0 +1,88 @@
+import { isByoGuardrailDefinition } from './definitions-enrich';
+import type { GuardrailPaletteDefinition, GuardrailPaletteGroup } from './palette-types';
+
+/**
+ * Pure helpers behind `GuardrailPalette`. Exported because hosts need the same answers
+ * outside the picker (a sidebar deciding whether it has anything to show, a telemetry
+ * payload naming the chosen item), and because this is where both products' duplicated
+ * grouping converges.
+ */
+
+/**
+ * A palette entry's identity.
+ *
+ * A UiPath validator is unique by its id. A bring-your-own validator name is unique per
+ * *connection*, not per tenant, so two connections can expose the same name and the
+ * connection id has to join the key. This is Agents' rule; Flow keys BYO entries by
+ * `byo:` alone today, which collides in exactly that case.
+ *
+ * Not the same question as `matchesGuardrailListDefinition`, which resolves a *saved*
+ * guardrail against the catalog and matches BYO on the name alone on purpose (an admin
+ * rebinding a configuration to another connection must still resolve).
+ */
+export function getGuardrailPaletteItemId(definition: GuardrailPaletteDefinition): string {
+ return definition.byoValidatorName === undefined
+ ? definition.validator
+ : `${definition.byoValidatorName}:${definition.byoGuardrailConnectionId ?? ''}`;
+}
+
+/**
+ * Group definitions the way both products already do (`groupGuardrailsForPalette`, shipped
+ * identically in Agents and Flow):
+ *
+ * - No bring-your-own definitions at all: one unheaded group, payload order preserved.
+ * - Otherwise: one group per BYO `folderPath ?? byoConnectorName`, sorted by that key, then a
+ * trailing UiPath group. Definitions inside a group sort by display name.
+ *
+ * The unheaded case keeps payload order because that is the backend's own ordering, and a
+ * single-group palette has nothing to disambiguate. Once folders appear, names sort so a
+ * customer's folder does not reshuffle whenever the catalog does.
+ */
+export function groupGuardrailsForPalette(
+ definitions: readonly T[],
+ uipathGroupLabel: string
+): Array> {
+ if (!definitions.some(isByoGuardrailDefinition)) {
+ return definitions.length === 0
+ ? []
+ : [{ key: '__all__', header: null, isByo: false, definitions: [...definitions] }];
+ }
+
+ const byoGroups = new Map();
+ const uipathDefinitions: T[] = [];
+
+ for (const definition of definitions) {
+ if (isByoGuardrailDefinition(definition)) {
+ const key = definition.folderPath ?? definition.byoConnectorName;
+ const existing = byoGroups.get(key);
+ if (existing) existing.push(definition);
+ else byoGroups.set(key, [definition]);
+ } else {
+ uipathDefinitions.push(definition);
+ }
+ }
+
+ const byDisplayName = (a: T, b: T) => a.displayName.localeCompare(b.displayName);
+
+ const groups: Array> = [...byoGroups.entries()]
+ .sort(([a], [b]) => (a ?? '').localeCompare(b ?? ''))
+ .map(([key, list]) => ({
+ // A BYO definition with neither a folder nor a connector name has nothing to head its
+ // group with; both products still render it, unheaded.
+ key: key ?? '__byo__',
+ header: key ?? null,
+ isByo: true,
+ definitions: [...list].sort(byDisplayName),
+ }));
+
+ if (uipathDefinitions.length > 0) {
+ groups.push({
+ key: '__uipath__',
+ header: uipathGroupLabel,
+ isByo: false,
+ definitions: [...uipathDefinitions].sort(byDisplayName),
+ });
+ }
+
+ return groups;
+}
diff --git a/packages/apollo-react/src/canvas/components/Guardrails/guardrail-palette.stories.tsx b/packages/apollo-react/src/canvas/components/Guardrails/guardrail-palette.stories.tsx
new file mode 100644
index 0000000000..82264b88b6
--- /dev/null
+++ b/packages/apollo-react/src/canvas/components/Guardrails/guardrail-palette.stories.tsx
@@ -0,0 +1,239 @@
+import type { Meta, StoryObj } from '@storybook/react-vite';
+import {
+ Button,
+ Dialog,
+ DialogContent,
+ DialogHeader,
+ DialogTitle,
+ TooltipProvider,
+} from '@uipath/apollo-wind';
+import { ArrowLeft, Plus } from 'lucide-react';
+import { useState } from 'react';
+import { ApI18nProvider } from '../../../i18n';
+import { GuardrailPalette } from './guardrail-palette';
+import type { GuardrailPaletteDefinition } from './palette-types';
+
+const meta = {
+ title: 'Components/UiPath/Guardrail Palette',
+ component: GuardrailPalette,
+ parameters: {
+ layout: 'padded',
+ docs: {
+ description: {
+ component: `
+The add-guardrail picker: the guardrail definitions a user may add, grouped by
+bring-your-own folder with the UiPath validators trailing, plus an optional create-custom
+entry.
+
+The host filters and this renders. Definitions arrive already filtered by feature flags,
+entitlements and scope, and both callbacks are intents: the builder that opens next, the
+default name it starts with and the telemetry all stay with the product. Only the picker
+ships. The shell around it is host orchestration, because the two products disagree and
+both are right for their surface: a dialog, a properties-panel overlay, or a whole sidebar.
+ `,
+ },
+ },
+ },
+ tags: ['autodocs'],
+ decorators: [
+ (Story) => (
+
+
+
+
+
+ ),
+ ],
+} satisfies Meta;
+
+export default meta;
+type Story = StoryObj;
+
+const uipathDefinitions: GuardrailPaletteDefinition[] = [
+ {
+ validator: 'pii_detection',
+ displayName: 'PII detection',
+ description: 'Detects personally identifiable information in agent traffic.',
+ status: 'Available',
+ },
+ {
+ validator: 'user_prompt_attacks',
+ displayName: 'Prompt attacks',
+ description: 'Detects attempts to override the agent instructions.',
+ status: 'Available',
+ },
+ {
+ validator: 'llm_as_judge',
+ displayName: 'LLM as a judge',
+ description: 'Evaluates agent traffic against a policy you describe in natural language.',
+ status: 'Available',
+ },
+];
+
+const byoDefinitions: GuardrailPaletteDefinition[] = [
+ {
+ validator: 'byo',
+ displayName: 'Noma prompt shield',
+ description: 'Vendor-managed prompt injection detection.',
+ status: 'Available',
+ byoValidatorName: 'noma_prompt_injection',
+ byoConnectorName: 'Noma Security',
+ byoGuardrailConnectionId: 'connection-1',
+ folderPath: 'Shared/Security',
+ },
+ {
+ validator: 'byo',
+ displayName: 'Acme policy check',
+ status: 'Available',
+ byoValidatorName: 'acme_policy',
+ byoConnectorName: 'Acme Guard',
+ byoGuardrailConnectionId: 'connection-2',
+ },
+];
+
+const noop = () => {};
+
+/** UiPath validators only: one unheaded group, in the order the catalog returned them. */
+export const Default: Story = {
+ args: {
+ ootbDefinitions: uipathDefinitions,
+ previewChip: true,
+ onSelectOotb: noop,
+ },
+};
+
+/** With a create-custom intent wired, the entry leads the picker. Flow passes it for tools only. */
+export const WithCreateCustom: Story = {
+ args: { ...Default.args, onCreateCustom: noop },
+};
+
+/**
+ * Bring-your-own definitions group by folder, falling back to the connector name, and the
+ * UiPath validators trail them.
+ */
+export const ByoGroups: Story = {
+ args: {
+ ootbDefinitions: [...byoDefinitions, ...uipathDefinitions],
+ previewChip: true,
+ onSelectOotb: noop,
+ onCreateCustom: noop,
+ },
+};
+
+/**
+ * A validator the tenant is not entitled to stays visible so it can be discovered, chipped
+ * and not choosable. It keeps its place in the tab order, so a keyboard user reaches the chip
+ * that says why.
+ */
+export const Unauthorized: Story = {
+ args: {
+ ootbDefinitions: [
+ ...uipathDefinitions.slice(0, 1),
+ {
+ validator: 'harmful_content',
+ displayName: 'Harmful content',
+ description: 'Detects harmful content categories in agent traffic.',
+ status: 'Unauthorised',
+ },
+ ],
+ previewChip: true,
+ onSelectOotb: noop,
+ },
+};
+
+export const Loading: Story = {
+ args: { ootbDefinitions: [], isLoading: true, onSelectOotb: noop },
+};
+
+/** A load failure is a banner, and any catalog that did arrive stays pickable under it. */
+export const LoadFailed: Story = {
+ args: {
+ ootbDefinitions: [],
+ error: new Error('Request failed with status 503'),
+ onSelectOotb: noop,
+ },
+};
+
+/** Nothing to pick and no create-custom intent: the only case that renders the empty line. */
+export const Empty: Story = {
+ args: { ootbDefinitions: [], onSelectOotb: noop },
+};
+
+/** Flow's shape: the picker inside a dialog the host owns, which closes on selection. */
+export const InADialog: Story = {
+ args: { ootbDefinitions: [...byoDefinitions, ...uipathDefinitions], onSelectOotb: noop },
+ render: (args) => {
+ function DialogExample() {
+ const [open, setOpen] = useState(true);
+ const [chosen, setChosen] = useState('nothing yet');
+ return (
+
+
setOpen(true)}>
+ Add guardrail
+
+
Last choice: {chosen}
+
+
+
+ Add guardrail
+
+ {
+ setChosen('a custom guardrail');
+ setOpen(false);
+ }}
+ onSelectOotb={(definition) => {
+ setChosen(definition.displayName);
+ setOpen(false);
+ }}
+ />
+
+
+
+ );
+ }
+ return ;
+ },
+};
+
+/**
+ * Agents' shape: a whole sidebar view, with the header, its back button and its own create
+ * affordance owned by the host. `onCreateCustom` stays unwired so the picker renders no
+ * duplicate entry.
+ */
+export const InAHostSidebar: Story = {
+ args: {
+ ootbDefinitions: [...byoDefinitions, ...uipathDefinitions],
+ previewChip: true,
+ onSelectOotb: noop,
+ },
+ render: (args) => (
+
+
+
+
+
+
Choose guardrail
+
+
+ Create custom
+
+
+
+
+
+
+ ),
+};
+
+/** Chrome strings resolved from the canvas lingui catalog (Japanese). */
+export const Localized: Story = {
+ args: { ...Default.args, onCreateCustom: noop },
+ render: (args) => (
+
+
+
+ ),
+};
diff --git a/packages/apollo-react/src/canvas/components/Guardrails/guardrail-palette.test.tsx b/packages/apollo-react/src/canvas/components/Guardrails/guardrail-palette.test.tsx
new file mode 100644
index 0000000000..a6bf36cb4b
--- /dev/null
+++ b/packages/apollo-react/src/canvas/components/Guardrails/guardrail-palette.test.tsx
@@ -0,0 +1,222 @@
+import { fireEvent, render, screen } from '@testing-library/react';
+import { axe } from 'jest-axe';
+import { describe, expect, it, vi } from 'vitest';
+import {
+ BYO_CONNECTOR_ONLY_DEFINITION,
+ BYO_FOLDER_DEFINITION,
+ MIXED_DEFINITIONS,
+ PII_DEFINITION,
+ UIPATH_DEFINITIONS,
+ UNAUTHORIZED_DEFINITION,
+} from './__fixtures__/guardrail-palette.fixtures';
+import type { EnrichedGuardrailDefinition } from './definitions-enrich';
+import { GuardrailPalette, type GuardrailPaletteProps } from './guardrail-palette';
+
+function renderPalette(props: Partial = {}) {
+ return render(
+
+ );
+}
+
+const entries = () => screen.getAllByRole('button').map((item) => item.textContent);
+
+describe('GuardrailPalette', () => {
+ it('offers every definition it is given, in payload order', () => {
+ renderPalette();
+
+ expect(entries()).toEqual([
+ 'PII detectionDetects personally identifiable information in agent traffic.',
+ 'Prompt attacksDetects attempts to override the agent instructions.',
+ ]);
+ });
+
+ it('reports the chosen definition, unchanged', () => {
+ const onSelectOotb = vi.fn();
+ renderPalette({ onSelectOotb });
+
+ fireEvent.click(screen.getByRole('button', { name: /PII detection/ }));
+ expect(onSelectOotb).toHaveBeenCalledWith(PII_DEFINITION);
+ });
+
+ it('hands a host its own richer definition type back', () => {
+ // The generic is what keeps `parameters` (and everything else the palette never reads) on
+ // the object the host gets back, so it can open its builder without a second lookup.
+ const enriched: EnrichedGuardrailDefinition[] = [
+ {
+ validator: 'pii_detection',
+ displayName: 'PII detection',
+ description: 'Detects PII.',
+ allowedScopes: ['Agent', 'Tool'],
+ parameters: [
+ {
+ id: 'entities',
+ type: 'enum-list',
+ label: 'Entities',
+ required: true,
+ defaultValue: [],
+ },
+ ],
+ status: 'Available',
+ },
+ ];
+ const onSelectOotb = vi.fn<(definition: EnrichedGuardrailDefinition) => void>();
+ render( );
+
+ fireEvent.click(screen.getByRole('button', { name: /PII detection/ }));
+ expect(onSelectOotb).toHaveBeenCalledWith(enriched[0]);
+ });
+
+ it('names its region, so a host dropping it into a sidebar gets one for free', () => {
+ renderPalette();
+
+ expect(screen.getByRole('group', { name: 'Available guardrails' })).toBeInTheDocument();
+ });
+
+ describe('grouping', () => {
+ it('renders no heading at all when there is nothing to disambiguate', () => {
+ const { container } = renderPalette();
+
+ expect(container.textContent).not.toContain('UiPath guardrails');
+ });
+
+ it('heads bring-your-own folders and trails the UiPath group', () => {
+ renderPalette({ ootbDefinitions: MIXED_DEFINITIONS });
+
+ expect(screen.getByRole('group', { name: 'Acme Guard' })).toBeInTheDocument();
+ expect(screen.getByRole('group', { name: 'Shared/Security' })).toBeInTheDocument();
+ expect(screen.getByRole('group', { name: 'UiPath guardrails' })).toBeInTheDocument();
+ expect(entries()).toEqual([
+ 'Acme policy checkAcme Guard',
+ 'Noma prompt shieldNoma SecurityVendor-managed prompt injection detection.',
+ 'PII detectionDetects personally identifiable information in agent traffic.',
+ 'Prompt attacksDetects attempts to override the agent instructions.',
+ ]);
+ });
+
+ it('chips the bring-your-own connector on its entry', () => {
+ renderPalette({ ootbDefinitions: [BYO_CONNECTOR_ONLY_DEFINITION, PII_DEFINITION] });
+
+ expect(
+ screen.getByText('Acme Guard', { selector: '[data-slot="guardrail-status-chip"]' })
+ ).toBeInTheDocument();
+ });
+ });
+
+ describe('an unauthorized definition', () => {
+ it('is offered, chipped and not choosable', () => {
+ const onSelectOotb = vi.fn();
+ renderPalette({
+ ootbDefinitions: [PII_DEFINITION, UNAUTHORIZED_DEFINITION],
+ onSelectOotb,
+ });
+
+ const item = screen.getByRole('button', { name: /Harmful content/ });
+ expect(item).toHaveTextContent('Unauthorized');
+ expect(item).toHaveAttribute('aria-disabled', 'true');
+ fireEvent.click(item);
+ expect(onSelectOotb).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('the create-custom entry', () => {
+ it('appears only when the host wires the intent, first and with a description', () => {
+ const onCreateCustom = vi.fn();
+ renderPalette({ onCreateCustom });
+
+ expect(entries()[0]).toBe('Custom guardrailCreate a guardrail with custom rules');
+ fireEvent.click(screen.getByRole('button', { name: /Custom guardrail/ }));
+ expect(onCreateCustom).toHaveBeenCalledTimes(1);
+ });
+
+ it('is absent when the host owns the affordance itself', () => {
+ renderPalette();
+
+ expect(screen.queryByRole('button', { name: /Custom guardrail/ })).not.toBeInTheDocument();
+ });
+ });
+
+ describe('the preview chip', () => {
+ it('is off by default', () => {
+ renderPalette();
+
+ expect(screen.queryByText('Preview')).not.toBeInTheDocument();
+ });
+
+ it('marks every definition entry when on, and never the create-custom one', () => {
+ renderPalette({
+ ootbDefinitions: MIXED_DEFINITIONS,
+ previewChip: true,
+ onCreateCustom: vi.fn(),
+ });
+
+ expect(screen.getAllByText('Preview')).toHaveLength(MIXED_DEFINITIONS.length);
+ expect(screen.getByRole('button', { name: /Custom guardrail/ })).not.toHaveTextContent(
+ 'Preview'
+ );
+ });
+ });
+
+ describe('empty, loading and failed', () => {
+ it('says so when there is nothing at all to pick', () => {
+ renderPalette({ ootbDefinitions: [] });
+
+ expect(screen.getByText('No guardrails available')).toBeInTheDocument();
+ });
+
+ it('stays quiet when the create-custom entry is still something to pick', () => {
+ renderPalette({ ootbDefinitions: [], onCreateCustom: vi.fn() });
+
+ expect(screen.queryByText('No guardrails available')).not.toBeInTheDocument();
+ expect(screen.getByRole('button', { name: /Custom guardrail/ })).toBeInTheDocument();
+ });
+
+ it('renders a polite loading line instead of the entries', () => {
+ renderPalette({ isLoading: true, onCreateCustom: vi.fn() });
+
+ expect(screen.getByRole('status')).toHaveTextContent('Loading...');
+ expect(screen.queryByRole('button')).not.toBeInTheDocument();
+ });
+
+ it('renders a transport failure as a status banner', () => {
+ renderPalette({ error: new Error('boom'), ootbDefinitions: [] });
+
+ expect(screen.getByText('Failed to load built-in validators')).toBeInTheDocument();
+ });
+
+ it('keeps the entries under the banner when a stale catalog is still usable', () => {
+ renderPalette({ error: new Error('boom') });
+
+ expect(screen.getByText('Failed to load built-in validators')).toBeInTheDocument();
+ expect(screen.getByRole('button', { name: /PII detection/ })).toBeInTheDocument();
+ });
+ });
+
+ it('takes per-string label overrides over the catalog', () => {
+ renderPalette({ ootbDefinitions: [], labels: { empty: 'Nothing here' } });
+
+ expect(screen.getByText('Nothing here')).toBeInTheDocument();
+ });
+
+ it('has no accessibility violations', async () => {
+ const { container } = renderPalette({
+ ootbDefinitions: [...MIXED_DEFINITIONS, UNAUTHORIZED_DEFINITION, BYO_FOLDER_DEFINITION],
+ onCreateCustom: vi.fn(),
+ previewChip: true,
+ });
+
+ expect(await axe(container)).toHaveNoViolations();
+ });
+
+ it('has no accessibility violations while loading or failed', async () => {
+ const { container, unmount } = renderPalette({ isLoading: true });
+ expect(await axe(container)).toHaveNoViolations();
+ unmount();
+
+ const { container: failed } = renderPalette({ error: new Error('boom'), ootbDefinitions: [] });
+ expect(await axe(failed)).toHaveNoViolations();
+ });
+});
diff --git a/packages/apollo-react/src/canvas/components/Guardrails/guardrail-palette.tsx b/packages/apollo-react/src/canvas/components/Guardrails/guardrail-palette.tsx
new file mode 100644
index 0000000000..40569ebfd1
--- /dev/null
+++ b/packages/apollo-react/src/canvas/components/Guardrails/guardrail-palette.tsx
@@ -0,0 +1,148 @@
+import { cn } from '@uipath/apollo-wind';
+import { FolderOpen, Loader2, Plus } from 'lucide-react';
+import { useMemo } from 'react';
+import { GuardrailPaletteItem } from './components/guardrail-palette-item';
+import { GuardrailStatusBanner } from './components/guardrail-status-banner';
+import { GuardrailStatusChip } from './components/guardrail-status-chip';
+import { getGuardrailPaletteItemId, groupGuardrailsForPalette } from './guardrail-palette-utils';
+import { type GuardrailPaletteLabels, useGuardrailPaletteLabels } from './i18n';
+import type { GuardrailPaletteDefinition } from './palette-types';
+
+export interface GuardrailPaletteProps<
+ T extends GuardrailPaletteDefinition = GuardrailPaletteDefinition,
+> {
+ /**
+ * The definitions to offer, **already filtered by the host**: feature flags, entitlements,
+ * `FeatureDisabled` / `Disabled` removal and scope filtering never cross this boundary.
+ * Both products already filter before rendering; the palette offers everything it is given.
+ */
+ ootbDefinitions: T[];
+ /** A definition was chosen. The builder, the default name and telemetry stay host-side. */
+ onSelectOotb: (definition: T) => void;
+ /**
+ * Create a custom guardrail. Its **presence** is what renders the create-custom entry, so a
+ * host offering it conditionally (Flow only for `scope === 'Tool'`) passes it conditionally,
+ * and a host that puts the affordance in its own chrome (Agents' palette header) omits it.
+ */
+ onCreateCustom?: () => void;
+ /** Definitions are in flight: renders a loading line instead of the entries. */
+ isLoading?: boolean;
+ /** Transport failure, shaped like `useGuardrailDefinitions`' `error`. */
+ error?: Error | null;
+ /** Render the "Preview" chip on definition entries. Product lifecycle, not a package concern. */
+ previewChip?: boolean;
+ labels?: Partial;
+ className?: string;
+}
+
+/** Chips after an entry's name: the BYO connector, the lifecycle chip, the status chip. */
+function definitionChips(
+ definition: GuardrailPaletteDefinition,
+ labels: GuardrailPaletteLabels,
+ previewChip: boolean
+) {
+ return (
+ <>
+ {definition.byoConnectorName !== undefined && (
+ {definition.byoConnectorName}
+ )}
+ {previewChip && {labels.preview} }
+ {definition.status === 'Unauthorised' && (
+ {labels.statusUnauthorized}
+ )}
+ >
+ );
+}
+
+/**
+ * The add-guardrail picker: the guardrail definitions a user may add, grouped, with an
+ * optional create-custom entry.
+ *
+ * Ships the picker only. The shell around it is host orchestration, because the two products
+ * disagree about it and both are right for their surface: Flow opens a dialog (or an inline
+ * overlay that renders the chosen builder underneath), Agents takes over the whole sidebar
+ * with a back button and its own create affordance. Composing either around this is a few
+ * lines; a wrapper that modelled both would be a worse contract than no wrapper.
+ */
+export function GuardrailPalette({
+ ootbDefinitions,
+ onSelectOotb,
+ onCreateCustom,
+ isLoading = false,
+ error = null,
+ previewChip = false,
+ labels: labelOverrides,
+ className,
+}: GuardrailPaletteProps) {
+ const labels = useGuardrailPaletteLabels(labelOverrides);
+
+ const groups = useMemo(
+ () => groupGuardrailsForPalette(ootbDefinitions, labels.uipathGroup),
+ [ootbDefinitions, labels.uipathGroup]
+ );
+
+ // The empty line only stands in for the whole palette: with a create-custom entry there is
+ // still something to pick, and announcing "no guardrails available" above it contradicts it.
+ const isEmpty = groups.length === 0 && onCreateCustom === undefined;
+
+ return (
+ // biome-ignore lint/a11y/useSemanticElements: the region groups buttons, not form controls; would add form semantics and its own default box (same call as ProbeCard and wind's ButtonGroup)
+
+ {error &&
}
+ {isLoading ? (
+ //
for its implicit role="status": a polite live region, and the one native
+ // element that carries it without bringing any styling of its own.
+
+
+ {labels.loading}
+
+ ) : (
+ <>
+ {onCreateCustom && (
+ }
+ onSelect={onCreateCustom}
+ />
+ )}
+ {isEmpty && {labels.empty}
}
+ {groups.map((group) => (
+ // biome-ignore lint/a11y/useSemanticElements: as above; the name is what tells a screen reader which folder an entry came from, since the heading is only adjacent text and two folders can hold the same validator
+
+ {group.header !== null && (
+
+ {group.isByo && }
+ {group.header}
+
+ )}
+ {group.definitions.map((definition) => (
+
onSelectOotb(definition)}
+ />
+ ))}
+
+ ))}
+ >
+ )}
+
+ );
+}
diff --git a/packages/apollo-react/src/canvas/components/Guardrails/guardrail-validator-form.stories.tsx b/packages/apollo-react/src/canvas/components/Guardrails/guardrail-validator-form.stories.tsx
new file mode 100644
index 0000000000..29fabd3806
--- /dev/null
+++ b/packages/apollo-react/src/canvas/components/Guardrails/guardrail-validator-form.stories.tsx
@@ -0,0 +1,302 @@
+import type { Meta, StoryObj } from '@storybook/react-vite';
+import { Button, FormField, FormFieldLabel, TooltipProvider } from '@uipath/apollo-wind';
+import { useState } from 'react';
+import { ApI18nProvider } from '../../../i18n';
+import { GuardrailValidatorForm } from './guardrail-validator-form';
+import type {
+ GuardrailParameterDefinition,
+ GuardrailValidatorFormProps,
+ GuardrailValidatorParameter,
+} from './types';
+import { getRequiredEmptyParameterIds, seedGuardrailParameters } from './utils';
+
+const meta = {
+ title: 'Components/UiPath/Guardrail Validator Form',
+ component: GuardrailValidatorForm,
+ parameters: {
+ layout: 'centered',
+ docs: {
+ description: {
+ component: `
+Configuration section of an OOTB guardrail validator: one editor per parameter definition,
+covering all seven parameter types (number, text, boolean, enum, enum-list, text-list,
+map-enum).
+
+The component is fully controlled and validation-free. The host owns values
+(\`parameters\` + \`onChange\`) and validation (\`errors\` + \`onClearError\`): compute
+required-field errors with the exported \`getRequiredEmptyParameterIds\` helper. Individual
+parameters can be replaced via \`renderParameter\`, for example to mount a product model
+picker for a judge-model parameter.
+
+Labels, tooltips and option labels on the definitions arrive pre-resolved (host-localized).
+The component's own chrome strings localize through lingui (\`useSafeLingui\`, English
+fallback without a provider; mount \`ApI18nProvider component="canvas"\` for translations),
+overridable per string via \`labels\`.
+ `,
+ },
+ },
+ },
+ tags: ['autodocs'],
+ decorators: [
+ (Story) => (
+
+
+
+
+
+ ),
+ ],
+} satisfies Meta;
+
+export default meta;
+type Story = StoryObj;
+
+/** Stateful wrapper so every story is interactive. */
+function ControlledForm(
+ props: Omit & {
+ initialParameters?: GuardrailValidatorParameter[];
+ }
+) {
+ const { initialParameters, ...rest } = props;
+ const [parameters, setParameters] = useState(
+ initialParameters ?? seedGuardrailParameters(props.parameterDefinitions)
+ );
+ return ;
+}
+
+const PII_ENTITIES = ['Person', 'Address', 'Email', 'PhoneNumber', 'CreditCardNumber'];
+const piiDefinitions: GuardrailParameterDefinition[] = [
+ {
+ id: 'entities',
+ type: 'enum-list',
+ label: 'Entities to detect',
+ required: true,
+ defaultValue: ['Email', 'CreditCardNumber'],
+ options: PII_ENTITIES,
+ optionLabels: {
+ Person: 'Person name',
+ Address: 'Street address',
+ Email: 'Email address',
+ PhoneNumber: 'Phone number',
+ CreditCardNumber: 'Credit card number',
+ },
+ tooltip: 'Which PII entity types the validator scans for.',
+ },
+ {
+ id: 'entityThresholds',
+ type: 'map-enum',
+ label: 'Detection thresholds',
+ required: false,
+ keySource: 'entities',
+ min: 0,
+ max: 1,
+ step: 0.1,
+ defaultValue: Object.fromEntries(PII_ENTITIES.map((e) => [e, 0.5])),
+ tooltip: 'Per-entity confidence threshold. Selecting an entity adds its threshold row.',
+ },
+ {
+ id: 'redact',
+ type: 'boolean',
+ label: 'Redact detected entities',
+ required: false,
+ defaultValue: true,
+ },
+];
+
+/** Enum-list plus a dependent map-enum: toggling an entity adds or removes its threshold row. */
+export const PiiDetection: Story = {
+ args: { parameterDefinitions: piiDefinitions, parameters: [], onChange: () => {} },
+ render: (args) => ,
+};
+
+const judgeDefinitions: GuardrailParameterDefinition[] = [
+ {
+ id: 'guardrailText',
+ type: 'text',
+ label: 'Rule prompt',
+ required: true,
+ defaultValue: null,
+ maxLength: 4000,
+ tooltip: 'Natural-language rule the judge model evaluates against each message.',
+ },
+ {
+ id: 'model',
+ type: 'enum',
+ label: 'Judge model',
+ required: true,
+ defaultValue: null,
+ options: ['model-fast', 'model-accurate'],
+ optionLabels: { 'model-fast': 'Fast model', 'model-accurate': 'Accurate model' },
+ },
+ {
+ id: 'threshold',
+ type: 'number',
+ label: 'Confidence threshold',
+ required: true,
+ defaultValue: 0.7,
+ min: 0,
+ max: 1,
+ step: 0.05,
+ },
+ {
+ id: 'positiveExamples',
+ type: 'text-list',
+ label: 'Positive examples',
+ required: false,
+ defaultValue: [],
+ maxItems: 5,
+ maxLength: 1000,
+ },
+];
+
+/** The LLM-as-Judge shape: text prompt, enum model, number threshold, text-list examples. */
+export const LlmAsJudge: Story = {
+ args: { parameterDefinitions: judgeDefinitions, parameters: [], onChange: () => {} },
+ render: (args) => ,
+};
+
+/**
+ * A product can replace one parameter's editor via renderParameter, for example mounting its
+ * own model picker for the judge model while every other parameter keeps the default editor.
+ */
+export const JudgeModelSlot: Story = {
+ args: { parameterDefinitions: judgeDefinitions, parameters: [], onChange: () => {} },
+ render: (args) => (
+ {
+ if (ctx.definition.id !== 'model') return undefined;
+ return (
+
+ {ctx.definition.label}
+ ctx.onValueChange('model-from-picker')}
+ >
+ {typeof ctx.value === 'string' && ctx.value.length > 0
+ ? ctx.value
+ : 'Open product model picker'}
+
+
+ Placeholder for a product-owned picker (for example ModelPicker in Agents).
+
+
+ );
+ }}
+ />
+ ),
+};
+
+const promptInjectionDefinitions: GuardrailParameterDefinition[] = [
+ {
+ id: 'blockOnDetection',
+ type: 'boolean',
+ label: 'Block on detection',
+ required: false,
+ defaultValue: true,
+ },
+ {
+ id: 'sensitivity',
+ type: 'number',
+ label: 'Sensitivity',
+ required: true,
+ defaultValue: 0.8,
+ min: 0,
+ max: 1,
+ step: 0.1,
+ },
+];
+
+/** The simplest shape: a switch and a number. */
+export const PromptInjection: Story = {
+ args: { parameterDefinitions: promptInjectionDefinitions, parameters: [], onChange: () => {} },
+ render: (args) => ,
+};
+
+/**
+ * Validation stays host-owned: this story computes the error map with
+ * getRequiredEmptyParameterIds on save, exactly as a product form would.
+ */
+export const WithErrors: Story = {
+ args: { parameterDefinitions: judgeDefinitions, parameters: [], onChange: () => {} },
+ render: (args) => {
+ function WithErrorsExample() {
+ const [parameters, setParameters] = useState(
+ seedGuardrailParameters(args.parameterDefinitions)
+ );
+ const [errors, setErrors] = useState>({});
+ const validate = () =>
+ setErrors(
+ Object.fromEntries(
+ getRequiredEmptyParameterIds(args.parameterDefinitions, parameters).map((id) => [
+ id,
+ 'Value is required',
+ ])
+ )
+ );
+ return (
+
+ setErrors(({ [paramId]: _cleared, ...rest }) => rest)}
+ />
+
+ Save
+
+
+ );
+ }
+ return ;
+ },
+};
+
+const MANY_OPTIONS = [
+ 'USSocialSecurityNumber',
+ 'NOIdentityNumber',
+ 'UKNationalInsuranceNumber',
+ 'CAHealthNumber',
+ 'AUTaxFileNumber',
+ 'DEIdentityCardNumber',
+ 'FRSocialSecurityNumber',
+ 'ESNifNumber',
+ 'ITFiscalCode',
+ 'NLCitizenServiceNumber',
+];
+
+/** More than eight options moves the chips into a popover. */
+export const LongEnumList: Story = {
+ args: {
+ parameterDefinitions: [
+ {
+ id: 'entities',
+ type: 'enum-list',
+ label: 'National identifiers',
+ required: true,
+ defaultValue: ['USSocialSecurityNumber'],
+ options: MANY_OPTIONS,
+ },
+ ],
+ parameters: [],
+ onChange: () => {},
+ },
+ render: (args) => ,
+};
+
+/** Component chrome strings resolved from the canvas lingui catalog (Japanese). */
+export const Localized: Story = {
+ args: {
+ parameterDefinitions: judgeDefinitions,
+ parameters: [],
+ onChange: () => {},
+ },
+ render: (args) => (
+
+
+
+ ),
+};
diff --git a/packages/apollo-react/src/canvas/components/Guardrails/guardrail-validator-form.test.tsx b/packages/apollo-react/src/canvas/components/Guardrails/guardrail-validator-form.test.tsx
new file mode 100644
index 0000000000..23a80b33e6
--- /dev/null
+++ b/packages/apollo-react/src/canvas/components/Guardrails/guardrail-validator-form.test.tsx
@@ -0,0 +1,1272 @@
+import { fireEvent, render as rtlRender, screen } from '@testing-library/react';
+import { TooltipProvider } from '@uipath/apollo-wind';
+import { axe } from 'jest-axe';
+import type { ReactElement, ReactNode } from 'react';
+import { describe, expect, it, vi } from 'vitest';
+import { ApI18nProvider } from '../../../i18n';
+import { GuardrailValidatorForm } from './guardrail-validator-form';
+import type { GuardrailParameterDefinition, GuardrailValidatorParameter } from './types';
+
+// TooltipProvider is required by the parameter label's info tooltip; wrapped via `wrapper:`
+// so rerender() inherits it.
+function Providers({ children }: { children: ReactNode }) {
+ return {children} ;
+}
+function render(ui: ReactElement) {
+ return rtlRender(ui, { wrapper: Providers });
+}
+
+const REQUIRED_ERROR = 'Value is required';
+
+function requiredErrors(...ids: string[]): Record {
+ return Object.fromEntries(ids.map((id) => [id, REQUIRED_ERROR]));
+}
+
+describe('GuardrailValidatorForm', () => {
+ describe('number parameter', () => {
+ it('renders number input with label', () => {
+ const defs: GuardrailParameterDefinition[] = [
+ {
+ id: 'threshold',
+ type: 'number',
+ label: 'Detection threshold',
+ required: true,
+ defaultValue: 0.5,
+ min: 0,
+ max: 1,
+ step: 0.1,
+ },
+ ];
+
+ render(
+ {}} />
+ );
+
+ expect(screen.getByText('Detection threshold')).toBeInTheDocument();
+ expect(screen.getByRole('spinbutton')).toHaveValue(0.5);
+ });
+
+ it('uses current parameter value over default', () => {
+ const defs: GuardrailParameterDefinition[] = [
+ {
+ id: 'threshold',
+ type: 'number',
+ label: 'Threshold',
+ required: true,
+ defaultValue: 0.5,
+ min: 0,
+ max: 1,
+ step: 0.1,
+ },
+ ];
+ const params: GuardrailValidatorParameter[] = [
+ { $parameterType: 'number', id: 'threshold', value: 0.8 },
+ ];
+
+ render(
+ {}}
+ />
+ );
+
+ expect(screen.getByRole('spinbutton')).toHaveValue(0.8);
+ });
+
+ it('calls onChange with updated value', () => {
+ const onChange = vi.fn();
+ const defs: GuardrailParameterDefinition[] = [
+ {
+ id: 'threshold',
+ type: 'number',
+ label: 'Threshold',
+ required: false,
+ defaultValue: 0.5,
+ min: 0,
+ max: 1,
+ step: 0.1,
+ },
+ ];
+
+ render(
+
+ );
+
+ fireEvent.change(screen.getByRole('spinbutton'), { target: { value: '0.7' } });
+ expect(onChange).toHaveBeenCalledWith([
+ { $parameterType: 'number', id: 'threshold', value: 0.7 },
+ ]);
+ });
+
+ it('shows required indicator when required', () => {
+ const defs: GuardrailParameterDefinition[] = [
+ { id: 'threshold', type: 'number', label: 'Threshold', required: true, defaultValue: 0.5 },
+ ];
+
+ render(
+ {}} />
+ );
+ expect(screen.getByText('*')).toBeInTheDocument();
+ });
+
+ it('renders a host-supplied error message', () => {
+ const defs: GuardrailParameterDefinition[] = [
+ { id: 'threshold', type: 'number', label: 'Threshold', required: true, defaultValue: 0.5 },
+ ];
+
+ render(
+ {}}
+ errors={requiredErrors('threshold')}
+ />
+ );
+ expect(screen.getByText(REQUIRED_ERROR)).toBeInTheDocument();
+ });
+ });
+
+ describe('enum-list parameter', () => {
+ it('renders enum options as toggle buttons', () => {
+ const defs: GuardrailParameterDefinition[] = [
+ {
+ id: 'entities',
+ type: 'enum-list',
+ label: 'Entities',
+ required: true,
+ defaultValue: ['Email'],
+ options: ['Email', 'Address', 'Phone'],
+ },
+ ];
+
+ render(
+ {}} />
+ );
+
+ expect(screen.getByText('Entities')).toBeInTheDocument();
+ expect(screen.getByText('Email')).toBeInTheDocument();
+ expect(screen.getByText('Address')).toBeInTheDocument();
+ expect(screen.getByText('Phone')).toBeInTheDocument();
+ });
+
+ it('calls onChange when toggling an option on', () => {
+ const onChange = vi.fn();
+ const defs: GuardrailParameterDefinition[] = [
+ {
+ id: 'entities',
+ type: 'enum-list',
+ label: 'Entities',
+ required: true,
+ defaultValue: ['Email'],
+ options: ['Email', 'Address'],
+ },
+ ];
+
+ render(
+
+ );
+
+ fireEvent.click(screen.getByText('Address'));
+ expect(onChange).toHaveBeenCalledWith([
+ { $parameterType: 'enum-list', id: 'entities', value: ['Email', 'Address'] },
+ ]);
+ });
+
+ it('calls onChange when toggling an option off', () => {
+ const onChange = vi.fn();
+ const defs: GuardrailParameterDefinition[] = [
+ {
+ id: 'entities',
+ type: 'enum-list',
+ label: 'Entities',
+ required: true,
+ defaultValue: ['Email', 'Address'],
+ options: ['Email', 'Address'],
+ },
+ ];
+
+ render(
+
+ );
+
+ fireEvent.click(screen.getByText('Email'));
+ expect(onChange).toHaveBeenCalledWith([
+ { $parameterType: 'enum-list', id: 'entities', value: ['Address'] },
+ ]);
+ });
+
+ it('renders friendly labels when optionLabels is provided, but saves raw option values', () => {
+ const onChange = vi.fn();
+ const defs: GuardrailParameterDefinition[] = [
+ {
+ id: 'entities',
+ type: 'enum-list',
+ label: 'Entities',
+ required: true,
+ defaultValue: [],
+ options: ['USSocialSecurityNumber', 'NOIdentityNumber'],
+ optionLabels: {
+ USSocialSecurityNumber: 'US Social Security Number (SSN)',
+ NOIdentityNumber: 'Norway Identity Number',
+ },
+ },
+ ];
+
+ render(
+
+ );
+
+ // Friendly labels are visible to the user
+ expect(screen.getByText('US Social Security Number (SSN)')).toBeInTheDocument();
+ expect(screen.getByText('Norway Identity Number')).toBeInTheDocument();
+ // Raw values are not rendered
+ expect(screen.queryByText('USSocialSecurityNumber')).not.toBeInTheDocument();
+ expect(screen.queryByText('NOIdentityNumber')).not.toBeInTheDocument();
+
+ // But the saved payload still uses the raw option value
+ fireEvent.click(screen.getByText('Norway Identity Number'));
+ expect(onChange).toHaveBeenCalledWith([
+ { $parameterType: 'enum-list', id: 'entities', value: ['NOIdentityNumber'] },
+ ]);
+ });
+
+ it('falls back to the raw option value when optionLabels has no entry for it', () => {
+ const defs: GuardrailParameterDefinition[] = [
+ {
+ id: 'entities',
+ type: 'enum-list',
+ label: 'Entities',
+ required: true,
+ defaultValue: [],
+ options: ['Email', 'SomeUnmappedEntity'],
+ optionLabels: { Email: 'Email address' },
+ },
+ ];
+
+ render(
+ {}} />
+ );
+
+ expect(screen.getByText('Email address')).toBeInTheDocument();
+ expect(screen.getByText('SomeUnmappedEntity')).toBeInTheDocument();
+ });
+
+ it('renders a MultiSelect for more than eight options and saves raw values', async () => {
+ const onChange = vi.fn();
+ const manyOptions = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J'];
+ const defs: GuardrailParameterDefinition[] = [
+ {
+ id: 'entities',
+ type: 'enum-list',
+ label: 'Entities',
+ required: true,
+ defaultValue: [],
+ options: manyOptions,
+ optionLabels: { A: 'Option A' },
+ },
+ ];
+
+ render(
+
+ );
+
+ // Options live behind the closed MultiSelect popover.
+ expect(screen.queryByText('Option A')).not.toBeInTheDocument();
+
+ fireEvent.click(screen.getByRole('combobox', { name: /select options/i }));
+ fireEvent.click(await screen.findByText('Option A'));
+
+ expect(onChange).toHaveBeenCalledWith([
+ { $parameterType: 'enum-list', id: 'entities', value: ['A'] },
+ ]);
+ });
+
+ it('renders a host-supplied error message', () => {
+ const defs: GuardrailParameterDefinition[] = [
+ {
+ id: 'entities',
+ type: 'enum-list',
+ label: 'Entities',
+ required: true,
+ defaultValue: [],
+ options: ['Email'],
+ },
+ ];
+
+ render(
+ {}}
+ errors={requiredErrors('entities')}
+ />
+ );
+ expect(screen.getByText(REQUIRED_ERROR)).toBeInTheDocument();
+ });
+ });
+
+ describe('map-enum parameter', () => {
+ it('renders threshold inputs for each key from source parameter', () => {
+ const defs: GuardrailParameterDefinition[] = [
+ {
+ id: 'entities',
+ type: 'enum-list',
+ label: 'Entities',
+ required: true,
+ defaultValue: ['Email', 'Phone'],
+ options: ['Email', 'Phone'],
+ },
+ {
+ id: 'thresholds',
+ type: 'map-enum',
+ label: 'Thresholds',
+ required: true,
+ defaultValue: {},
+ keySource: 'entities',
+ min: 0,
+ max: 1,
+ step: 0.1,
+ },
+ ];
+
+ render(
+ {}} />
+ );
+
+ expect(screen.getByText('Thresholds')).toBeInTheDocument();
+ // "Email" and "Phone" appear both as enum-list chips and as map-enum threshold labels
+ expect(screen.getAllByText('Email')).toHaveLength(2);
+ expect(screen.getAllByText('Phone')).toHaveLength(2);
+ });
+
+ it('renders friendly labels for threshold rows via the source parameter optionLabels', () => {
+ const defs: GuardrailParameterDefinition[] = [
+ {
+ id: 'entities',
+ type: 'enum-list',
+ label: 'Entities',
+ required: true,
+ defaultValue: ['USSocialSecurityNumber'],
+ options: ['USSocialSecurityNumber'],
+ optionLabels: { USSocialSecurityNumber: 'US Social Security Number (SSN)' },
+ },
+ {
+ id: 'thresholds',
+ type: 'map-enum',
+ label: 'Thresholds',
+ required: true,
+ defaultValue: {},
+ keySource: 'entities',
+ min: 0,
+ max: 1,
+ step: 0.1,
+ },
+ ];
+
+ render(
+ {}} />
+ );
+
+ // Friendly label appears twice: once in the entities chip and once in the threshold row
+ expect(screen.getAllByText('US Social Security Number (SSN)')).toHaveLength(2);
+ expect(screen.queryByText('USSocialSecurityNumber')).not.toBeInTheDocument();
+ });
+
+ it('displays the per-entity default threshold for a newly selected entity not yet in the map', () => {
+ const defs: GuardrailParameterDefinition[] = [
+ {
+ id: 'entities',
+ type: 'enum-list',
+ label: 'Entities',
+ required: true,
+ defaultValue: ['Email'],
+ options: ['Email', 'Address'],
+ },
+ {
+ id: 'thresholds',
+ type: 'map-enum',
+ label: 'Thresholds',
+ required: true,
+ // Backend ships a per-entity default map; min is a distinct value to prove the
+ // display falls back to the default (0.5), not to min (0.1).
+ defaultValue: { Email: 0.5, Address: 0.5 },
+ keySource: 'entities',
+ min: 0.1,
+ max: 1,
+ step: 0.1,
+ },
+ ];
+
+ // 'Address' is selected but absent from the saved threshold map.
+ const parameters: GuardrailValidatorParameter[] = [
+ { $parameterType: 'enum-list', id: 'entities', value: ['Email', 'Address'] },
+ { $parameterType: 'map-enum', id: 'thresholds', value: { Email: 0.9 } },
+ ];
+
+ render(
+ {}}
+ />
+ );
+
+ const inputs = screen.getAllByRole('spinbutton');
+ // Email keeps its edited value; Address shows the per-entity default, not min (0.1) or 0.
+ expect(inputs.map((i) => (i as HTMLInputElement).value)).toEqual(['0.9', '0.5']);
+ });
+
+ it('renders nothing when source parameter has no keys', () => {
+ const defs: GuardrailParameterDefinition[] = [
+ {
+ id: 'entities',
+ type: 'enum-list',
+ label: 'Entities',
+ required: true,
+ defaultValue: [],
+ options: [],
+ },
+ {
+ id: 'thresholds',
+ type: 'map-enum',
+ label: 'Thresholds',
+ required: true,
+ defaultValue: {},
+ keySource: 'entities',
+ min: 0,
+ max: 1,
+ step: 0.1,
+ },
+ ];
+
+ render(
+ {}} />
+ );
+
+ expect(screen.queryByText('Thresholds')).not.toBeInTheDocument();
+ });
+ });
+
+ describe('text parameter', () => {
+ it('renders a multiline textarea with the parameter label and maxLength', () => {
+ const defs: GuardrailParameterDefinition[] = [
+ {
+ id: 'guardrailText',
+ type: 'text',
+ label: 'Rule prompt',
+ required: true,
+ defaultValue: '',
+ maxLength: 4000,
+ },
+ ];
+
+ render(
+ {}} />
+ );
+
+ expect(screen.getByText('Rule prompt')).toBeInTheDocument();
+ const textarea = screen.getByRole('textbox');
+ expect(textarea.tagName).toBe('TEXTAREA');
+ expect(textarea).toHaveAttribute('maxlength', '4000');
+ });
+
+ it('uses current parameter value over default', () => {
+ const defs: GuardrailParameterDefinition[] = [
+ {
+ id: 'guardrailText',
+ type: 'text',
+ label: 'Rule prompt',
+ required: true,
+ defaultValue: '',
+ },
+ ];
+ const params: GuardrailValidatorParameter[] = [
+ { $parameterType: 'text', id: 'guardrailText', value: 'be polite' },
+ ];
+
+ render(
+ {}}
+ />
+ );
+
+ expect(screen.getByRole('textbox')).toHaveValue('be polite');
+ });
+
+ it('falls back to empty string when defaultValue is null', () => {
+ const defs: GuardrailParameterDefinition[] = [
+ {
+ id: 'guardrailText',
+ type: 'text',
+ label: 'Rule prompt',
+ required: true,
+ defaultValue: null,
+ },
+ ];
+
+ render(
+ {}} />
+ );
+
+ expect(screen.getByRole('textbox')).toHaveValue('');
+ });
+
+ it('calls onChange with updated value', () => {
+ const onChange = vi.fn();
+ const defs: GuardrailParameterDefinition[] = [
+ {
+ id: 'guardrailText',
+ type: 'text',
+ label: 'Rule prompt',
+ required: false,
+ defaultValue: '',
+ },
+ ];
+
+ render(
+
+ );
+
+ fireEvent.change(screen.getByRole('textbox'), { target: { value: 'hi' } });
+ expect(onChange).toHaveBeenCalledWith([
+ { $parameterType: 'text', id: 'guardrailText', value: 'hi' },
+ ]);
+ });
+
+ it('renders the error message only while the errors map carries one', () => {
+ const defs: GuardrailParameterDefinition[] = [
+ {
+ id: 'guardrailText',
+ type: 'text',
+ label: 'Rule prompt',
+ required: true,
+ defaultValue: '',
+ },
+ ];
+
+ const { rerender } = render(
+ {}} />
+ );
+ expect(screen.queryByText(REQUIRED_ERROR)).not.toBeInTheDocument();
+
+ rerender(
+ {}}
+ errors={requiredErrors('guardrailText')}
+ />
+ );
+ expect(screen.getByText(REQUIRED_ERROR)).toBeInTheDocument();
+ });
+ });
+
+ describe('enum parameter', () => {
+ it('renders backend options and keeps raw values in the payload', async () => {
+ const onChange = vi.fn();
+ const defs: GuardrailParameterDefinition[] = [
+ {
+ id: 'tier',
+ type: 'enum',
+ label: 'Tier',
+ required: false,
+ defaultValue: '',
+ options: ['fast', 'slow'],
+ optionLabels: { fast: 'Fast tier', slow: 'Slow tier' },
+ },
+ ];
+
+ render(
+
+ );
+
+ fireEvent.click(screen.getByRole('combobox'));
+ expect(await screen.findByText('Fast tier')).toBeInTheDocument();
+ expect(screen.queryByRole('option', { name: 'fast' })).not.toBeInTheDocument();
+ });
+
+ it('lists every backend option', async () => {
+ const defs: GuardrailParameterDefinition[] = [
+ {
+ id: 'tier',
+ type: 'enum',
+ label: 'Tier',
+ required: false,
+ defaultValue: 'a',
+ options: ['a', 'b'],
+ },
+ ];
+
+ render(
+ {}} />
+ );
+
+ fireEvent.click(screen.getByRole('combobox'));
+ expect(await screen.findByRole('option', { name: 'a' })).toBeInTheDocument();
+ expect(screen.getByRole('option', { name: 'b' })).toBeInTheDocument();
+ });
+
+ it('keeps a stored value absent from the options as a synthetic option', async () => {
+ const defs: GuardrailParameterDefinition[] = [
+ {
+ id: 'tier',
+ type: 'enum',
+ label: 'Tier',
+ required: false,
+ defaultValue: '',
+ options: ['a', 'b'],
+ },
+ ];
+ const params: GuardrailValidatorParameter[] = [
+ { $parameterType: 'enum', id: 'tier', value: 'legacy-option' },
+ ];
+
+ render(
+ {}}
+ />
+ );
+
+ // The stale stored value is still shown in the trigger instead of blanking the field.
+ expect(screen.getByRole('combobox')).toHaveTextContent('legacy-option');
+ fireEvent.click(screen.getByRole('combobox'));
+ expect(await screen.findByRole('option', { name: 'legacy-option' })).toBeInTheDocument();
+ });
+
+ it('renders the error message only while the errors map carries one', () => {
+ const defs: GuardrailParameterDefinition[] = [
+ {
+ id: 'tier',
+ type: 'enum',
+ label: 'Tier',
+ required: true,
+ defaultValue: null,
+ options: ['a'],
+ },
+ ];
+
+ const { rerender } = render(
+ {}} />
+ );
+ expect(screen.queryByText(REQUIRED_ERROR)).not.toBeInTheDocument();
+
+ rerender(
+ {}}
+ errors={requiredErrors('tier')}
+ />
+ );
+ expect(screen.getByText(REQUIRED_ERROR)).toBeInTheDocument();
+ });
+ });
+
+ describe('text-list parameter', () => {
+ const listDef = (
+ overrides?: Partial
+ ): GuardrailParameterDefinition => ({
+ id: 'positiveExamples',
+ type: 'text-list',
+ label: 'Positive examples',
+ required: false,
+ defaultValue: [],
+ maxItems: 3,
+ ...overrides,
+ });
+
+ it('renders one row per item', () => {
+ const defs = [listDef({ maxLength: 1000 })];
+ const params: GuardrailValidatorParameter[] = [
+ { $parameterType: 'text-list', id: 'positiveExamples', value: ['one', 'two'] },
+ ];
+
+ render(
+ {}}
+ />
+ );
+
+ expect(screen.getByDisplayValue('one')).toBeInTheDocument();
+ expect(screen.getByDisplayValue('two')).toBeInTheDocument();
+ });
+
+ it('clicking Add appends an empty entry', () => {
+ const onChange = vi.fn();
+
+ render(
+
+ );
+
+ fireEvent.click(screen.getByRole('button', { name: 'Add' }));
+ expect(onChange).toHaveBeenCalledWith([
+ { $parameterType: 'text-list', id: 'positiveExamples', value: [''] },
+ ]);
+ });
+
+ it('hides the Add button when at maxItems', () => {
+ const params: GuardrailValidatorParameter[] = [
+ { $parameterType: 'text-list', id: 'positiveExamples', value: ['a', 'b'] },
+ ];
+
+ render(
+ {}}
+ />
+ );
+
+ expect(screen.queryByRole('button', { name: 'Add' })).not.toBeInTheDocument();
+ });
+
+ it('shows the Add button when below maxItems', () => {
+ const params: GuardrailValidatorParameter[] = [
+ { $parameterType: 'text-list', id: 'positiveExamples', value: ['a'] },
+ ];
+
+ render(
+ {}}
+ />
+ );
+
+ expect(screen.getByRole('button', { name: 'Add' })).toBeInTheDocument();
+ });
+
+ it('clicking Remove drops that index from the value', () => {
+ const onChange = vi.fn();
+ const params: GuardrailValidatorParameter[] = [
+ { $parameterType: 'text-list', id: 'positiveExamples', value: ['a', 'b', 'c'] },
+ ];
+
+ render(
+
+ );
+
+ fireEvent.click(screen.getByRole('button', { name: 'Remove Positive examples 2' }));
+ expect(onChange).toHaveBeenCalledWith([
+ { $parameterType: 'text-list', id: 'positiveExamples', value: ['a', 'c'] },
+ ]);
+ });
+
+ it('editing a single row preserves the other rows', () => {
+ const onChange = vi.fn();
+ const params: GuardrailValidatorParameter[] = [
+ { $parameterType: 'text-list', id: 'positiveExamples', value: ['one', 'two'] },
+ ];
+
+ render(
+
+ );
+
+ fireEvent.change(screen.getByDisplayValue('two'), { target: { value: 'TWO' } });
+ expect(onChange).toHaveBeenCalledWith([
+ { $parameterType: 'text-list', id: 'positiveExamples', value: ['one', 'TWO'] },
+ ]);
+ });
+
+ it('honors per-row maxLength on the textarea', () => {
+ const params: GuardrailValidatorParameter[] = [
+ { $parameterType: 'text-list', id: 'positiveExamples', value: ['x'] },
+ ];
+
+ render(
+ {}}
+ />
+ );
+
+ expect(screen.getByDisplayValue('x')).toHaveAttribute('maxlength', '1000');
+ });
+
+ it('renders a host-supplied error message', () => {
+ render(
+ {}}
+ errors={requiredErrors('positiveExamples')}
+ />
+ );
+
+ expect(screen.getByText(REQUIRED_ERROR)).toBeInTheDocument();
+ });
+ });
+
+ describe('boolean parameter', () => {
+ it('toggles and reports the new value', () => {
+ const onChange = vi.fn();
+ const defs: GuardrailParameterDefinition[] = [
+ {
+ id: 'strict',
+ type: 'boolean',
+ label: 'Strict mode',
+ required: false,
+ defaultValue: false,
+ },
+ ];
+
+ render(
+
+ );
+
+ fireEvent.click(screen.getByRole('switch'));
+ expect(onChange).toHaveBeenCalledWith([
+ { $parameterType: 'boolean', id: 'strict', value: true },
+ ]);
+ });
+
+ it('renders a host-supplied error message', () => {
+ const defs: GuardrailParameterDefinition[] = [
+ { id: 'strict', type: 'boolean', label: 'Strict mode', required: true, defaultValue: null },
+ ];
+
+ render(
+ {}}
+ errors={requiredErrors('strict')}
+ />
+ );
+ expect(screen.getByText(REQUIRED_ERROR)).toBeInTheDocument();
+ });
+ });
+
+ describe('parameter label', () => {
+ it('renders the info-tooltip trigger when the definition has a tooltip', () => {
+ const defs: GuardrailParameterDefinition[] = [
+ {
+ id: 'guardrailText',
+ type: 'text',
+ label: 'Rule prompt',
+ required: true,
+ defaultValue: '',
+ tooltip: 'Some helpful explanation.',
+ },
+ ];
+
+ render(
+ {}} />
+ );
+
+ expect(screen.getByLabelText('More information')).toBeInTheDocument();
+ });
+
+ it('does not render the info-tooltip trigger without a tooltip', () => {
+ const defs: GuardrailParameterDefinition[] = [
+ {
+ id: 'guardrailText',
+ type: 'text',
+ label: 'Rule prompt',
+ required: true,
+ defaultValue: '',
+ },
+ ];
+
+ render(
+ {}} />
+ );
+
+ expect(screen.queryByLabelText('More information')).not.toBeInTheDocument();
+ });
+ });
+
+ describe('updates existing parameters', () => {
+ it('replaces existing parameter value on change', () => {
+ const onChange = vi.fn();
+ const defs: GuardrailParameterDefinition[] = [
+ {
+ id: 'threshold',
+ type: 'number',
+ label: 'Threshold',
+ required: false,
+ defaultValue: 0.5,
+ min: 0,
+ max: 1,
+ step: 0.1,
+ },
+ ];
+ const existingParams: GuardrailValidatorParameter[] = [
+ { $parameterType: 'number', id: 'threshold', value: 0.3 },
+ ];
+
+ render(
+
+ );
+
+ fireEvent.change(screen.getByRole('spinbutton'), { target: { value: '0.9' } });
+ expect(onChange).toHaveBeenCalledWith([
+ { $parameterType: 'number', id: 'threshold', value: 0.9 },
+ ]);
+ });
+ });
+
+ describe('onClearError', () => {
+ it('fires with the parameter id before onChange on any edit of that parameter', () => {
+ const calls: string[] = [];
+ const onClearError = vi.fn(() => calls.push('clear'));
+ const onChange = vi.fn(() => calls.push('change'));
+ const defs: GuardrailParameterDefinition[] = [
+ {
+ id: 'guardrailText',
+ type: 'text',
+ label: 'Rule prompt',
+ required: true,
+ defaultValue: '',
+ },
+ ];
+
+ render(
+
+ );
+
+ fireEvent.change(screen.getByRole('textbox'), { target: { value: 'hi' } });
+ expect(onClearError).toHaveBeenCalledWith('guardrailText');
+ expect(calls).toEqual(['clear', 'change']);
+ });
+ });
+
+ describe('renderParameter override', () => {
+ const defs: GuardrailParameterDefinition[] = [
+ {
+ id: 'model',
+ type: 'enum',
+ label: 'Judge model',
+ required: true,
+ defaultValue: null,
+ options: [],
+ },
+ { id: 'threshold', type: 'number', label: 'Threshold', required: false, defaultValue: 0.5 },
+ ];
+
+ it('replaces the matched parameter and falls through for the rest', () => {
+ render(
+ {}}
+ renderParameter={(ctx) =>
+ ctx.definition.id === 'model' ? (
+ picker
+ ) : undefined
+ }
+ />
+ );
+
+ expect(screen.getByTestId('custom-picker')).toBeInTheDocument();
+ // The default editor for the overridden param is gone, the sibling stays.
+ expect(screen.queryByRole('combobox')).not.toBeInTheDocument();
+ expect(screen.getByRole('spinbutton')).toBeInTheDocument();
+ });
+
+ it('onValueChange upserts the overridden parameter', () => {
+ const onChange = vi.fn();
+ render(
+
+ ctx.definition.id === 'model' ? (
+ ctx.onValueChange('model-a')}>
+ pick
+
+ ) : undefined
+ }
+ />
+ );
+
+ fireEvent.click(screen.getByRole('button', { name: 'pick' }));
+ expect(onChange).toHaveBeenCalledWith([
+ { $parameterType: 'enum', id: 'model', value: 'model-a' },
+ ]);
+ });
+
+ it('onParametersChange can write sidecar parameters alongside the overridden one', () => {
+ const onChange = vi.fn();
+ render(
+
+ ctx.definition.id === 'model' ? (
+
+ ctx.onParametersChange([
+ { $parameterType: 'enum', id: 'model', value: 'byo-model' },
+ { $parameterType: 'text', id: 'byomConnectionId', value: 'conn-1' },
+ ])
+ }
+ >
+ pick byo
+
+ ) : undefined
+ }
+ />
+ );
+
+ fireEvent.click(screen.getByRole('button', { name: 'pick byo' }));
+ expect(onChange).toHaveBeenCalledWith([
+ { $parameterType: 'enum', id: 'model', value: 'byo-model' },
+ { $parameterType: 'text', id: 'byomConnectionId', value: 'conn-1' },
+ ]);
+ });
+ });
+
+ describe('localization', () => {
+ it('overrides individual strings via the labels prop', () => {
+ const defs: GuardrailParameterDefinition[] = [
+ {
+ id: 'positiveExamples',
+ type: 'text-list',
+ label: 'Positive examples',
+ required: false,
+ defaultValue: [],
+ },
+ ];
+
+ render(
+ {}}
+ labels={{ addItem: 'Hinzufügen' }}
+ />
+ );
+
+ expect(screen.getByRole('button', { name: 'Hinzufügen' })).toBeInTheDocument();
+ expect(screen.queryByRole('button', { name: 'Add' })).not.toBeInTheDocument();
+ });
+
+ it('resolves the component strings from the ambient lingui catalog', async () => {
+ const defs: GuardrailParameterDefinition[] = [
+ {
+ id: 'positiveExamples',
+ type: 'text-list',
+ label: 'Positive examples',
+ required: false,
+ defaultValue: [],
+ },
+ ];
+
+ render(
+
+ {}} />
+
+ );
+
+ expect(await screen.findByRole('button', { name: '追加' })).toBeInTheDocument();
+ });
+ });
+
+ describe('accessibility', () => {
+ it('has no accessibility violations on a kitchen-sink definition', async () => {
+ const defs: GuardrailParameterDefinition[] = [
+ { id: 'prompt', type: 'text', label: 'Prompt', required: true, defaultValue: 'check tone' },
+ {
+ id: 'model',
+ type: 'enum',
+ label: 'Model',
+ required: true,
+ defaultValue: 'a',
+ options: ['a', 'b'],
+ },
+ { id: 'threshold', type: 'number', label: 'Threshold', required: false, defaultValue: 0.5 },
+ {
+ id: 'strict',
+ type: 'boolean',
+ label: 'Strict mode',
+ required: false,
+ defaultValue: true,
+ },
+ {
+ id: 'entities',
+ type: 'enum-list',
+ label: 'Entities',
+ required: true,
+ defaultValue: ['Email'],
+ options: ['Email', 'Phone'],
+ },
+ {
+ id: 'thresholds',
+ type: 'map-enum',
+ label: 'Entity thresholds',
+ required: false,
+ defaultValue: { Email: 0.5, Phone: 0.5 },
+ keySource: 'entities',
+ min: 0,
+ max: 1,
+ step: 0.1,
+ },
+ {
+ id: 'examples',
+ type: 'text-list',
+ label: 'Examples',
+ required: false,
+ defaultValue: ['sample'],
+ maxItems: 3,
+ },
+ ];
+
+ const { container } = render(
+ {}} />
+ );
+
+ const results = await axe(container);
+ expect(results).toHaveNoViolations();
+ });
+ });
+});
+
+// ============================================================================
+// Controlled contract under the MetadataForm internals
+// ============================================================================
+
+describe('controlled contract (MetadataForm internals)', () => {
+ const textDef: GuardrailParameterDefinition = {
+ id: 'prompt',
+ type: 'text',
+ label: 'Prompt',
+ required: true,
+ defaultValue: '',
+ };
+
+ it('a synchronous host echo is a no-op: no re-emission, focus and value survive', () => {
+ const onChange = vi.fn();
+
+ const { rerender } = render(
+
+ );
+
+ const textarea = screen.getByLabelText(/Prompt/);
+ textarea.focus();
+ fireEvent.change(textarea, { target: { value: 'hello' } });
+ expect(onChange).toHaveBeenCalledTimes(1);
+ const emitted = onChange.mock.calls[0][0] as GuardrailValidatorParameter[];
+ expect(emitted).toEqual([{ $parameterType: 'text', id: 'prompt', value: 'hello' }]);
+
+ // Host echoes the emitted array back, as controlled hosts do on every keystroke.
+ rerender(
+
+ );
+
+ expect(onChange).toHaveBeenCalledTimes(1);
+ expect(textarea).toHaveFocus();
+ expect(textarea).toHaveValue('hello');
+ });
+
+ it('an external (non-echo) parameters change updates the rendered value', () => {
+ const { rerender } = render(
+ {}}
+ />
+ );
+ expect(screen.getByLabelText(/Prompt/)).toHaveValue('one');
+
+ rerender(
+ {}}
+ />
+ );
+ expect(screen.getByLabelText(/Prompt/)).toHaveValue('two');
+ });
+
+ it('sidecar parameters without a definition ride along untouched through later edits', () => {
+ const onChange = vi.fn();
+ const defs: GuardrailParameterDefinition[] = [
+ {
+ id: 'model',
+ type: 'enum',
+ label: 'Model',
+ required: true,
+ defaultValue: 'a',
+ options: ['a', 'b'],
+ },
+ textDef,
+ ];
+ // byomConnectionId has no definition — it was written by a renderParameter override.
+ const parameters: GuardrailValidatorParameter[] = [
+ { $parameterType: 'enum', id: 'model', value: 'byo-model' },
+ { $parameterType: 'text', id: 'byomConnectionId', value: 'conn-1' },
+ ];
+
+ render(
+
+ );
+
+ fireEvent.change(screen.getByLabelText(/Prompt/), { target: { value: 'edited' } });
+ expect(onChange).toHaveBeenCalledWith([
+ { $parameterType: 'enum', id: 'model', value: 'byo-model' },
+ { $parameterType: 'text', id: 'byomConnectionId', value: 'conn-1' },
+ { $parameterType: 'text', id: 'prompt', value: 'edited' },
+ ]);
+ });
+
+ it('clearing a number input emits 0, never NaN', () => {
+ const onChange = vi.fn();
+ const defs: GuardrailParameterDefinition[] = [
+ { id: 'threshold', type: 'number', label: 'Threshold', required: true, defaultValue: 0.5 },
+ ];
+
+ render(
+
+ );
+
+ fireEvent.change(screen.getByRole('spinbutton'), { target: { value: '' } });
+ expect(onChange).toHaveBeenCalledWith([
+ { $parameterType: 'number', id: 'threshold', value: 0 },
+ ]);
+ });
+});
diff --git a/packages/apollo-react/src/canvas/components/Guardrails/guardrail-validator-form.tsx b/packages/apollo-react/src/canvas/components/Guardrails/guardrail-validator-form.tsx
new file mode 100644
index 0000000000..2354c8c7cf
--- /dev/null
+++ b/packages/apollo-react/src/canvas/components/Guardrails/guardrail-validator-form.tsx
@@ -0,0 +1,191 @@
+import { cn, MetadataForm } from '@uipath/apollo-wind';
+import { forwardRef, useCallback, useMemo, useRef, useState } from 'react';
+import { EnumListChipsField } from './components/enum-list-chips-field';
+import { MapEnumField } from './components/map-enum-field';
+import {
+ buildGuardrailFormSchema,
+ coerceGuardrailParameterValue,
+ GUARDRAIL_ENUM_LIST_CHIPS_COMPONENT,
+ GUARDRAIL_MAP_ENUM_COMPONENT,
+ GUARDRAIL_RENDER_PARAMETER_COMPONENT,
+} from './form-schema-builder';
+import { useGuardrailFormLabels } from './i18n';
+import { GuardrailRenderParameterProvider, RenderParameterBridge } from './render-parameter-bridge';
+import type {
+ GuardrailParameterDefinition,
+ GuardrailValidatorFormProps,
+ GuardrailValidatorParameter,
+} from './types';
+
+const GUARDRAIL_CUSTOM_COMPONENTS = {
+ [GUARDRAIL_ENUM_LIST_CHIPS_COMPONENT]: EnumListChipsField,
+ [GUARDRAIL_MAP_ENUM_COMPONENT]: MapEnumField,
+ [GUARDRAIL_RENDER_PARAMETER_COMPONENT]: RenderParameterBridge,
+};
+
+const EMPTY_OVERRIDES: ReadonlySet = new Set();
+
+/**
+ * Renders the configuration section of an OOTB guardrail validator: one editor per parameter
+ * definition, covering all seven parameter types (`number`, `text`, `boolean`, `enum`,
+ * `enum-list`, `text-list`, `map-enum`).
+ *
+ * Internally this is `buildGuardrailFormSchema` + the forms/ `MetadataForm` stack (see
+ * README, "Built on the forms/ MetadataForm stack"): five parameter types map onto
+ * first-class field types, while the chip-style enum-list, `map-enum`, and `renderParameter`
+ * overrides register as custom field components. The public contract is unchanged — fully
+ * controlled and validation-free: the host owns values (`parameters` + `onChange`, echoed
+ * back synchronously) and validation (`errors` + `onClearError`); compute required-field
+ * errors with `getRequiredEmptyParameterIds`. Parameters the host's `parameters` array
+ * carries without a matching definition (sidecars written via `onParametersChange`, e.g. a
+ * model picker's connection metadata) never enter the form and round-trip untouched.
+ */
+export const GuardrailValidatorForm = forwardRef(
+ (
+ {
+ parameterDefinitions,
+ parameters,
+ onChange,
+ errors,
+ onClearError,
+ renderParameter,
+ labels: labelOverrides,
+ className,
+ },
+ ref
+ ) => {
+ const labels = useGuardrailFormLabels(labelOverrides);
+
+ const defsById = useMemo(
+ () => new Map(parameterDefinitions.map((d) => [d.id, d])),
+ [parameterDefinitions]
+ );
+
+ // Host truth, read at emit time — the carrier for sidecar parameters and the base every
+ // upsert starts from, so untouched defaults never leak into the emitted array.
+ const parametersRef = useRef(parameters);
+ parametersRef.current = parameters;
+
+ const updateParam = useCallback(
+ (paramDef: GuardrailParameterDefinition, value: unknown) => {
+ onClearError?.(paramDef.id);
+ const current = parametersRef.current;
+ const existing = current.findIndex((p) => p.id === paramDef.id);
+ const newParam = {
+ $parameterType: paramDef.type,
+ id: paramDef.id,
+ value,
+ } as GuardrailValidatorParameter;
+ const newParams = [...current];
+ if (existing >= 0) {
+ newParams[existing] = newParam;
+ } else {
+ newParams.push(newParam);
+ }
+ onChange(newParams);
+ },
+ [onChange, onClearError]
+ );
+
+ const replaceParams = useCallback(
+ (paramId: string, next: GuardrailValidatorParameter[]) => {
+ onClearError?.(paramId);
+ onChange(next);
+ },
+ [onChange, onClearError]
+ );
+
+ // Which definitions the override claims — probed the way the pre-convergence form probed
+ // on every render: call it and check for undefined-fallthrough. Overrides are expected to
+ // discriminate on the definition (id/type), not on transient values.
+ const overriddenIds = useMemo(() => {
+ if (!renderParameter) return EMPTY_OVERRIDES;
+ const ids = new Set();
+ for (const def of parameterDefinitions) {
+ const entry = parameters.find((p) => p.id === def.id);
+ const node = renderParameter({
+ definition: def,
+ value: entry?.value,
+ error: errors?.[def.id],
+ parameters,
+ onValueChange: () => {},
+ onParametersChange: () => {},
+ });
+ if (node !== undefined) ids.add(def.id);
+ }
+ return ids;
+ }, [renderParameter, parameterDefinitions, parameters, errors]);
+
+ // A stored enum value missing from the option catalog stays visible as a synthetic option.
+ const enumSyntheticValues = useMemo(() => {
+ const map: Record = {};
+ for (const def of parameterDefinitions) {
+ if (def.type !== 'enum') continue;
+ const entry = parameters.find((p) => p.id === def.id);
+ const value =
+ entry?.$parameterType === 'enum'
+ ? entry.value
+ : ((def.defaultValue as string | null) ?? '');
+ if (value.length > 0 && !(def.options ?? []).includes(value)) {
+ map[def.id] = value;
+ }
+ }
+ return map;
+ }, [parameterDefinitions, parameters]);
+
+ // Frozen at mount so keystrokes never churn schema identity: it only seeds field
+ // defaultValues for the first paint. Later external changes arrive via `values`.
+ const [initialParameters] = useState(parameters);
+
+ const schema = useMemo(
+ () =>
+ buildGuardrailFormSchema(parameterDefinitions, labels, {
+ overriddenIds,
+ initialParameters,
+ enumSyntheticValues,
+ }),
+ [parameterDefinitions, labels, overriddenIds, initialParameters, enumSyntheticValues]
+ );
+
+ // parameters -> record: only ids with a definition enter the form; sidecars stay in
+ // parametersRef and round-trip untouched.
+ const values = useMemo(() => {
+ const record: Record = {};
+ for (const p of parameters) {
+ if (defsById.has(p.id)) record[p.id] = p.value;
+ }
+ return record;
+ }, [parameters, defsById]);
+
+ const handleValuesChange = useCallback(
+ (record: Record, changedField: string) => {
+ const def = defsById.get(changedField);
+ if (!def) return;
+ updateParam(def, coerceGuardrailParameterValue(record[changedField], def));
+ },
+ [defsById, updateParam]
+ );
+
+ const bridgeContext = useMemo(
+ () => ({ renderParameter, defsById, parameters, errors, updateParam, replaceParams }),
+ [renderParameter, defsById, parameters, errors, updateParam, replaceParams]
+ );
+
+ return (
+
+
+
+
+
+ );
+ }
+);
+GuardrailValidatorForm.displayName = 'GuardrailValidatorForm';
diff --git a/packages/apollo-react/src/canvas/components/Guardrails/i18n.test.ts b/packages/apollo-react/src/canvas/components/Guardrails/i18n.test.ts
new file mode 100644
index 0000000000..4634c874bf
--- /dev/null
+++ b/packages/apollo-react/src/canvas/components/Guardrails/i18n.test.ts
@@ -0,0 +1,162 @@
+import { readFileSync } from 'node:fs';
+import { dirname, join } from 'node:path';
+import { fileURLToPath } from 'node:url';
+import { describe, expect, it } from 'vitest';
+import {
+ formatGuardrailFormMessage,
+ GUARDRAIL_LIST_EN_LABELS,
+ GUARDRAIL_LIST_EN_MESSAGES,
+ GUARDRAIL_PALETTE_EN_LABELS,
+ GUARDRAIL_PALETTE_EN_MESSAGES,
+ resolveGuardrailListLabels,
+ resolveGuardrailPaletteLabels,
+} from './i18n';
+
+describe('resolveGuardrailListLabels', () => {
+ it('returns the English defaults when there is nothing to merge', () => {
+ expect(resolveGuardrailListLabels()).toEqual(GUARDRAIL_LIST_EN_LABELS);
+ });
+
+ it('layers the catalog over the defaults and the overrides over both', () => {
+ const labels = resolveGuardrailListLabels(
+ { title: 'Absicherungen', add: 'Hinzufügen' },
+ { add: 'Neu' }
+ );
+
+ expect(labels.title).toBe('Absicherungen');
+ expect(labels.add).toBe('Neu');
+ expect(labels.empty).toBe(GUARDRAIL_LIST_EN_LABELS.empty);
+ });
+
+ it('never lets an absent string blank a default', () => {
+ const labels = resolveGuardrailListLabels({ title: undefined }, { add: undefined });
+
+ expect(labels.title).toBe('Guardrails');
+ expect(labels.add).toBe('Add');
+ });
+});
+
+describe('GUARDRAIL_LIST_EN_LABELS', () => {
+ it('carries the template convention the row interpolates with', () => {
+ expect(GUARDRAIL_LIST_EN_LABELS.reorderItem).toBe('Reorder guardrail {{name}}');
+ expect(
+ formatGuardrailFormMessage(GUARDRAIL_LIST_EN_LABELS.editRow, { name: 'PII detection 1' })
+ ).toBe('Edit PII detection 1');
+ });
+});
+
+describe('the shared canvas catalog', () => {
+ // `src/canvas` uses no lingui macros, so `lingui extract` does not feed this catalog: the
+ // entries are hand-authored. This test is what `extract` would otherwise be doing, and it is
+ // the only thing standing between a new list string and an untranslatable one.
+ const catalog: Record = JSON.parse(
+ readFileSync(join(dirname(fileURLToPath(import.meta.url)), '../../locales/en.json'), 'utf8')
+ );
+
+ it('carries every list message with the same English', () => {
+ const missing: string[] = [];
+ const drifted: string[] = [];
+ for (const [id, message] of Object.entries(GUARDRAIL_LIST_EN_MESSAGES)) {
+ if (!(id in catalog)) missing.push(id);
+ else if (catalog[id] !== message)
+ drifted.push(`${id}\n src: ${message}\n en: ${catalog[id]}`);
+ }
+
+ expect({ missing, drifted }).toEqual({ missing: [], drifted: [] });
+ });
+
+ it('carries no list message the source no longer declares', () => {
+ const orphans = Object.keys(catalog)
+ .filter((id) => id.startsWith('guardrails.list.'))
+ .filter((id) => !(id in GUARDRAIL_LIST_EN_MESSAGES));
+
+ expect(orphans).toEqual([]);
+ });
+});
+
+describe('resolveGuardrailPaletteLabels', () => {
+ it('returns the English defaults when there is nothing to merge', () => {
+ expect(resolveGuardrailPaletteLabels()).toEqual(GUARDRAIL_PALETTE_EN_LABELS);
+ });
+
+ it('layers the catalog over the defaults and the overrides over both', () => {
+ const labels = resolveGuardrailPaletteLabels(
+ { empty: 'Keine Leitplanken verfügbar', preview: 'Vorschau' },
+ { preview: 'Beta' }
+ );
+
+ expect(labels.empty).toBe('Keine Leitplanken verfügbar');
+ expect(labels.preview).toBe('Beta');
+ expect(labels.loading).toBe(GUARDRAIL_PALETTE_EN_LABELS.loading);
+ });
+
+ it('never lets an absent string blank a default', () => {
+ const labels = resolveGuardrailPaletteLabels({ empty: undefined }, { preview: undefined });
+
+ expect(labels.empty).toBe('No guardrails available');
+ expect(labels.preview).toBe('Preview');
+ });
+});
+
+describe('the shared canvas catalog', () => {
+ // `src/canvas` uses no lingui macros, so `lingui extract` does not feed this catalog: the
+ // entries are hand-authored. This test is what `extract` would otherwise be doing, and it is
+ // the only thing standing between a new palette string and an untranslatable one.
+ const localesDir = join(dirname(fileURLToPath(import.meta.url)), '../../locales');
+ const readCatalog = (locale: string): Record =>
+ JSON.parse(readFileSync(join(localesDir, `${locale}.json`), 'utf8'));
+ const catalog = readCatalog('en');
+ const TRANSLATED_LOCALES = [
+ 'de',
+ 'es',
+ 'es-MX',
+ 'fr',
+ 'ja',
+ 'ko',
+ 'pt',
+ 'pt-BR',
+ 'ro',
+ 'tr',
+ 'zh-CN',
+ 'zh-TW',
+ ];
+
+ it('carries every palette message with the same English', () => {
+ const missing: string[] = [];
+ const drifted: string[] = [];
+ for (const [id, message] of Object.entries(GUARDRAIL_PALETTE_EN_MESSAGES)) {
+ if (!(id in catalog)) missing.push(id);
+ else if (catalog[id] !== message)
+ drifted.push(`${id}\n src: ${message}\n en: ${catalog[id]}`);
+ }
+
+ expect({ missing, drifted }).toEqual({ missing: [], drifted: [] });
+ });
+
+ it('carries no palette message the source no longer declares', () => {
+ // Every locale, not just English: a renamed id otherwise leaves twelve dead translations
+ // behind, and the harvest that produced them is a one-off script, not a pipeline.
+ const orphans = ['en', ...TRANSLATED_LOCALES].flatMap((locale) =>
+ Object.keys(readCatalog(locale))
+ .filter((id) => id.startsWith('guardrails.palette.'))
+ .filter((id) => !(id in GUARDRAIL_PALETTE_EN_MESSAGES))
+ .map((id) => `${locale}: ${id}`)
+ );
+
+ expect(orphans).toEqual([]);
+ });
+
+ it('translates every palette message in all twelve translated locales', () => {
+ // The palette's nine strings were harvested 1:1 from the two products' own catalogs, so
+ // full coverage is the shipped state; `ru` is deliberately absent (both products ship it
+ // empty, and apollo-react's `ru` falls back to English by convention).
+ const gaps = TRANSLATED_LOCALES.flatMap((locale) => {
+ const messages = readCatalog(locale);
+ return Object.keys(GUARDRAIL_PALETTE_EN_MESSAGES)
+ .filter((id) => !messages[id])
+ .map((id) => `${locale}: ${id}`);
+ });
+
+ expect(gaps).toEqual([]);
+ });
+});
diff --git a/packages/apollo-react/src/canvas/components/Guardrails/i18n.ts b/packages/apollo-react/src/canvas/components/Guardrails/i18n.ts
new file mode 100644
index 0000000000..d90c29e68f
--- /dev/null
+++ b/packages/apollo-react/src/canvas/components/Guardrails/i18n.ts
@@ -0,0 +1,674 @@
+import { useMemo } from 'react';
+import { useSafeLingui } from '../../../i18n';
+
+/**
+ * The component's own chrome strings. Domain strings (parameter labels, tooltips, option
+ * labels) are NOT localized here — they arrive pre-resolved on `GuardrailParameterDefinition`.
+ *
+ * Values may contain `{{placeholder}}` tokens; interpolate with `formatGuardrailFormMessage`.
+ * The required-error message is deliberately absent: validation policy and its messages are
+ * host-owned and arrive through the `errors` prop.
+ */
+export interface GuardrailValidatorFormLabels {
+ /** Aria label of the per-parameter info-tooltip trigger. */
+ moreInformation: string;
+ /** Placeholder of the single-select (enum) trigger. */
+ enumPlaceholder: string;
+ /** Placeholder of the multi-select (enum-list) popover trigger. */
+ enumListPlaceholder: string;
+ /** Label of the text-list "Add" button. */
+ addItem: string;
+ /** Aria-label template of a text-list row's remove button: `{{label}}`, `{{position}}`. */
+ removeItem: string;
+}
+
+export const GUARDRAIL_FORM_EN_LABELS: GuardrailValidatorFormLabels = {
+ moreInformation: 'More information',
+ enumPlaceholder: 'Select...',
+ enumListPlaceholder: 'Select options...',
+ addItem: 'Add',
+ removeItem: 'Remove {{label}} {{position}}',
+};
+
+/**
+ * Chrome strings of the guardrail builder screen (labels, placeholders, buttons, banners,
+ * and the builder's own validation messages — it gates its own Save, so the messages ship
+ * with it; hosts override per string via `labels` or per field via `errors`). Domain strings
+ * (`definition.displayName`, `usageNote`, `otherAppliedScopes` labels) stay pre-resolved.
+ */
+export interface GuardrailBuilderLabels {
+ /** Dialog/header title templates; `{{name}}` is the definition display name. */
+ editTitle: string;
+ addTitle: string;
+ typeLabel: string;
+ nameLabel: string;
+ namePlaceholder: string;
+ descriptionLabel: string;
+ descriptionPlaceholder: string;
+ evalsLabel: string;
+ evalsInfoAriaLabel: string;
+ evalsTooltip: string;
+ saveAsNew: string;
+ cancel: string;
+ save: string;
+ // Status banners
+ byoDisabledMessage: string;
+ unauthorizedMessage: string;
+ featureDisabledMessage: string;
+ // Scope selector
+ scopesLabel: string;
+ toolsLabel: string;
+ scopeAgentLabel: string;
+ scopeLlmLabel: string;
+ scopeToolLabel: string;
+ // Action section
+ actionTypeLabel: string;
+ actionLogLabel: string;
+ actionBlockLabel: string;
+ actionFilterLabel: string;
+ actionEscalateLabel: string;
+ severityLabel: string;
+ severityInfoLabel: string;
+ severityWarningLabel: string;
+ severityErrorLabel: string;
+ blockReasonLabel: string;
+ blockReasonPlaceholder: string;
+ // Escalation
+ assignToLabel: string;
+ recipientUserLabel: string;
+ recipientGroupLabel: string;
+ recipientEmailLabel: string;
+ recipientGroupNameLabel: string;
+ recipientFallbackLabel: string;
+ userSearchPlaceholder: string;
+ groupSearchPlaceholder: string;
+ emailPlaceholder: string;
+ groupNamePlaceholder: string;
+ actionAppLabel: string;
+ appPickerUnavailable: string;
+ // Mixed scopes banner
+ mixedScopesAlsoApplied: string;
+ mixedScopesSaveAsNewHint: string;
+ // Validation messages
+ nameRequiredError: string;
+ nameDuplicateError: string;
+ parameterRequiredError: string;
+ scopesRequiredError: string;
+ toolsRequiredError: string;
+ blockReasonRequiredError: string;
+ filterFieldsRequiredError: string;
+ recipientRequiredError: string;
+ actionAppRequiredError: string;
+}
+
+export const GUARDRAIL_BUILDER_EN_LABELS: GuardrailBuilderLabels = {
+ editTitle: 'Edit {{name}} guardrail',
+ addTitle: 'Add {{name}} guardrail',
+ typeLabel: 'Guardrail type',
+ nameLabel: 'Guardrail name',
+ namePlaceholder: 'Enter guardrail name',
+ descriptionLabel: 'Guardrail description',
+ descriptionPlaceholder: 'Enter guardrail description',
+ evalsLabel: 'Enable guardrail for evaluations',
+ evalsInfoAriaLabel: 'More information',
+ evalsTooltip: 'When enabled, this guardrail will be applied during evaluation runs.',
+ saveAsNew: 'Save as new',
+ cancel: 'Cancel',
+ save: 'Save',
+ byoDisabledMessage:
+ "This guardrail's configuration has been disabled and can no longer be used. Contact your administrator to re-enable the configuration or replace this guardrail before running the agent.",
+ unauthorizedMessage:
+ 'You are not entitled to use guardrails. You can access the configuration settings, but modifications cannot be saved.',
+ featureDisabledMessage:
+ 'This guardrail feature is disabled. You can access the configuration settings, but modifications cannot be saved. It is best to remove this guardrail as long as the feature is disabled.',
+ scopesLabel: 'Scopes',
+ toolsLabel: 'Tools',
+ scopeAgentLabel: 'Agent',
+ scopeLlmLabel: 'LLM calls',
+ scopeToolLabel: 'Tools',
+ actionTypeLabel: 'Action type',
+ actionLogLabel: 'Log',
+ actionBlockLabel: 'Block',
+ actionFilterLabel: 'Filter',
+ actionEscalateLabel: 'Escalate',
+ severityLabel: 'Severity level',
+ severityInfoLabel: 'Info',
+ severityWarningLabel: 'Warning',
+ severityErrorLabel: 'Error',
+ blockReasonLabel: 'Blocking reason',
+ blockReasonPlaceholder: 'Enter reason for blocking',
+ assignToLabel: 'Assign to',
+ recipientUserLabel: 'User',
+ recipientGroupLabel: 'Group',
+ recipientEmailLabel: 'Email address',
+ recipientGroupNameLabel: 'Group name',
+ recipientFallbackLabel: 'Recipient',
+ userSearchPlaceholder: 'Search for a user...',
+ groupSearchPlaceholder: 'Search for a group...',
+ emailPlaceholder: 'Enter email address',
+ groupNamePlaceholder: 'Enter group name',
+ actionAppLabel: 'Action App',
+ appPickerUnavailable: 'App picker unavailable — requires Studio Web host.',
+ mixedScopesAlsoApplied: 'This guardrail is also applied to:',
+ mixedScopesSaveAsNewHint: 'Use "Save as new" to create a separate copy for this tool only.',
+ nameRequiredError: 'Guardrail name is required',
+ nameDuplicateError: 'A guardrail with this name already exists',
+ parameterRequiredError: 'Value is required',
+ scopesRequiredError: 'At least one scope is required',
+ toolsRequiredError: 'At least one tool is required',
+ blockReasonRequiredError: 'Block reason is required',
+ filterFieldsRequiredError: 'Fields selection is required',
+ recipientRequiredError: 'Recipient is required',
+ actionAppRequiredError: 'Action app is required',
+};
+
+// One merge for every label set: English defaults, then the catalog, then the host's
+// overrides, skipping `undefined` so a partial source never blanks a string.
+function mergeLabels(
+ defaults: T,
+ catalog?: Partial,
+ overrides?: Partial
+): T {
+ const merged: T = { ...defaults };
+ for (const source of [catalog, overrides]) {
+ if (!source) continue;
+ for (const key of Object.keys(merged) as Array) {
+ const value = source[key];
+ // `Partial[keyof T]` is `T[keyof T] | undefined`; TS cannot follow the narrowing
+ // through a generic index, hence the assertion.
+ if (value !== undefined) merged[key] = value as T[keyof T];
+ }
+ }
+ return merged;
+}
+
+/** Merge English defaults, a loaded catalog, and per-string overrides (undefined skipped). */
+export function resolveGuardrailBuilderLabels(
+ catalog?: Partial,
+ overrides?: Partial
+): GuardrailBuilderLabels {
+ return mergeLabels(GUARDRAIL_BUILDER_EN_LABELS, catalog, overrides);
+}
+
+/** Interpolate `{{token}}` placeholders in a catalog message. Unknown tokens are left as-is. */
+export function formatGuardrailFormMessage(
+ template: string,
+ vars: Record
+): string {
+ return template.replace(/\{\{(\w+)\}\}/g, (match, token: string) =>
+ token in vars ? String(vars[token]) : match
+ );
+}
+
+/** Merge English defaults, a loaded catalog, and per-string overrides (undefined skipped). */
+export function resolveGuardrailFormLabels(
+ catalog?: Partial,
+ overrides?: Partial
+): GuardrailValidatorFormLabels {
+ return mergeLabels(GUARDRAIL_FORM_EN_LABELS, catalog, overrides);
+}
+
+// Reifies each ICU placeholder back into the `{{token}}` template convention: the
+// localized strings cross into plain-string template APIs (`formatGuardrailFormMessage`,
+// wind's `formatTemplate`) as data, while translators work with standard ICU placeholders.
+const TEMPLATE_TOKENS = { name: '{{name}}', label: '{{label}}', position: '{{position}}' };
+
+/** Localized chrome strings of the validator form; per-string `overrides` always win. */
+export function useGuardrailFormLabels(
+ overrides?: Partial
+): GuardrailValidatorFormLabels {
+ const { _ } = useSafeLingui();
+ return useMemo(
+ () =>
+ resolveGuardrailFormLabels(
+ {
+ moreInformation: _({
+ id: 'guardrails.form.more-information',
+ message: 'More information',
+ }),
+ enumPlaceholder: _({ id: 'guardrails.form.enum-placeholder', message: 'Select...' }),
+ enumListPlaceholder: _({
+ id: 'guardrails.form.enum-list-placeholder',
+ message: 'Select options...',
+ }),
+ addItem: _({ id: 'guardrails.form.add-item', message: 'Add' }),
+ removeItem: _({
+ id: 'guardrails.form.remove-item',
+ message: 'Remove {label} {position}',
+ values: TEMPLATE_TOKENS,
+ }),
+ },
+ overrides
+ ),
+ [_, overrides]
+ );
+}
+
+/** Localized chrome strings of the builder screen; per-string `overrides` always win. */
+export function useGuardrailBuilderLabels(
+ overrides?: Partial
+): GuardrailBuilderLabels {
+ const { _ } = useSafeLingui();
+ return useMemo(
+ () =>
+ resolveGuardrailBuilderLabels(
+ {
+ editTitle: _({
+ id: 'guardrails.builder.edit-title',
+ message: 'Edit {name} guardrail',
+ values: TEMPLATE_TOKENS,
+ }),
+ addTitle: _({
+ id: 'guardrails.builder.add-title',
+ message: 'Add {name} guardrail',
+ values: TEMPLATE_TOKENS,
+ }),
+ typeLabel: _({ id: 'guardrails.builder.type-label', message: 'Guardrail type' }),
+ nameLabel: _({ id: 'guardrails.builder.name-label', message: 'Guardrail name' }),
+ namePlaceholder: _({
+ id: 'guardrails.builder.name-placeholder',
+ message: 'Enter guardrail name',
+ }),
+ descriptionLabel: _({
+ id: 'guardrails.builder.description-label',
+ message: 'Guardrail description',
+ }),
+ descriptionPlaceholder: _({
+ id: 'guardrails.builder.description-placeholder',
+ message: 'Enter guardrail description',
+ }),
+ evalsLabel: _({
+ id: 'guardrails.builder.evals-label',
+ message: 'Enable guardrail for evaluations',
+ }),
+ evalsInfoAriaLabel: _({
+ id: 'guardrails.builder.evals-info-aria-label',
+ message: 'More information',
+ }),
+ evalsTooltip: _({
+ id: 'guardrails.builder.evals-tooltip',
+ message: 'When enabled, this guardrail will be applied during evaluation runs.',
+ }),
+ saveAsNew: _({ id: 'guardrails.builder.save-as-new', message: 'Save as new' }),
+ cancel: _({ id: 'guardrails.builder.cancel', message: 'Cancel' }),
+ save: _({ id: 'guardrails.builder.save', message: 'Save' }),
+ byoDisabledMessage: _({
+ id: 'guardrails.builder.byo-disabled-message',
+ message:
+ "This guardrail's configuration has been disabled and can no longer be used. Contact your administrator to re-enable the configuration or replace this guardrail before running the agent.",
+ }),
+ unauthorizedMessage: _({
+ id: 'guardrails.builder.unauthorized-message',
+ message:
+ 'You are not entitled to use guardrails. You can access the configuration settings, but modifications cannot be saved.',
+ }),
+ featureDisabledMessage: _({
+ id: 'guardrails.builder.feature-disabled-message',
+ message:
+ 'This guardrail feature is disabled. You can access the configuration settings, but modifications cannot be saved. It is best to remove this guardrail as long as the feature is disabled.',
+ }),
+ scopesLabel: _({ id: 'guardrails.builder.scopes-label', message: 'Scopes' }),
+ toolsLabel: _({ id: 'guardrails.builder.tools-label', message: 'Tools' }),
+ scopeAgentLabel: _({ id: 'guardrails.builder.scope-agent-label', message: 'Agent' }),
+ scopeLlmLabel: _({ id: 'guardrails.builder.scope-llm-label', message: 'LLM calls' }),
+ scopeToolLabel: _({ id: 'guardrails.builder.scope-tool-label', message: 'Tools' }),
+ actionTypeLabel: _({
+ id: 'guardrails.builder.action-type-label',
+ message: 'Action type',
+ }),
+ actionLogLabel: _({ id: 'guardrails.builder.action-log-label', message: 'Log' }),
+ actionBlockLabel: _({ id: 'guardrails.builder.action-block-label', message: 'Block' }),
+ actionFilterLabel: _({ id: 'guardrails.builder.action-filter-label', message: 'Filter' }),
+ actionEscalateLabel: _({
+ id: 'guardrails.builder.action-escalate-label',
+ message: 'Escalate',
+ }),
+ severityLabel: _({ id: 'guardrails.builder.severity-label', message: 'Severity level' }),
+ severityInfoLabel: _({ id: 'guardrails.builder.severity-info-label', message: 'Info' }),
+ severityWarningLabel: _({
+ id: 'guardrails.builder.severity-warning-label',
+ message: 'Warning',
+ }),
+ severityErrorLabel: _({
+ id: 'guardrails.builder.severity-error-label',
+ message: 'Error',
+ }),
+ blockReasonLabel: _({
+ id: 'guardrails.builder.block-reason-label',
+ message: 'Blocking reason',
+ }),
+ blockReasonPlaceholder: _({
+ id: 'guardrails.builder.block-reason-placeholder',
+ message: 'Enter reason for blocking',
+ }),
+ assignToLabel: _({ id: 'guardrails.builder.assign-to-label', message: 'Assign to' }),
+ recipientUserLabel: _({ id: 'guardrails.builder.recipient-user-label', message: 'User' }),
+ recipientGroupLabel: _({
+ id: 'guardrails.builder.recipient-group-label',
+ message: 'Group',
+ }),
+ recipientEmailLabel: _({
+ id: 'guardrails.builder.recipient-email-label',
+ message: 'Email address',
+ }),
+ recipientGroupNameLabel: _({
+ id: 'guardrails.builder.recipient-group-name-label',
+ message: 'Group name',
+ }),
+ recipientFallbackLabel: _({
+ id: 'guardrails.builder.recipient-fallback-label',
+ message: 'Recipient',
+ }),
+ userSearchPlaceholder: _({
+ id: 'guardrails.builder.user-search-placeholder',
+ message: 'Search for a user...',
+ }),
+ groupSearchPlaceholder: _({
+ id: 'guardrails.builder.group-search-placeholder',
+ message: 'Search for a group...',
+ }),
+ emailPlaceholder: _({
+ id: 'guardrails.builder.email-placeholder',
+ message: 'Enter email address',
+ }),
+ groupNamePlaceholder: _({
+ id: 'guardrails.builder.group-name-placeholder',
+ message: 'Enter group name',
+ }),
+ actionAppLabel: _({ id: 'guardrails.builder.action-app-label', message: 'Action App' }),
+ appPickerUnavailable: _({
+ id: 'guardrails.builder.app-picker-unavailable',
+ message: 'App picker unavailable — requires Studio Web host.',
+ }),
+ mixedScopesAlsoApplied: _({
+ id: 'guardrails.builder.mixed-scopes-also-applied',
+ message: 'This guardrail is also applied to:',
+ }),
+ mixedScopesSaveAsNewHint: _({
+ id: 'guardrails.builder.mixed-scopes-save-as-new-hint',
+ message: 'Use "Save as new" to create a separate copy for this tool only.',
+ }),
+ nameRequiredError: _({
+ id: 'guardrails.builder.name-required-error',
+ message: 'Guardrail name is required',
+ }),
+ nameDuplicateError: _({
+ id: 'guardrails.builder.name-duplicate-error',
+ message: 'A guardrail with this name already exists',
+ }),
+ parameterRequiredError: _({
+ id: 'guardrails.builder.parameter-required-error',
+ message: 'Value is required',
+ }),
+ scopesRequiredError: _({
+ id: 'guardrails.builder.scopes-required-error',
+ message: 'At least one scope is required',
+ }),
+ toolsRequiredError: _({
+ id: 'guardrails.builder.tools-required-error',
+ message: 'At least one tool is required',
+ }),
+ blockReasonRequiredError: _({
+ id: 'guardrails.builder.block-reason-required-error',
+ message: 'Block reason is required',
+ }),
+ filterFieldsRequiredError: _({
+ id: 'guardrails.builder.filter-fields-required-error',
+ message: 'Fields selection is required',
+ }),
+ recipientRequiredError: _({
+ id: 'guardrails.builder.recipient-required-error',
+ message: 'Recipient is required',
+ }),
+ actionAppRequiredError: _({
+ id: 'guardrails.builder.action-app-required-error',
+ message: 'Action app is required',
+ }),
+ },
+ overrides
+ ),
+ [_, overrides]
+ );
+}
+
+/**
+ * Chrome strings of the guardrail list section (header, add affordance, row actions, status
+ * chips, BYO notices). Domain copy stays out: scope and action wording arrive through
+ * `formatScopes` / `formatAction`, and the guardrail's own name and description are data.
+ *
+ * Values may contain `{{placeholder}}` tokens; interpolate with `formatGuardrailFormMessage`.
+ */
+export interface GuardrailListLabels {
+ /** Section header title. */
+ title: string;
+ /** Header add-button label. */
+ add: string;
+ /** Line shown instead of the rows when the list is empty. */
+ empty: string;
+ /** Aria-label template of a row's drag handle: `{{name}}`. */
+ reorderItem: string;
+ /** Aria-label of a row's edit button. */
+ editItem: string;
+ /** Aria-label template of the row body when `rowActivatesEdit` is set: `{{name}}`. */
+ editRow: string;
+ /** Aria-label of a row's remove button. */
+ removeItem: string;
+ /** Lifecycle badge on built-in-validator rows (rendered only with `previewChip`). */
+ preview: string;
+ /** Prefix of the BYO connector line, rendered as `{provider}: {connector}`. */
+ provider: string;
+ /** Action badge text for a row whose action is missing or unrecognized. */
+ actionUnknown: string;
+ /** Row notice: the BYO configuration this guardrail points at was disabled. */
+ byoDisabledNotice: string;
+ /** Row notice: the BYO configuration this guardrail points at is gone. */
+ byoUnavailableNotice: string;
+ /** Status chip: the validator's feature flag is off tenant-wide. */
+ statusFeatureDisabled: string;
+ /** Status chip: the tenant is not entitled to the validator. */
+ statusUnauthorized: string;
+ /** Status chip: the (BYO) configuration is disabled. */
+ statusDisabled: string;
+ /** Status chip: no definition resolves for this row any more. */
+ statusUnavailable: string;
+ /** Administration chip on rows a governance policy owns. */
+ administrationGovernance: string;
+}
+
+/** The subset of `useSafeLingui`'s translator the list labels need. */
+type ListTranslate = (descriptor: {
+ id: string;
+ message: string;
+ values?: Record;
+}) => string;
+
+// One builder holds every `_({ id, message })` call, so the English defaults, the flat record
+// the catalog test diffs and the runtime lingui path cannot drift, and `lingui extract` still
+// sees static calls. Same shape as `definitions-copy.ts`.
+function buildGuardrailListLabels(_: ListTranslate): GuardrailListLabels {
+ return {
+ title: _({ id: 'guardrails.list.title', message: 'Guardrails' }),
+ add: _({ id: 'guardrails.list.add', message: 'Add' }),
+ empty: _({ id: 'guardrails.list.empty', message: 'No guardrails configured' }),
+ reorderItem: _({
+ id: 'guardrails.list.reorder-item',
+ message: 'Reorder guardrail {name}',
+ values: TEMPLATE_TOKENS,
+ }),
+ editItem: _({ id: 'guardrails.list.edit-item', message: 'Edit guardrail' }),
+ editRow: _({
+ id: 'guardrails.list.edit-row',
+ message: 'Edit {name}',
+ values: TEMPLATE_TOKENS,
+ }),
+ removeItem: _({ id: 'guardrails.list.remove-item', message: 'Remove guardrail' }),
+ preview: _({ id: 'guardrails.list.preview', message: 'Preview' }),
+ provider: _({ id: 'guardrails.list.provider', message: 'Provider' }),
+ actionUnknown: _({ id: 'guardrails.list.action-unknown', message: 'Unknown' }),
+ byoDisabledNotice: _({
+ id: 'guardrails.list.byo-disabled-notice',
+ message:
+ "This guardrail's configuration has been disabled and can no longer be used. Contact your administrator to re-enable the configuration or replace this guardrail before running the agent.",
+ }),
+ byoUnavailableNotice: _({
+ id: 'guardrails.list.byo-unavailable-notice',
+ message:
+ "This guardrail's configuration is no longer available. Replace it before running the agent.",
+ }),
+ statusFeatureDisabled: _({
+ id: 'guardrails.list.status-feature-disabled',
+ message: 'Feature disabled',
+ }),
+ statusUnauthorized: _({
+ id: 'guardrails.list.status-unauthorized',
+ message: 'Unauthorized',
+ }),
+ statusDisabled: _({ id: 'guardrails.list.status-disabled', message: 'Disabled' }),
+ statusUnavailable: _({ id: 'guardrails.list.status-unavailable', message: 'Unavailable' }),
+ administrationGovernance: _({
+ id: 'guardrails.list.administration-governance',
+ message: 'Governance managed',
+ }),
+ };
+}
+
+// Resolves a descriptor the way lingui does with `values: TEMPLATE_TOKENS`, so the English
+// defaults carry the same `{{token}}` convention as a translated catalog entry.
+const englishListTranslate: ListTranslate = ({ message, values }) =>
+ values
+ ? message.replace(/\{(\w+)\}/g, (match, token: string) => values[token] ?? match)
+ : message;
+
+/** The English chrome strings, resolved without a lingui provider. */
+export const GUARDRAIL_LIST_EN_LABELS: GuardrailListLabels =
+ buildGuardrailListLabels(englishListTranslate);
+
+/**
+ * The same strings flattened to message id to the **ICU source message**, which is the form
+ * the catalogs store: the parity test compares these against `locales/en.json` verbatim.
+ */
+export const GUARDRAIL_LIST_EN_MESSAGES: Readonly> = Object.freeze(
+ (() => {
+ const messages: Record = {};
+ buildGuardrailListLabels((descriptor) => {
+ messages[descriptor.id] = descriptor.message;
+ return descriptor.message;
+ });
+ return messages;
+ })()
+);
+
+/** Merge English defaults, a loaded catalog, and per-string overrides (undefined skipped). */
+export function resolveGuardrailListLabels(
+ catalog?: Partial,
+ overrides?: Partial
+): GuardrailListLabels {
+ return mergeLabels(GUARDRAIL_LIST_EN_LABELS, catalog, overrides);
+}
+
+/** Localized chrome strings of the list section; per-string `overrides` always win. */
+export function useGuardrailListLabels(
+ overrides?: Partial
+): GuardrailListLabels {
+ const { _ } = useSafeLingui();
+ return useMemo(
+ () => resolveGuardrailListLabels(buildGuardrailListLabels(_), overrides),
+ [_, overrides]
+ );
+}
+
+/**
+ * Chrome strings of the add-guardrail palette. Definition copy (display names, descriptions,
+ * connector and folder names) is not localized here: it arrives resolved on the definitions,
+ * from the canonical copy table or the wire.
+ */
+export interface GuardrailPaletteLabels {
+ /** Accessible name of the palette region. */
+ listAriaLabel: string;
+ /** Create-custom entry, rendered only when the host wires `onCreateCustom`. */
+ createCustom: string;
+ createCustomDescription: string;
+ /** Line shown instead of the entries when there is nothing to pick. */
+ empty: string;
+ /** Line shown while the definitions are in flight. */
+ loading: string;
+ /** Banner message when the definitions failed to load. */
+ loadError: string;
+ /** Heading of the trailing group of UiPath validators, shown only alongside BYO groups. */
+ uipathGroup: string;
+ /** Lifecycle chip on definition entries (rendered only with `previewChip`). */
+ preview: string;
+ /** Status chip on an entry the tenant is not entitled to. */
+ statusUnauthorized: string;
+}
+
+/** The subset of `useSafeLingui`'s translator the palette labels need. */
+type PaletteTranslate = (descriptor: { id: string; message: string }) => string;
+
+// One builder holds every `_({ id, message })` call, so the English defaults, the flat record
+// the catalog test diffs and the runtime lingui path cannot drift, and `lingui extract` still
+// sees static calls. Same shape as `definitions-copy.ts`.
+function buildGuardrailPaletteLabels(_: PaletteTranslate): GuardrailPaletteLabels {
+ return {
+ listAriaLabel: _({
+ id: 'guardrails.palette.list-aria-label',
+ message: 'Available guardrails',
+ }),
+ createCustom: _({ id: 'guardrails.palette.create-custom', message: 'Custom guardrail' }),
+ createCustomDescription: _({
+ id: 'guardrails.palette.create-custom-description',
+ message: 'Create a guardrail with custom rules',
+ }),
+ empty: _({ id: 'guardrails.palette.empty', message: 'No guardrails available' }),
+ loading: _({ id: 'guardrails.palette.loading', message: 'Loading...' }),
+ loadError: _({
+ id: 'guardrails.palette.load-error',
+ message: 'Failed to load built-in validators',
+ }),
+ uipathGroup: _({ id: 'guardrails.palette.uipath-group', message: 'UiPath guardrails' }),
+ preview: _({ id: 'guardrails.palette.preview', message: 'Preview' }),
+ statusUnauthorized: _({
+ id: 'guardrails.palette.status-unauthorized',
+ message: 'Unauthorized',
+ }),
+ };
+}
+
+/** The English chrome strings, resolved without a lingui provider. */
+export const GUARDRAIL_PALETTE_EN_LABELS: GuardrailPaletteLabels = buildGuardrailPaletteLabels(
+ ({ message }) => message
+);
+
+/**
+ * The same strings flattened to message id to the **ICU source message**, which is the form
+ * the catalogs store: the parity test compares these against `locales/en.json` verbatim.
+ */
+export const GUARDRAIL_PALETTE_EN_MESSAGES: Readonly> = Object.freeze(
+ (() => {
+ const messages: Record = {};
+ buildGuardrailPaletteLabels((descriptor) => {
+ messages[descriptor.id] = descriptor.message;
+ return descriptor.message;
+ });
+ return messages;
+ })()
+);
+
+/** Merge English defaults, a loaded catalog, and per-string overrides (undefined skipped). */
+export function resolveGuardrailPaletteLabels(
+ catalog?: Partial,
+ overrides?: Partial
+): GuardrailPaletteLabels {
+ return mergeLabels(GUARDRAIL_PALETTE_EN_LABELS, catalog, overrides);
+}
+
+/** Localized chrome strings of the palette; per-string `overrides` always win. */
+export function useGuardrailPaletteLabels(
+ overrides?: Partial
+): GuardrailPaletteLabels {
+ const { _ } = useSafeLingui();
+ return useMemo(
+ () => resolveGuardrailPaletteLabels(buildGuardrailPaletteLabels(_), overrides),
+ [_, overrides]
+ );
+}
diff --git a/packages/apollo-react/src/canvas/components/Guardrails/index.ts b/packages/apollo-react/src/canvas/components/Guardrails/index.ts
new file mode 100644
index 0000000000..0254e7ae06
--- /dev/null
+++ b/packages/apollo-react/src/canvas/components/Guardrails/index.ts
@@ -0,0 +1,141 @@
+export type {
+ GuardrailAction,
+ GuardrailAppPickerContext,
+ GuardrailBuilderErrors,
+ GuardrailBuilderSlots,
+ GuardrailBuilderValue,
+ GuardrailDefinition,
+ GuardrailDefinitionStatus,
+ GuardrailEscalateApp,
+ GuardrailEscalateRecipient,
+ GuardrailRecipientSearchContext,
+ GuardrailRecipientTypeValue,
+ GuardrailScope,
+ GuardrailSelector,
+ GuardrailSeverityLevel,
+ GuardrailStaticRecipientContext,
+} from './builder-types';
+export {
+ GUARDRAIL_BYO_VALIDATOR_TYPE,
+ GuardrailRecipientType,
+} from './builder-types';
+export type {
+ GuardrailActionErrorField,
+ GuardrailBuilderFormData,
+ GuardrailSelectorErrorField,
+} from './builder-utils';
+export {
+ createDefaultGuardrailAction,
+ generateGuardrailId,
+ getGuardrailActionErrorFields,
+ getGuardrailSelectorErrorFields,
+ initGuardrailBuilderFormData,
+} from './builder-utils';
+export type { GuardrailChipProps } from './components/guardrail-chip';
+export { GuardrailChip, guardrailChipVariants } from './components/guardrail-chip';
+export type { GuardrailStatusBannerProps } from './components/guardrail-status-banner';
+export { GuardrailStatusBanner } from './components/guardrail-status-banner';
+export type { GuardrailStatusChipProps } from './components/guardrail-status-chip';
+export { GuardrailStatusChip } from './components/guardrail-status-chip';
+export type { MixedScopesBannerProps } from './components/mixed-scopes-banner';
+export { MixedScopesBanner } from './components/mixed-scopes-banner';
+export type { GuardrailCopyTable, GuardrailValidatorCopy } from './definitions-copy';
+export {
+ CURATED_GUARDRAIL_VALIDATORS,
+ GUARDRAIL_COPY_EN,
+ GUARDRAIL_COPY_EN_MESSAGES,
+ useGuardrailDefinitionCopy,
+} from './definitions-copy';
+export type {
+ EnrichedGuardrailDefinition,
+ EnrichGuardrailDefinitionsOptions,
+ GuardrailFolderMetadata,
+} from './definitions-enrich';
+export {
+ enrichGuardrailDefinitions,
+ humanizeGuardrailParameterId,
+ isByoGuardrailDefinition,
+ withGuardrailFolderMetadata,
+} from './definitions-enrich';
+export type {
+ GuardrailDefinitionParseIssue,
+ GuardrailDefinitionsParseResult,
+} from './definitions-parse';
+export { parseGuardrailDefinitions } from './definitions-parse';
+export type {
+ GuardrailDefinitionWire,
+ GuardrailParameterDefinitionWire,
+ GuardrailParameterWireBase,
+} from './definitions-wire';
+export type { GuardrailBuilderProps } from './guardrail-builder';
+export { GuardrailBuilder } from './guardrail-builder';
+export type { GuardrailFormLayoutProps } from './guardrail-form-layout';
+export { GuardrailFormLayout } from './guardrail-form-layout';
+export type { GuardrailListProps } from './guardrail-list';
+export { GuardrailList } from './guardrail-list';
+export {
+ getGuardrailListChips,
+ getGuardrailListItemId,
+ matchesGuardrailListDefinition,
+ resolveGuardrailListItemState,
+} from './guardrail-list-utils';
+export type { GuardrailPaletteProps } from './guardrail-palette';
+export { GuardrailPalette } from './guardrail-palette';
+export { getGuardrailPaletteItemId, groupGuardrailsForPalette } from './guardrail-palette-utils';
+export { GuardrailValidatorForm } from './guardrail-validator-form';
+export type {
+ GuardrailBuilderLabels,
+ GuardrailListLabels,
+ GuardrailPaletteLabels,
+ GuardrailValidatorFormLabels,
+} from './i18n';
+export {
+ formatGuardrailFormMessage,
+ GUARDRAIL_BUILDER_EN_LABELS,
+ GUARDRAIL_FORM_EN_LABELS,
+ GUARDRAIL_LIST_EN_LABELS,
+ GUARDRAIL_LIST_EN_MESSAGES,
+ GUARDRAIL_PALETTE_EN_LABELS,
+ GUARDRAIL_PALETTE_EN_MESSAGES,
+ resolveGuardrailBuilderLabels,
+ resolveGuardrailFormLabels,
+ resolveGuardrailListLabels,
+ resolveGuardrailPaletteLabels,
+ useGuardrailBuilderLabels,
+ useGuardrailFormLabels,
+ useGuardrailListLabels,
+ useGuardrailPaletteLabels,
+} from './i18n';
+export type {
+ GuardrailListAdministration,
+ GuardrailListChip,
+ GuardrailListDefinition,
+ GuardrailListItem,
+ GuardrailListItemActionsContext,
+ GuardrailListItemState,
+ GuardrailListStatus,
+ GuardrailReorderMove,
+} from './list-types';
+export type { GuardrailPaletteDefinition, GuardrailPaletteGroup } from './palette-types';
+export type {
+ GuardrailParameterDefinition,
+ GuardrailParameterRenderContext,
+ GuardrailParameterType,
+ GuardrailValidatorFormProps,
+ GuardrailValidatorParameter,
+} from './types';
+export type {
+ GuardrailDefinitionsRequestContext,
+ UseGuardrailDefinitionsOptions,
+ UseGuardrailDefinitionsResult,
+} from './use-guardrail-definitions';
+export {
+ GUARDRAIL_DEFINITIONS_PATH,
+ useGuardrailDefinitions,
+} from './use-guardrail-definitions';
+export {
+ dropEmptyOptionalParameters,
+ getRequiredEmptyParameterIds,
+ seedGuardrailParameters,
+ syncMapEnumParameters,
+} from './utils';
diff --git a/packages/apollo-react/src/canvas/components/Guardrails/list-types.ts b/packages/apollo-react/src/canvas/components/Guardrails/list-types.ts
new file mode 100644
index 0000000000..bee93c31be
--- /dev/null
+++ b/packages/apollo-react/src/canvas/components/Guardrails/list-types.ts
@@ -0,0 +1,107 @@
+import type { ReactNode } from 'react';
+import type { GuardrailDefinitionStatus, GuardrailSelector } from './builder-types';
+
+/**
+ * Structural mirrors for the guardrails list. Like `builder-types.ts`, these describe the
+ * shapes both products already persist rather than introducing a package type hosts have to
+ * map to; mutual assignability is asserted host-side.
+ *
+ * Where the list only reads one field of a larger shape it asks for that field alone
+ * (`action` is `{ $actionType }`, not the full action union): the list renders a row, it never
+ * interprets an action, so the narrower requirement is what keeps both products' unions and
+ * their custom-guardrail members assignable.
+ */
+
+/**
+ * One row. Covers both products' guardrail kinds: the built-in-validator shape the builder
+ * edits, and the custom/deterministic shape only the list has to display.
+ */
+export interface GuardrailListItem {
+ /** Stable id where the product has one (Flow). Rows fall back to `name` (Agents). */
+ id?: string;
+ name: string;
+ description?: string;
+ selector?: GuardrailSelector;
+ /** Only the discriminator is read, for the action badge. */
+ action?: { $actionType: string };
+ /** Widened to `string`: the list branches on `'builtInValidator'` and ignores the rest. */
+ $guardrailType?: string;
+ validatorType?: string;
+ /** Present on bring-your-own rows; it is what resolves them to a definition. */
+ byoValidatorName?: string;
+}
+
+/**
+ * The definition fields a row resolves against. A `GuardrailDefinition` (and the enriched
+ * definitions the definitions layer produces) satisfies this, so hosts pass the array they
+ * already have, filtered by their own flags and entitlements.
+ */
+export interface GuardrailListDefinition {
+ validator: string;
+ status: GuardrailDefinitionStatus;
+ byoValidatorName?: string;
+ /** BYO connector display name, rendered as the row's provider line. */
+ byoConnectorName?: string;
+}
+
+/**
+ * Who administers a row: the agent's own configuration, or a tenant governance policy.
+ *
+ * Deliberately not called "origin": both products already use that word for BYO versus
+ * UiPath-managed validators (`GuardrailOriginChip`, `CentralizedGuardrailOriginBadge`), which
+ * is a different axis and is carried here by `byoValidatorName`.
+ */
+export type GuardrailListAdministration = 'local' | 'governance';
+
+/**
+ * A row's resolved status. `'Unavailable'` is not a wire status: it is what a BYO row shows
+ * once its definition has stopped resolving.
+ */
+export type GuardrailListStatus = GuardrailDefinitionStatus | 'Unavailable';
+
+/** What resolving a row against the definitions told us about it. */
+export interface GuardrailListItemState {
+ /** The matched definition, if any. */
+ definition?: GuardrailListDefinition;
+ /** Chip-worthy status, `undefined` when nothing resolved (a custom guardrail, say). */
+ status?: GuardrailListStatus;
+ isByo: boolean;
+ /** BYO connector name, from the matched definition. */
+ provider?: string;
+ /** The BYO configuration exists but has been disabled: blocking notice on the row. */
+ byoDisabled: boolean;
+ /** The BYO configuration no longer resolves: blocking notice on the row. */
+ byoUnavailable: boolean;
+}
+
+/** One resolved chip. `tone` maps onto the wind `Badge` variants the chip uses. */
+export interface GuardrailListChip {
+ /** Which axis produced it, and the React key. */
+ id: 'status' | 'administration';
+ tone: 'neutral' | 'warning' | 'error';
+ label: string;
+}
+
+/** Where a row moved to, alongside the reordered array. */
+export interface GuardrailReorderMove {
+ from: number;
+ to: number;
+ /** The moved row's id, as `getItemId` resolved it. */
+ id: string;
+}
+
+/** Context handed to the `renderItemActions` slot (Agents' overflow menu). */
+export interface GuardrailListItemActionsContext {
+ item: GuardrailListItem;
+ /** The row's resolved id. */
+ id: string;
+ /** Row index in the rendered (visible) list. */
+ index: number;
+ /** Whether the list is read-only. */
+ disabled: boolean;
+ /** The list's intents, already bound to this row. Absent when the host passed no handler. */
+ onEdit?: () => void;
+ onRemove?: () => void;
+ /** The default inline Edit / Remove buttons, so a slot can add to them instead of replacing. */
+ defaultActions: ReactNode;
+}
diff --git a/packages/apollo-react/src/canvas/components/Guardrails/palette-types.ts b/packages/apollo-react/src/canvas/components/Guardrails/palette-types.ts
new file mode 100644
index 0000000000..2cf8da4e6f
--- /dev/null
+++ b/packages/apollo-react/src/canvas/components/Guardrails/palette-types.ts
@@ -0,0 +1,33 @@
+import type { GuardrailDefinitionStatus } from './builder-types';
+
+/**
+ * The minimum a definition needs for the palette to render it.
+ *
+ * Deliberately narrower than `EnrichedGuardrailDefinition` (which satisfies it), for the same
+ * reason the list keeps its own definition shape: the palette reads eight fields, and a
+ * product feeding its own definition type should not have to adopt ours to show a picker.
+ * `GuardrailPalette` is generic over this, so whatever goes in comes back out of
+ * `onSelectOotb` unchanged and the host still has `parameters` to open its builder with.
+ */
+export interface GuardrailPaletteDefinition {
+ validator: string;
+ displayName: string;
+ description?: string;
+ status: GuardrailDefinitionStatus;
+ /** Present for bring-your-own definitions; decides grouping and the connector chip. */
+ byoValidatorName?: string;
+ byoConnectorName?: string;
+ byoGuardrailConnectionId?: string;
+ /** BYO folder placement, resolved host-side; the preferred group key when present. */
+ folderPath?: string;
+}
+
+/** One rendered section of the palette: a BYO folder or connector, or the UiPath validators. */
+export interface GuardrailPaletteGroup {
+ /** Stable react key. Synthetic (`__all__`, `__uipath__`, `__byo__`) where there is no name. */
+ key: string;
+ /** Rendered heading, or `null` for a group that carries no heading at all. */
+ header: string | null;
+ isByo: boolean;
+ definitions: T[];
+}
diff --git a/packages/apollo-react/src/canvas/components/Guardrails/render-parameter-bridge.tsx b/packages/apollo-react/src/canvas/components/Guardrails/render-parameter-bridge.tsx
new file mode 100644
index 0000000000..7a431c313e
--- /dev/null
+++ b/packages/apollo-react/src/canvas/components/Guardrails/render-parameter-bridge.tsx
@@ -0,0 +1,54 @@
+import type { CustomFieldComponentProps } from '@uipath/apollo-wind';
+import { createContext, useContext } from 'react';
+import type {
+ GuardrailParameterDefinition,
+ GuardrailValidatorFormProps,
+ GuardrailValidatorParameter,
+} from './types';
+
+/**
+ * Live host context for `renderParameter` overrides. The bridge component below is
+ * registered once (as the `guardrail-render-parameter` custom component) while the fresh
+ * per-render callbacks and host truth flow through this context — the schema stays a plain
+ * serializable object.
+ */
+export interface GuardrailRenderParameterContextValue {
+ renderParameter: GuardrailValidatorFormProps['renderParameter'];
+ defsById: Map;
+ /** Host truth — includes sidecar parameters that never enter the form. */
+ parameters: GuardrailValidatorParameter[];
+ errors?: Record;
+ updateParam: (def: GuardrailParameterDefinition, value: unknown) => void;
+ replaceParams: (paramId: string, next: GuardrailValidatorParameter[]) => void;
+}
+
+const GuardrailRenderParameterContext = createContext(
+ null
+);
+
+export const GuardrailRenderParameterProvider = GuardrailRenderParameterContext.Provider;
+
+/**
+ * Mounts the host's `renderParameter` node for an overridden parameter. `onValueChange`
+ * upserts the one parameter; `onParametersChange` hands the host the whole array — the seam
+ * overrides use to write sidecar parameters (e.g. a model picker persisting connection
+ * metadata) atomically with their own value.
+ */
+export function RenderParameterBridge(props: CustomFieldComponentProps) {
+ const ctx = useContext(GuardrailRenderParameterContext);
+ if (!ctx?.renderParameter) return null;
+ const def = ctx.defsById.get(props.name);
+ if (!def) return null;
+
+ const entry = ctx.parameters.find((p) => p.id === props.name);
+ const node = ctx.renderParameter({
+ definition: def,
+ value: entry?.value,
+ error: ctx.errors?.[props.name],
+ parameters: ctx.parameters,
+ onValueChange: (value) => ctx.updateParam(def, value),
+ onParametersChange: (next) => ctx.replaceParams(def.id, next),
+ });
+
+ return <>{node ?? null}>;
+}
diff --git a/packages/apollo-react/src/canvas/components/Guardrails/types.ts b/packages/apollo-react/src/canvas/components/Guardrails/types.ts
new file mode 100644
index 0000000000..1c7bcd7c09
--- /dev/null
+++ b/packages/apollo-react/src/canvas/components/Guardrails/types.ts
@@ -0,0 +1,96 @@
+import type * as React from 'react';
+import type { GuardrailValidatorFormLabels } from './i18n';
+
+/**
+ * A configured guardrail validator parameter value, discriminated on `$parameterType`.
+ *
+ * Structurally mirrors the wire shape both consuming products (Flow, Agents) persist for
+ * OOTB validator guardrails — hosts can pass their own equivalent union without mapping.
+ */
+export type GuardrailValidatorParameter =
+ | { $parameterType: 'number'; id: string; value: number }
+ | { $parameterType: 'text'; id: string; value: string }
+ | { $parameterType: 'boolean'; id: string; value: boolean }
+ | { $parameterType: 'enum'; id: string; value: string }
+ | { $parameterType: 'enum-list'; id: string; value: string[] }
+ | { $parameterType: 'text-list'; id: string; value: string[] }
+ | { $parameterType: 'map-enum'; id: string; value: Record };
+
+export type GuardrailParameterType = GuardrailValidatorParameter['$parameterType'];
+
+/**
+ * The flat, display-ready definition of one configurable guardrail parameter.
+ *
+ * `label`, `tooltip` and `optionLabels` arrive pre-resolved (already localized by the host):
+ * catalog/domain strings such as PII entity names belong to the products, not to this
+ * component. Only the component's own chrome strings are localized here (see i18n.ts).
+ */
+export interface GuardrailParameterDefinition {
+ id: string;
+ type: GuardrailParameterType;
+ /** Pre-resolved, host-localized display label. */
+ label: string;
+ required: boolean;
+ defaultValue: unknown;
+ /** Optional per-parameter info tooltip (pre-resolved). */
+ tooltip?: string;
+ /** For enum / enum-list: available options (raw values). */
+ options?: string[];
+ /** Friendly per-option labels keyed by raw option value. */
+ optionLabels?: Record;
+ /** For map-enum: id of the sibling enum-list parameter whose selection provides the keys. */
+ keySource?: string;
+ /** For number / map-enum: numeric constraints. */
+ min?: number;
+ max?: number;
+ step?: number;
+ /** For text / text-list: per-item character cap. */
+ maxLength?: number;
+ /** For text-list: cap on how many entries can be added. */
+ maxItems?: number;
+}
+
+/**
+ * Everything a `renderParameter` override needs to replace one parameter's default editor,
+ * e.g. mounting a product-specific model picker for a judge-model parameter.
+ */
+export interface GuardrailParameterRenderContext {
+ definition: GuardrailParameterDefinition;
+ /** The parameter's current raw value, if one is stored. */
+ value: GuardrailValidatorParameter['value'] | undefined;
+ /** Host-supplied error message for this parameter, if any. */
+ error?: string;
+ /** All current parameter values (for editors that read or write siblings). */
+ parameters: GuardrailValidatorParameter[];
+ /** Upsert this parameter's value; the form emits the full updated array via `onChange`. */
+ onValueChange: (value: GuardrailValidatorParameter['value']) => void;
+ /**
+ * Replace the whole parameters array. Needed by overrides that write sidecar parameters
+ * alongside their own (e.g. a model picker persisting connection metadata).
+ */
+ onParametersChange: (parameters: GuardrailValidatorParameter[]) => void;
+}
+
+export interface GuardrailValidatorFormProps {
+ /** The guardrail definition's configurable parameters, in render order. */
+ parameterDefinitions: GuardrailParameterDefinition[];
+ /** Current parameter values (controlled). */
+ parameters: GuardrailValidatorParameter[];
+ /** Called with the full updated parameters array on every edit. */
+ onChange: (parameters: GuardrailValidatorParameter[]) => void;
+ /**
+ * Host-supplied validation errors keyed by parameter id. The form never validates on its
+ * own: compute errors host-side (see `getRequiredEmptyParameterIds`) and pass them here.
+ */
+ errors?: Record;
+ /** Called with the parameter id before `onChange` whenever that parameter is edited. */
+ onClearError?: (paramId: string) => void;
+ /**
+ * Override the editor for individual parameters. Return `undefined` to fall through to the
+ * default editor for that parameter type.
+ */
+ renderParameter?: (ctx: GuardrailParameterRenderContext) => React.ReactNode | undefined;
+ /** Per-string overrides; take precedence over the ambient lingui catalog. */
+ labels?: Partial;
+ className?: string;
+}
diff --git a/packages/apollo-react/src/canvas/components/Guardrails/use-guardrail-definitions.test.tsx b/packages/apollo-react/src/canvas/components/Guardrails/use-guardrail-definitions.test.tsx
new file mode 100644
index 0000000000..2f3290c2f7
--- /dev/null
+++ b/packages/apollo-react/src/canvas/components/Guardrails/use-guardrail-definitions.test.tsx
@@ -0,0 +1,303 @@
+import { renderHook, waitFor } from '@testing-library/react';
+import { beforeEach, describe, expect, it, vi } from 'vitest';
+import {
+ ALL_BUILT_IN_WIRE,
+ BYO_WIRE,
+ PII_DETECTION_WIRE,
+ PROMPT_INJECTION_WIRE,
+ RAW_PAYLOAD_WITH_NOISE,
+} from './__fixtures__/definitions-wire.fixtures';
+import { GUARDRAIL_COPY_EN, type GuardrailCopyTable } from './definitions-copy';
+import {
+ type GuardrailDefinitionsRequestContext,
+ useGuardrailDefinitions,
+} from './use-guardrail-definitions';
+
+function jsonResponse(body: unknown, init: { ok?: boolean; status?: number } = {}): Response {
+ return {
+ ok: init.ok ?? true,
+ status: init.status ?? 200,
+ statusText: init.ok === false ? 'Server Error' : 'OK',
+ json: async () => body,
+ } as Response;
+}
+
+function ctxWith(
+ fetchImpl: typeof fetch,
+ overrides: Partial = {}
+) {
+ return {
+ baseUrl: 'https://cloud.uipath.com/acme/tenant/agents_',
+ tenantId: 'tenant-1',
+ fetch: fetchImpl,
+ ...overrides,
+ } satisfies GuardrailDefinitionsRequestContext;
+}
+
+describe('useGuardrailDefinitions', () => {
+ beforeEach(() => {
+ vi.restoreAllMocks();
+ });
+
+ describe('fetching', () => {
+ it('loads, validates and enriches in one pass', async () => {
+ const fetchImpl = vi.fn(async () => jsonResponse(ALL_BUILT_IN_WIRE));
+
+ const { result } = renderHook(() => useGuardrailDefinitions(ctxWith(fetchImpl)));
+
+ await waitFor(() => expect(result.current.loading).toBe(false));
+ expect(result.current.definitions.map((d) => d.displayName)).toContain('PII detection');
+ expect(result.current.wire).toHaveLength(ALL_BUILT_IN_WIRE.length);
+ expect(result.current.error).toBeNull();
+ });
+
+ it('calls the definitions endpoint under the host base URL', async () => {
+ const fetchImpl = vi.fn(async () => jsonResponse([]));
+
+ renderHook(() => useGuardrailDefinitions(ctxWith(fetchImpl)));
+
+ await waitFor(() => expect(fetchImpl).toHaveBeenCalled());
+ expect(fetchImpl.mock.calls[0]?.[0]).toBe(
+ 'https://cloud.uipath.com/acme/tenant/agents_/api/execution/guardrails/definitions'
+ );
+ });
+
+ it('trims a trailing slash off the base URL', async () => {
+ const fetchImpl = vi.fn(async () => jsonResponse([]));
+
+ renderHook(() =>
+ useGuardrailDefinitions(ctxWith(fetchImpl, { baseUrl: 'https://example.test/' }))
+ );
+
+ await waitFor(() => expect(fetchImpl).toHaveBeenCalled());
+ expect(fetchImpl.mock.calls[0]?.[0]).toBe(
+ 'https://example.test/api/execution/guardrails/definitions'
+ );
+ });
+
+ it('sends the tenant header both products already send', async () => {
+ const fetchImpl = vi.fn(async () => jsonResponse([]));
+
+ renderHook(() =>
+ useGuardrailDefinitions(
+ ctxWith(fetchImpl, { token: 'jwt', accountId: 'acct-1', headers: { 'X-Extra': '1' } })
+ )
+ );
+
+ await waitFor(() => expect(fetchImpl).toHaveBeenCalled());
+ expect(fetchImpl.mock.calls[0]?.[1]?.headers).toEqual({
+ Accept: 'application/json',
+ Authorization: 'Bearer jwt',
+ 'X-UiPath-Internal-TenantId': 'tenant-1',
+ 'X-UiPath-Internal-AccountId': 'acct-1',
+ 'X-Extra': '1',
+ });
+ });
+
+ it('omits the auth header when the host relies on its session', async () => {
+ const fetchImpl = vi.fn(async () => jsonResponse([]));
+
+ renderHook(() => useGuardrailDefinitions(ctxWith(fetchImpl)));
+
+ await waitFor(() => expect(fetchImpl).toHaveBeenCalled());
+ expect(fetchImpl.mock.calls[0]?.[1]?.headers).not.toHaveProperty('Authorization');
+ });
+
+ it('surfaces a transport failure without throwing', async () => {
+ const fetchImpl = vi.fn(async () => jsonResponse(null, { ok: false, status: 503 }));
+
+ const { result } = renderHook(() => useGuardrailDefinitions(ctxWith(fetchImpl)));
+
+ await waitFor(() => expect(result.current.error).not.toBeNull());
+ expect(result.current.error?.message).toMatch(/503/);
+ expect(result.current.definitions).toEqual([]);
+ expect(result.current.loading).toBe(false);
+ });
+
+ it('surfaces a rejected request as an error', async () => {
+ const fetchImpl = vi.fn(async () => {
+ throw new Error('offline');
+ });
+
+ const { result } = renderHook(() => useGuardrailDefinitions(ctxWith(fetchImpl)));
+
+ await waitFor(() => expect(result.current.error?.message).toBe('offline'));
+ });
+
+ it('reports malformed definitions through `invalid`, not `error`', async () => {
+ const fetchImpl = vi.fn(async () => jsonResponse(RAW_PAYLOAD_WITH_NOISE));
+
+ const { result } = renderHook(() => useGuardrailDefinitions(ctxWith(fetchImpl)));
+
+ await waitFor(() => expect(result.current.loading).toBe(false));
+ expect(result.current.error).toBeNull();
+ expect(result.current.invalid).toHaveLength(1);
+ expect(result.current.definitions).toHaveLength(2);
+ });
+
+ it('reports a non-array payload through `inputError`', async () => {
+ const fetchImpl = vi.fn(async () => jsonResponse({ items: [] }));
+
+ const { result } = renderHook(() => useGuardrailDefinitions(ctxWith(fetchImpl)));
+
+ await waitFor(() => expect(result.current.inputError).toBeDefined());
+ expect(result.current.error).toBeNull();
+ });
+
+ it('refetches on demand', async () => {
+ const fetchImpl = vi.fn(async () => jsonResponse([PII_DETECTION_WIRE]));
+ const { result } = renderHook(() => useGuardrailDefinitions(ctxWith(fetchImpl)));
+ await waitFor(() => expect(result.current.loading).toBe(false));
+
+ result.current.refetch();
+
+ await waitFor(() => expect(fetchImpl).toHaveBeenCalledTimes(2));
+ });
+
+ it('fetches once for a context rebuilt inline on every render', async () => {
+ // Regression: keying the effect off context identity made every response set state,
+ // which rerendered, which built a new context, which fetched again, without end.
+ const fetchImpl = vi.fn(async () => jsonResponse([PII_DETECTION_WIRE]));
+
+ const { result, rerender } = renderHook(() => useGuardrailDefinitions(ctxWith(fetchImpl)));
+ await waitFor(() => expect(result.current.definitions).toHaveLength(1));
+ rerender();
+ rerender();
+
+ expect(fetchImpl).toHaveBeenCalledTimes(1);
+ });
+
+ it('refetches when the context contents actually change', async () => {
+ const fetchImpl = vi.fn(async () => jsonResponse([]));
+ const { rerender } = renderHook(
+ ({ tenantId }: { tenantId: string }) =>
+ useGuardrailDefinitions(ctxWith(fetchImpl, { tenantId })),
+ { initialProps: { tenantId: 'tenant-1' } }
+ );
+ await waitFor(() => expect(fetchImpl).toHaveBeenCalledTimes(1));
+
+ rerender({ tenantId: 'tenant-2' });
+
+ await waitFor(() => expect(fetchImpl).toHaveBeenCalledTimes(2));
+ expect(fetchImpl.mock.calls[1]?.[1]?.headers).toMatchObject({
+ 'X-UiPath-Internal-TenantId': 'tenant-2',
+ });
+ });
+
+ it('aborts the in-flight request on unmount', async () => {
+ const fetchImpl = vi.fn(async () => jsonResponse([]));
+ const { unmount } = renderHook(() => useGuardrailDefinitions(ctxWith(fetchImpl)));
+ await waitFor(() => expect(fetchImpl).toHaveBeenCalled());
+ const signal = fetchImpl.mock.calls[0]?.[1]?.signal;
+
+ unmount();
+
+ expect(signal?.aborted).toBe(true);
+ });
+ });
+
+ describe('disabled', () => {
+ it('makes no request for a null context', () => {
+ const fetchImpl = vi.fn(async () => jsonResponse([]));
+
+ const { result } = renderHook(() =>
+ useGuardrailDefinitions(null, { copy: GUARDRAIL_COPY_EN })
+ );
+
+ expect(fetchImpl).not.toHaveBeenCalled();
+ expect(result.current).toMatchObject({ definitions: [], loading: false, error: null });
+ });
+
+ it('clears previous results when the host disables it', async () => {
+ const fetchImpl = vi.fn(async () => jsonResponse([PII_DETECTION_WIRE]));
+ const ctx = ctxWith(fetchImpl);
+ const { result, rerender } = renderHook(
+ ({ enabled }: { enabled: boolean }) => useGuardrailDefinitions(enabled ? ctx : null),
+ { initialProps: { enabled: true } }
+ );
+ await waitFor(() => expect(result.current.definitions).toHaveLength(1));
+
+ rerender({ enabled: false });
+
+ // A stale list behind a disabled picker is worse than an empty one.
+ expect(result.current.definitions).toEqual([]);
+ });
+ });
+
+ describe('definitions override', () => {
+ it('parses and enriches a host-supplied payload without fetching', () => {
+ const fetchImpl = vi.fn(async () => jsonResponse([]));
+
+ const { result } = renderHook(() =>
+ useGuardrailDefinitions(ctxWith(fetchImpl), { definitions: [PII_DETECTION_WIRE] })
+ );
+
+ expect(fetchImpl).not.toHaveBeenCalled();
+ expect(result.current.definitions[0]?.displayName).toBe('PII detection');
+ expect(result.current.loading).toBe(false);
+ });
+
+ it('works with a null context, the Flow vsix shape', () => {
+ const { result } = renderHook(() =>
+ useGuardrailDefinitions(null, { definitions: [BYO_WIRE] })
+ );
+
+ expect(result.current.definitions[0]?.displayName).toBe('Acme PII scan');
+ });
+
+ it('reports a malformed host payload the same way a fetched one is reported', () => {
+ const { result } = renderHook(() =>
+ useGuardrailDefinitions(null, { definitions: 'not an array' })
+ );
+
+ expect(result.current.inputError).toMatch(/Expected an array/);
+ expect(result.current.definitions).toEqual([]);
+ });
+ });
+
+ describe('options', () => {
+ it('hides the validators the host names', () => {
+ const { result } = renderHook(() =>
+ useGuardrailDefinitions(null, {
+ definitions: [PROMPT_INJECTION_WIRE, PII_DETECTION_WIRE],
+ hiddenValidators: ['prompt_injection'],
+ })
+ );
+
+ expect(result.current.definitions.map((d) => d.validator)).toEqual(['pii_detection']);
+ // `wire` stays complete: hiding is a display decision, not a data one.
+ expect(result.current.wire).toHaveLength(2);
+ });
+
+ it('compares hiddenValidators by content, so an inline array does not churn', () => {
+ // `definitions` is held stable here so the only inline value under test is
+ // `hiddenValidators`; re-enriching on every render would make the returned array a new
+ // reference each time and rerender every consumer downstream.
+ const definitions = [PII_DETECTION_WIRE];
+ const { result, rerender } = renderHook(() =>
+ useGuardrailDefinitions(null, { definitions, hiddenValidators: ['prompt_injection'] })
+ );
+ const first = result.current.definitions;
+
+ rerender();
+
+ expect(result.current.definitions).toBe(first);
+ });
+
+ it('resolves copy from an override table', () => {
+ const copy: GuardrailCopyTable = {
+ pii_detection: {
+ displayName: 'Datenschutz',
+ description: 'Beschreibung',
+ paramLabels: {},
+ },
+ };
+
+ const { result } = renderHook(() =>
+ useGuardrailDefinitions(null, { definitions: [PII_DETECTION_WIRE], copy })
+ );
+
+ expect(result.current.definitions[0]?.displayName).toBe('Datenschutz');
+ });
+ });
+});
diff --git a/packages/apollo-react/src/canvas/components/Guardrails/use-guardrail-definitions.ts b/packages/apollo-react/src/canvas/components/Guardrails/use-guardrail-definitions.ts
new file mode 100644
index 0000000000..20bd20c242
--- /dev/null
+++ b/packages/apollo-react/src/canvas/components/Guardrails/use-guardrail-definitions.ts
@@ -0,0 +1,210 @@
+import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
+import { type GuardrailCopyTable, useGuardrailDefinitionCopy } from './definitions-copy';
+import { type EnrichedGuardrailDefinition, enrichGuardrailDefinitions } from './definitions-enrich';
+import {
+ type GuardrailDefinitionParseIssue,
+ type GuardrailDefinitionsParseResult,
+ parseGuardrailDefinitions,
+} from './definitions-parse';
+import type { GuardrailDefinitionWire } from './definitions-wire';
+
+/** Path of the definitions endpoint, appended to `baseUrl`. */
+export const GUARDRAIL_DEFINITIONS_PATH = '/api/execution/guardrails/definitions';
+
+export interface GuardrailDefinitionsRequestContext {
+ /**
+ * Base URL of the agents runtime. Same-origin when omitted. Agents passes `''`; Flow
+ * passes its `/{org}/{tenantName}/agents_` prefix.
+ */
+ baseUrl?: string;
+ /** Bearer token, no `Bearer ` prefix. Omit when the host's session already authorizes. */
+ token?: string;
+ /** Sent as `X-UiPath-Internal-TenantId`; both products send it today. */
+ tenantId: string;
+ /** Sent as `X-UiPath-Internal-AccountId` when the host scopes by account. */
+ accountId?: string;
+ /** Merged last, so a host can add or override any header. */
+ headers?: Record;
+ /** Injectable fetch for tests and hosts with an instrumented client. */
+ fetch?: typeof fetch;
+}
+
+export interface UseGuardrailDefinitionsOptions {
+ /**
+ * A payload the host already has. When present no request is made at all: the value is
+ * parsed and enriched instead. This is the seam that lets each product keep its own
+ * transport (Agents' SWR, Flow studio and workbench's react-query, Flow vsix's
+ * postMessage, which never fetches).
+ */
+ definitions?: unknown;
+ /**
+ * Non-BYO validators to hide. Compared by content, so an inline array literal is fine.
+ */
+ hiddenValidators?: readonly string[];
+ /** Override the lingui-backed copy table, e.g. to pin English in a test. */
+ copy?: GuardrailCopyTable;
+}
+
+export interface UseGuardrailDefinitionsResult {
+ /** Display-ready definitions, ready to hand to `GuardrailBuilder`. */
+ definitions: EnrichedGuardrailDefinition[];
+ /** The validated wire definitions behind them, for hosts that need the raw fields. */
+ wire: GuardrailDefinitionWire[];
+ /** Definitions dropped during validation. Surface as a status banner, not an error page. */
+ invalid: GuardrailDefinitionParseIssue[];
+ /** Set when the payload was not an array at all. */
+ inputError?: string;
+ loading: boolean;
+ /** Transport failure. Validation failures are reported through `invalid`, not here. */
+ error: Error | null;
+ refetch: () => void;
+}
+
+const EMPTY_RESULT: GuardrailDefinitionsParseResult = { definitions: [], invalid: [] };
+
+/** The serializable half of the context: everything that decides what request to make. */
+type GuardrailDefinitionsRequest = Omit;
+
+/**
+ * Load the guardrail definitions and resolve their display copy.
+ *
+ * Deliberately minimal, following `useDiscoveryModels` (the ModelPicker precedent): plain
+ * `useState` plus `fetch` plus an `AbortController`, no query library. Hosts that already run
+ * SWR or React Query should keep doing so and feed the payload in through
+ * `options.definitions` rather than adopting a second cache.
+ *
+ * Passing `null` as the context disables the hook and clears its state; combined with
+ * `options.definitions` that is the pure "enrich what I already have" path.
+ *
+ * Both the context and `hiddenValidators` are compared by **content**, not identity, so a
+ * host can build them inline at the call site. This is a deliberate departure from
+ * `useDiscoveryModels`, which depends on context identity: there, an inline object refetches
+ * on every render, and since every response sets state the loop never terminates.
+ */
+export function useGuardrailDefinitions(
+ ctx: GuardrailDefinitionsRequestContext | null,
+ options: UseGuardrailDefinitionsOptions = {}
+): UseGuardrailDefinitionsResult {
+ const { definitions: provided, hiddenValidators, copy: copyOverride } = options;
+
+ const linguiCopy = useGuardrailDefinitionCopy();
+ const copy = copyOverride ?? linguiCopy;
+
+ // Hosts build this object inline at the call site, so keying the request off its identity
+ // would refetch on every render and, because each response sets state, never stop. The
+ // request keys off the context's contents instead. `fetch` stays out of the key and is read
+ // from a ref: swapping an inline wrapper is not a reason to call the API again.
+ const requestKey =
+ ctx === null
+ ? ''
+ : JSON.stringify({
+ baseUrl: ctx.baseUrl,
+ token: ctx.token,
+ tenantId: ctx.tenantId,
+ accountId: ctx.accountId,
+ headers: ctx.headers,
+ });
+ const request = useMemo(
+ () => (requestKey === '' ? null : (JSON.parse(requestKey) as GuardrailDefinitionsRequest)),
+ [requestKey]
+ );
+ const fetchRef = useRef(ctx?.fetch);
+ fetchRef.current = ctx?.fetch;
+
+ // A host-provided payload replaces the request entirely rather than racing it.
+ const enabled = provided === undefined && request !== null;
+
+ const [fetched, setFetched] = useState(EMPTY_RESULT);
+ const [loading, setLoading] = useState(false);
+ const [error, setError] = useState(null);
+ const abortRef = useRef(null);
+
+ const load = useCallback(async () => {
+ if (request === null) return;
+ abortRef.current?.abort();
+ const controller = new AbortController();
+ abortRef.current = controller;
+
+ setLoading(true);
+ setError(null);
+
+ const base = (request.baseUrl ?? '').replace(/\/$/, '');
+ const headers: Record = {
+ Accept: 'application/json',
+ 'X-UiPath-Internal-TenantId': request.tenantId,
+ };
+ if (request.token) headers.Authorization = `Bearer ${request.token}`;
+ if (request.accountId) headers['X-UiPath-Internal-AccountId'] = request.accountId;
+ Object.assign(headers, request.headers);
+
+ // Called through a wrapper rather than as a bare reference so a detached global `fetch`
+ // keeps its receiver.
+ const doFetch: typeof fetch =
+ fetchRef.current ?? ((input, init) => globalThis.fetch(input, init));
+
+ try {
+ const response = await doFetch(`${base}${GUARDRAIL_DEFINITIONS_PATH}`, {
+ headers,
+ signal: controller.signal,
+ });
+ if (!response.ok) {
+ throw new Error(`Guardrail definitions API ${response.status} ${response.statusText}`);
+ }
+ const payload: unknown = await response.json();
+ if (!controller.signal.aborted) setFetched(parseGuardrailDefinitions(payload));
+ } catch (err: unknown) {
+ if ((err as { name?: string })?.name === 'AbortError') return;
+ if (!controller.signal.aborted) {
+ setError(err instanceof Error ? err : new Error(String(err)));
+ }
+ } finally {
+ if (!controller.signal.aborted) setLoading(false);
+ }
+ }, [request]);
+
+ useEffect(() => {
+ if (!enabled) {
+ // Disabled: abort anything in flight and drop previous results, so a host that
+ // switches to its own payload never renders stale data or a stuck spinner.
+ abortRef.current?.abort();
+ setFetched(EMPTY_RESULT);
+ setLoading(false);
+ setError(null);
+ return undefined;
+ }
+ load();
+ return () => abortRef.current?.abort();
+ }, [enabled, load]);
+
+ const parsed = useMemo(
+ () => (provided === undefined ? fetched : parseGuardrailDefinitions(provided)),
+ [provided, fetched]
+ );
+
+ // Round-tripping through a joined key keeps an inline `['prompt_injection']` from
+ // re-enriching on every render, without asking hosts to memoize it.
+ const hiddenValidatorsKey = (hiddenValidators ?? []).join('\u0000');
+ const stableHiddenValidators = useMemo(
+ () => (hiddenValidatorsKey === '' ? [] : hiddenValidatorsKey.split('\u0000')),
+ [hiddenValidatorsKey]
+ );
+
+ const definitions = useMemo(
+ () =>
+ enrichGuardrailDefinitions(parsed.definitions, {
+ copy,
+ hiddenValidators: stableHiddenValidators,
+ }),
+ [parsed.definitions, copy, stableHiddenValidators]
+ );
+
+ return {
+ definitions,
+ wire: parsed.definitions,
+ invalid: parsed.invalid,
+ inputError: parsed.inputError,
+ loading,
+ error,
+ refetch: load,
+ };
+}
diff --git a/packages/apollo-react/src/canvas/components/Guardrails/utils.test.ts b/packages/apollo-react/src/canvas/components/Guardrails/utils.test.ts
new file mode 100644
index 0000000000..98911ace91
--- /dev/null
+++ b/packages/apollo-react/src/canvas/components/Guardrails/utils.test.ts
@@ -0,0 +1,381 @@
+import { describe, expect, it } from 'vitest';
+import type { GuardrailParameterDefinition, GuardrailValidatorParameter } from './types';
+import {
+ dropEmptyOptionalParameters,
+ getRequiredEmptyParameterIds,
+ seedGuardrailParameters,
+ syncMapEnumParameters,
+} from './utils';
+
+describe('seedGuardrailParameters', () => {
+ const definitions: GuardrailParameterDefinition[] = [
+ { id: 'prompt', type: 'text', label: 'Prompt', required: true, defaultValue: null },
+ { id: 'model', type: 'enum', label: 'Model', required: true, defaultValue: null },
+ { id: 'examples', type: 'text-list', label: 'Examples', required: false, defaultValue: null },
+ { id: 'strict', type: 'boolean', label: 'Strict', required: false, defaultValue: null },
+ { id: 'threshold', type: 'number', label: 'Threshold', required: true, defaultValue: 0.7 },
+ {
+ id: 'entities',
+ type: 'enum-list',
+ label: 'Entities',
+ required: true,
+ defaultValue: ['Email'],
+ options: ['Email', 'Person'],
+ },
+ ];
+
+ it('returns existing parameters verbatim when editing', () => {
+ const existing: GuardrailValidatorParameter[] = [
+ { $parameterType: 'text', id: 'prompt', value: 'hello' },
+ ];
+ expect(seedGuardrailParameters(definitions, existing)).toBe(existing);
+ });
+
+ it('seeds one entry per definition from its defaultValue', () => {
+ const seeded = seedGuardrailParameters(definitions);
+ expect(seeded).toHaveLength(definitions.length);
+ expect(seeded.find((p) => p.id === 'threshold')).toEqual({
+ $parameterType: 'number',
+ id: 'threshold',
+ value: 0.7,
+ });
+ expect(seeded.find((p) => p.id === 'entities')).toEqual({
+ $parameterType: 'enum-list',
+ id: 'entities',
+ value: ['Email'],
+ });
+ });
+
+ it('coerces null defaults: text and enum to empty string', () => {
+ const seeded = seedGuardrailParameters(definitions);
+ expect(seeded.find((p) => p.id === 'prompt')).toEqual({
+ $parameterType: 'text',
+ id: 'prompt',
+ value: '',
+ });
+ expect(seeded.find((p) => p.id === 'model')).toEqual({
+ $parameterType: 'enum',
+ id: 'model',
+ value: '',
+ });
+ });
+
+ it('coerces null defaults: text-list to empty array', () => {
+ expect(seedGuardrailParameters(definitions).find((p) => p.id === 'examples')).toEqual({
+ $parameterType: 'text-list',
+ id: 'examples',
+ value: [],
+ });
+ });
+
+ it('coerces null defaults: boolean to false', () => {
+ expect(seedGuardrailParameters(definitions).find((p) => p.id === 'strict')).toEqual({
+ $parameterType: 'boolean',
+ id: 'strict',
+ value: false,
+ });
+ });
+});
+
+describe('syncMapEnumParameters', () => {
+ // Mirrors the PII detection validator: an `entities` enum-list and an `entityThresholds`
+ // map-enum whose default map covers every possible entity.
+ const ALL_ENTITIES = ['Person', 'Address', 'Email', 'CreditCardNumber'];
+ const entitiesDef: GuardrailParameterDefinition = {
+ id: 'entities',
+ type: 'enum-list',
+ label: 'Entities to detect',
+ required: true,
+ defaultValue: ['Email', 'Address'],
+ options: ALL_ENTITIES,
+ };
+ const thresholdsDef: GuardrailParameterDefinition = {
+ id: 'entityThresholds',
+ type: 'map-enum',
+ label: 'Detection thresholds',
+ required: false,
+ keySource: 'entities',
+ min: 0,
+ max: 1,
+ step: 0.1,
+ defaultValue: Object.fromEntries(ALL_ENTITIES.map((e) => [e, 0.5])),
+ };
+ const paramDefs = [entitiesDef, thresholdsDef];
+
+ function getMap(params: GuardrailValidatorParameter[]): Record {
+ const p = params.find((x) => x.id === 'entityThresholds');
+ if (p?.$parameterType !== 'map-enum') throw new Error('expected map-enum param');
+ return p.value;
+ }
+
+ it('prunes the threshold map down to the selected entities (the default-config bug)', () => {
+ const params: GuardrailValidatorParameter[] = [
+ { $parameterType: 'enum-list', id: 'entities', value: ['Email', 'Address'] },
+ {
+ $parameterType: 'map-enum',
+ id: 'entityThresholds',
+ value: thresholdsDef.defaultValue as Record,
+ },
+ ];
+
+ const synced = syncMapEnumParameters(params, paramDefs);
+
+ expect(Object.keys(getMap(synced)).sort()).toEqual(['Address', 'Email']);
+ expect(getMap(synced)).toEqual({ Email: 0.5, Address: 0.5 });
+ });
+
+ it('preserves user-edited threshold values', () => {
+ const params: GuardrailValidatorParameter[] = [
+ { $parameterType: 'enum-list', id: 'entities', value: ['Email', 'Address'] },
+ {
+ $parameterType: 'map-enum',
+ id: 'entityThresholds',
+ value: { ...(thresholdsDef.defaultValue as Record), Email: 0.9 },
+ },
+ ];
+
+ expect(getMap(syncMapEnumParameters(params, paramDefs))).toEqual({ Email: 0.9, Address: 0.5 });
+ });
+
+ it('rebuilds the map from defaults when the persisted value is array-shaped', () => {
+ const params: GuardrailValidatorParameter[] = [
+ { $parameterType: 'enum-list', id: 'entities', value: ['Email', 'Address'] },
+ {
+ $parameterType: 'map-enum',
+ id: 'entityThresholds',
+ value: [0.7, 0.3] as unknown as Record,
+ },
+ ];
+
+ // A malformed array must not be indexed into as a map; every key falls to its default.
+ expect(getMap(syncMapEnumParameters(params, paramDefs))).toEqual({ Email: 0.5, Address: 0.5 });
+ });
+
+ it('drops thresholds for entities removed from the selection', () => {
+ const params: GuardrailValidatorParameter[] = [
+ { $parameterType: 'enum-list', id: 'entities', value: ['Email'] },
+ { $parameterType: 'map-enum', id: 'entityThresholds', value: { Email: 0.7, Address: 0.3 } },
+ ];
+
+ expect(getMap(syncMapEnumParameters(params, paramDefs))).toEqual({ Email: 0.7 });
+ });
+
+ it('fills a newly selected entity using the per-entity default, then min', () => {
+ const params: GuardrailValidatorParameter[] = [
+ { $parameterType: 'enum-list', id: 'entities', value: ['Email', 'Person'] },
+ { $parameterType: 'map-enum', id: 'entityThresholds', value: { Email: 0.7 } },
+ ];
+
+ // Person comes from the default map (0.5); Email keeps its edited value.
+ expect(getMap(syncMapEnumParameters(params, paramDefs))).toEqual({ Email: 0.7, Person: 0.5 });
+ });
+
+ it('falls back to min when an entity is in neither the current map nor the defaults', () => {
+ const sparseThresholdsDef: GuardrailParameterDefinition = {
+ ...thresholdsDef,
+ defaultValue: {},
+ min: 0.2,
+ };
+ const params: GuardrailValidatorParameter[] = [
+ { $parameterType: 'enum-list', id: 'entities', value: ['Email'] },
+ { $parameterType: 'map-enum', id: 'entityThresholds', value: {} },
+ ];
+
+ expect(getMap(syncMapEnumParameters(params, [entitiesDef, sparseThresholdsDef]))).toEqual({
+ Email: 0.2,
+ });
+ });
+
+ it('falls back to the source definition default selection when no selection is stored', () => {
+ const params: GuardrailValidatorParameter[] = [
+ {
+ $parameterType: 'map-enum',
+ id: 'entityThresholds',
+ value: thresholdsDef.defaultValue as Record,
+ },
+ ];
+
+ expect(Object.keys(getMap(syncMapEnumParameters(params, paramDefs))).sort()).toEqual([
+ 'Address',
+ 'Email',
+ ]);
+ });
+
+ it('leaves non map-enum parameters untouched', () => {
+ const params: GuardrailValidatorParameter[] = [
+ { $parameterType: 'enum-list', id: 'entities', value: ['Email'] },
+ { $parameterType: 'text', id: 'note', value: 'hello' },
+ ];
+
+ expect(syncMapEnumParameters(params, paramDefs)).toEqual(params);
+ });
+
+ it('leaves a map-enum without a keySource untouched', () => {
+ const noSourceDef: GuardrailParameterDefinition = {
+ id: 'entityThresholds',
+ type: 'map-enum',
+ label: 'Thresholds',
+ required: false,
+ defaultValue: {},
+ };
+ const params: GuardrailValidatorParameter[] = [
+ { $parameterType: 'map-enum', id: 'entityThresholds', value: { Email: 0.7 } },
+ ];
+
+ expect(syncMapEnumParameters(params, [noSourceDef])).toEqual(params);
+ });
+
+ // The logic is generic over any enum-list + map-enum pair, not just PII. This mirrors the
+ // harmful_content shape (different param ids, integer 0-6 thresholds).
+ it('prunes harmful-content thresholds to the selected content categories', () => {
+ const hcEntitiesDef: GuardrailParameterDefinition = {
+ id: 'harmfulContentEntities',
+ type: 'enum-list',
+ label: 'Content categories',
+ required: true,
+ defaultValue: ['Hate', 'SelfHarm'],
+ options: ['Hate', 'SelfHarm', 'Sexual', 'Violence'],
+ };
+ const hcThresholdsDef: GuardrailParameterDefinition = {
+ id: 'harmfulContentEntityThresholds',
+ type: 'map-enum',
+ label: 'Severity thresholds',
+ required: true,
+ keySource: 'harmfulContentEntities',
+ min: 0,
+ max: 6,
+ step: 2,
+ defaultValue: { Hate: 2, SelfHarm: 2, Sexual: 2, Violence: 2 },
+ };
+ const params: GuardrailValidatorParameter[] = [
+ { $parameterType: 'enum-list', id: 'harmfulContentEntities', value: ['Hate', 'SelfHarm'] },
+ {
+ $parameterType: 'map-enum',
+ id: 'harmfulContentEntityThresholds',
+ value: hcThresholdsDef.defaultValue as Record,
+ },
+ ];
+
+ const synced = syncMapEnumParameters(params, [hcEntitiesDef, hcThresholdsDef]);
+ const map = synced.find((p) => p.id === 'harmfulContentEntityThresholds');
+ if (map?.$parameterType !== 'map-enum') throw new Error('expected map-enum param');
+ expect(map.value).toEqual({ Hate: 2, SelfHarm: 2 });
+ });
+});
+
+describe('dropEmptyOptionalParameters', () => {
+ const definitions: GuardrailParameterDefinition[] = [
+ { id: 'prompt', type: 'text', label: 'Prompt', required: true, defaultValue: null },
+ { id: 'note', type: 'text', label: 'Note', required: false, defaultValue: null },
+ { id: 'examples', type: 'text-list', label: 'Examples', required: false, defaultValue: null },
+ { id: 'strict', type: 'boolean', label: 'Strict', required: false, defaultValue: false },
+ ];
+
+ it('drops optional parameters with empty string or empty array values', () => {
+ const params: GuardrailValidatorParameter[] = [
+ { $parameterType: 'text', id: 'prompt', value: '' },
+ { $parameterType: 'text', id: 'note', value: '' },
+ { $parameterType: 'text-list', id: 'examples', value: [] },
+ { $parameterType: 'boolean', id: 'strict', value: false },
+ ];
+
+ const result = dropEmptyOptionalParameters(params, definitions);
+ expect(result.map((p) => p.id)).toEqual(['prompt', 'strict']);
+ });
+
+ it('keeps optional parameters with values, and unknown parameters', () => {
+ const params: GuardrailValidatorParameter[] = [
+ { $parameterType: 'text', id: 'note', value: 'keep me' },
+ { $parameterType: 'text', id: 'unknownParam', value: '' },
+ ];
+
+ const result = dropEmptyOptionalParameters(params, definitions);
+ expect(result.map((p) => p.id)).toEqual(['note', 'unknownParam']);
+ });
+});
+
+describe('getRequiredEmptyParameterIds', () => {
+ const definitions: GuardrailParameterDefinition[] = [
+ { id: 'prompt', type: 'text', label: 'Prompt', required: true, defaultValue: null },
+ { id: 'model', type: 'enum', label: 'Model', required: true, defaultValue: null },
+ {
+ id: 'entities',
+ type: 'enum-list',
+ label: 'Entities',
+ required: true,
+ defaultValue: [],
+ options: ['Email'],
+ },
+ { id: 'examples', type: 'text-list', label: 'Examples', required: true, defaultValue: null },
+ {
+ id: 'thresholds',
+ type: 'map-enum',
+ label: 'Thresholds',
+ required: true,
+ keySource: 'entities',
+ defaultValue: {},
+ },
+ { id: 'threshold', type: 'number', label: 'Threshold', required: true, defaultValue: 0.5 },
+ { id: 'strict', type: 'boolean', label: 'Strict', required: true, defaultValue: false },
+ { id: 'note', type: 'text', label: 'Note', required: false, defaultValue: null },
+ ];
+
+ it('reports every required parameter whose value is empty', () => {
+ const params: GuardrailValidatorParameter[] = [
+ { $parameterType: 'text', id: 'prompt', value: ' ' },
+ { $parameterType: 'enum', id: 'model', value: '' },
+ { $parameterType: 'enum-list', id: 'entities', value: [] },
+ { $parameterType: 'text-list', id: 'examples', value: ['', ' '] },
+ { $parameterType: 'map-enum', id: 'thresholds', value: {} },
+ { $parameterType: 'number', id: 'threshold', value: 0 },
+ { $parameterType: 'boolean', id: 'strict', value: false },
+ { $parameterType: 'text', id: 'note', value: '' },
+ ];
+
+ expect(getRequiredEmptyParameterIds(definitions, params)).toEqual([
+ 'prompt',
+ 'model',
+ 'entities',
+ 'examples',
+ 'thresholds',
+ ]);
+ });
+
+ it('treats a missing required parameter as empty', () => {
+ expect(getRequiredEmptyParameterIds(definitions, [])).toEqual([
+ 'prompt',
+ 'model',
+ 'entities',
+ 'examples',
+ 'thresholds',
+ 'threshold',
+ 'strict',
+ ]);
+ });
+
+ it('returns no ids when every required parameter is filled', () => {
+ const params: GuardrailValidatorParameter[] = [
+ { $parameterType: 'text', id: 'prompt', value: 'check tone' },
+ { $parameterType: 'enum', id: 'model', value: 'model-a' },
+ { $parameterType: 'enum-list', id: 'entities', value: ['Email'] },
+ { $parameterType: 'text-list', id: 'examples', value: ['one filled', ''] },
+ { $parameterType: 'map-enum', id: 'thresholds', value: { Email: 0.5 } },
+ { $parameterType: 'number', id: 'threshold', value: 0 },
+ { $parameterType: 'boolean', id: 'strict', value: false },
+ ];
+
+ expect(getRequiredEmptyParameterIds(definitions, params)).toEqual([]);
+ });
+
+ it('never reports numbers or booleans once present, even falsy ones', () => {
+ const defs: GuardrailParameterDefinition[] = [
+ { id: 'threshold', type: 'number', label: 'Threshold', required: true, defaultValue: 1 },
+ { id: 'strict', type: 'boolean', label: 'Strict', required: true, defaultValue: true },
+ ];
+ const params: GuardrailValidatorParameter[] = [
+ { $parameterType: 'number', id: 'threshold', value: 0 },
+ { $parameterType: 'boolean', id: 'strict', value: false },
+ ];
+ expect(getRequiredEmptyParameterIds(defs, params)).toEqual([]);
+ });
+});
diff --git a/packages/apollo-react/src/canvas/components/Guardrails/utils.ts b/packages/apollo-react/src/canvas/components/Guardrails/utils.ts
new file mode 100644
index 0000000000..68650745c8
--- /dev/null
+++ b/packages/apollo-react/src/canvas/components/Guardrails/utils.ts
@@ -0,0 +1,147 @@
+import type { GuardrailParameterDefinition, GuardrailValidatorParameter } from './types';
+
+/**
+ * Seed parameter values for a guardrail: existing values verbatim when editing, otherwise
+ * one entry per definition from its `defaultValue`. The wire schema permits
+ * `defaultValue: null` for text / enum / text-list / boolean; coerce so the resulting
+ * parameter satisfies the union (`value` is never null at runtime).
+ */
+export function seedGuardrailParameters(
+ definitions: GuardrailParameterDefinition[],
+ existing?: GuardrailValidatorParameter[]
+): GuardrailValidatorParameter[] {
+ if (existing) return existing;
+ return definitions.map((paramDef) => {
+ let value: unknown = paramDef.defaultValue;
+ if (paramDef.type === 'text' || paramDef.type === 'enum') {
+ if (value == null) value = '';
+ } else if (paramDef.type === 'text-list') {
+ if (value == null) value = [];
+ } else if (paramDef.type === 'boolean') {
+ if (value == null) value = false;
+ }
+ return {
+ $parameterType: paramDef.type,
+ id: paramDef.id,
+ value,
+ } as GuardrailValidatorParameter;
+ });
+}
+
+/**
+ * Keep `map-enum` parameters (e.g. PII entity thresholds) in lockstep with the selection of
+ * their source `enum-list` parameter (e.g. the selected entities). The backend ships a
+ * default threshold map covering *every* possible entity, so an untouched guardrail would
+ * otherwise persist dozens of thresholds while only a few entities are selected — and stale
+ * thresholds would linger after an entity is deselected. Returns parameters whose map keys
+ * exactly match the current selection, preserving user-edited values (falling back to the
+ * per-entity default, then the parameter's `min`, then 0).
+ *
+ * Mirrors the map-enum editor's key resolution exactly so the persisted map matches the
+ * rendered rows. Call at save time.
+ */
+export function syncMapEnumParameters(
+ parameters: GuardrailValidatorParameter[],
+ definitions: GuardrailParameterDefinition[]
+): GuardrailValidatorParameter[] {
+ const selectionBySourceId = new Map();
+ for (const param of parameters) {
+ // Defend against non-array wire values: the type says `string[]`, but persisted data
+ // could be null/malformed, which would corrupt syncing downstream.
+ if (param.$parameterType === 'enum-list' && Array.isArray(param.value)) {
+ selectionBySourceId.set(param.id, param.value);
+ }
+ }
+ const defById = new Map(definitions.map((d) => [d.id, d]));
+
+ return parameters.map((param) => {
+ if (param.$parameterType !== 'map-enum') return param;
+ const def = defById.get(param.id);
+ const keySource = def?.keySource;
+ if (!keySource) return param;
+ const sourceDef = defById.get(keySource);
+ const allowedKeys =
+ selectionBySourceId.get(keySource) ??
+ (Array.isArray(sourceDef?.defaultValue) ? (sourceDef.defaultValue as string[]) : undefined);
+ if (!allowedKeys) return param;
+
+ // Guard against a null/array/non-object persisted map before indexing into it.
+ const currentMap =
+ param.value && typeof param.value === 'object' && !Array.isArray(param.value)
+ ? param.value
+ : {};
+ const defaults = (def?.defaultValue as Record | undefined) ?? {};
+ const fallback = def?.min ?? 0;
+ const synced: Record = {};
+ for (const key of allowedKeys) {
+ synced[key] = currentMap[key] ?? defaults[key] ?? fallback;
+ }
+ return { ...param, value: synced };
+ });
+}
+
+/**
+ * Drop optional parameters whose value is an empty string or an empty array. Runtimes
+ * typically require persisted values to be non-empty, so an empty optional value would
+ * reject the configuration at publish/run time. Required parameters are kept (empty) so the
+ * form can surface their error.
+ */
+export function dropEmptyOptionalParameters(
+ parameters: readonly GuardrailValidatorParameter[],
+ definitions: readonly GuardrailParameterDefinition[]
+): GuardrailValidatorParameter[] {
+ return parameters.filter((param) => {
+ const parameterDefinition = definitions.find((p) => p.id === param.id);
+ if (!parameterDefinition || parameterDefinition.required) {
+ return true;
+ }
+ const isEmpty =
+ (typeof param.value === 'string' && param.value === '') ||
+ (Array.isArray(param.value) && param.value.length === 0);
+ return !isEmpty;
+ });
+}
+
+/**
+ * Ids of required parameters whose current value counts as empty: a missing entry, a
+ * blank/whitespace-only text or enum, an empty enum-list, a text-list whose rows are all
+ * blank, or a map-enum with no keys. Numbers and booleans are never empty once present.
+ *
+ * This is the validation predicate matching what the form's editors visually treat as
+ * unfilled — hosts map the returned ids to their own error messages.
+ */
+export function getRequiredEmptyParameterIds(
+ definitions: readonly GuardrailParameterDefinition[],
+ parameters: readonly GuardrailValidatorParameter[]
+): string[] {
+ const emptyIds: string[] = [];
+ for (const paramDef of definitions) {
+ if (!paramDef.required) continue;
+ const param = parameters.find((p) => p.id === paramDef.id);
+ if (!param) {
+ emptyIds.push(paramDef.id);
+ continue;
+ }
+ const value = param.value;
+ let isEmpty = false;
+ if (paramDef.type === 'enum-list' && Array.isArray(value)) {
+ isEmpty = value.length === 0;
+ } else if (paramDef.type === 'text-list' && Array.isArray(value)) {
+ isEmpty = value.every((entry) => typeof entry === 'string' && entry.trim().length === 0);
+ } else if (
+ (paramDef.type === 'text' || paramDef.type === 'enum') &&
+ typeof value === 'string'
+ ) {
+ isEmpty = value.trim().length === 0;
+ } else if (
+ paramDef.type === 'map-enum' &&
+ typeof value === 'object' &&
+ value !== null &&
+ !Array.isArray(value)
+ ) {
+ isEmpty = Object.keys(value).length === 0;
+ }
+ if (isEmpty) emptyIds.push(paramDef.id);
+ }
+ return emptyIds;
+}
diff --git a/packages/apollo-react/src/canvas/components/index.ts b/packages/apollo-react/src/canvas/components/index.ts
index 27e8880aa9..6dfcc5c0e4 100644
--- a/packages/apollo-react/src/canvas/components/index.ts
+++ b/packages/apollo-react/src/canvas/components/index.ts
@@ -17,6 +17,7 @@ export * from './Edges';
export * from './ExecutionStatusIcon';
export * from './FloatingCanvasPanel';
export * from './GroupNode';
+export * from './Guardrails';
export * from './HierarchicalCanvas';
export * from './JsonTree';
export * from './LoopNode';
diff --git a/packages/apollo-react/src/canvas/locales/de.json b/packages/apollo-react/src/canvas/locales/de.json
index 296d9cc677..d145f1443f 100644
--- a/packages/apollo-react/src/canvas/locales/de.json
+++ b/packages/apollo-react/src/canvas/locales/de.json
@@ -150,5 +150,148 @@
"canvas.node_mode_select.simulated_description": "Antwort dynamisch mit einem LLM generieren",
"canvas.node_mode_select.simulated_label": "Simuliert",
"canvas.node_mode_select.static_description": "Immer einen von Ihnen definierten Wert zurückgeben",
- "canvas.node_mode_select.static_label": "Statisches Pseudo-Objekt"
+ "canvas.node_mode_select.static_label": "Statisches Pseudo-Objekt",
+ "guardrails.form.more-information": "Weitere Informationen",
+ "guardrails.form.enum-placeholder": "Auswählen…",
+ "guardrails.form.enum-list-placeholder": "Optionen auswählen…",
+ "guardrails.form.add-item": "Hinzufügen",
+ "guardrails.form.remove-item": "{label} {position} entfernen",
+ "guardrails.builder.edit-title": "Leitplanke {name} bearbeiten",
+ "guardrails.builder.add-title": "Leitplanke {name} hinzufügen",
+ "guardrails.builder.type-label": "Leitplankentyp",
+ "guardrails.builder.name-label": "Leitplankenname",
+ "guardrails.builder.name-placeholder": "Leitplankennamen eingeben",
+ "guardrails.builder.description-label": "Leitplankenbeschreibung",
+ "guardrails.builder.description-placeholder": "Beschreibung der Leitplanke eingeben",
+ "guardrails.builder.evals-label": "Leitplanke für Auswertungen aktivieren",
+ "guardrails.builder.evals-info-aria-label": "Weitere Informationen",
+ "guardrails.builder.evals-tooltip": "Wenn diese Option aktiviert ist, wird diese Leitplanke während der Auswertungsausführungen angewendet.",
+ "guardrails.builder.save-as-new": "Als neu speichern",
+ "guardrails.builder.cancel": "Abbrechen",
+ "guardrails.builder.save": "Speichern",
+ "guardrails.builder.byo-disabled-message": "Die Konfiguration dieser Leitplanke wurde deaktiviert und kann nicht mehr verwendet werden. Wenden Sie sich an Ihren Administrator, um die Konfiguration wieder zu aktivieren oder diese Leitplanke zu ersetzen, bevor Sie den Agent ausführen.",
+ "guardrails.builder.unauthorized-message": "Sie sind nicht berechtigt, Leitplanken zu verwenden. Sie können auf die Konfigurationseinstellungen zugreifen, aber Änderungen können nicht gespeichert werden.",
+ "guardrails.builder.feature-disabled-message": "Diese Leitplankenfunktion ist deaktiviert. Sie können auf die Konfigurationseinstellungen zugreifen, aber Änderungen können nicht gespeichert werden. Am besten entfernen Sie diese Leitplanke, solange die Funktion deaktiviert ist.",
+ "guardrails.builder.scopes-label": "Scopes",
+ "guardrails.builder.tools-label": "Tools",
+ "guardrails.builder.scope-agent-label": "Agent",
+ "guardrails.builder.scope-llm-label": "LLM-Aufrufe",
+ "guardrails.builder.scope-tool-label": "Tools",
+ "guardrails.builder.action-type-label": "Aktionstyp",
+ "guardrails.builder.action-log-label": "Protokoll",
+ "guardrails.builder.action-block-label": "Block",
+ "guardrails.builder.action-filter-label": "Filter",
+ "guardrails.builder.action-escalate-label": "Eskalieren",
+ "guardrails.builder.severity-label": "Schweregrad",
+ "guardrails.builder.severity-info-label": "Info",
+ "guardrails.builder.severity-warning-label": "Warnung",
+ "guardrails.builder.severity-error-label": "Fehler",
+ "guardrails.builder.block-reason-label": "Blockierungsgrund",
+ "guardrails.builder.block-reason-placeholder": "Grund für Blockierung eingeben",
+ "guardrails.builder.assign-to-label": "Zuweisen an",
+ "guardrails.builder.recipient-user-label": "Benutzer",
+ "guardrails.builder.recipient-group-label": "Gruppe",
+ "guardrails.builder.recipient-email-label": "E-Mail-Adresse",
+ "guardrails.builder.recipient-group-name-label": "Gruppenname",
+ "guardrails.builder.recipient-fallback-label": "Empfänger",
+ "guardrails.builder.user-search-placeholder": "Nach einem Benutzer suchen…",
+ "guardrails.builder.group-search-placeholder": "Nach einer Gruppe suchen…",
+ "guardrails.builder.email-placeholder": "E-Mail-Adresse eingeben",
+ "guardrails.builder.group-name-placeholder": "Gruppennamen eingeben",
+ "guardrails.builder.action-app-label": "Aktions-App",
+ "guardrails.builder.app-picker-unavailable": "App-Auswahl nicht verfügbar – erfordert Studio Web-Host.",
+ "guardrails.builder.mixed-scopes-also-applied": "Diese Leitplanke gilt auch für:",
+ "guardrails.builder.mixed-scopes-save-as-new-hint": "Verwenden Sie „Als neu speichern“, um eine separate Kopie nur für dieses Tool zu erstellen.",
+ "guardrails.builder.name-required-error": "Der Leitplankenname ist erforderlich",
+ "guardrails.builder.name-duplicate-error": "Eine Leitplanke mit diesem Namen ist bereits vorhanden.",
+ "guardrails.builder.parameter-required-error": "Wert erforderlich",
+ "guardrails.builder.scopes-required-error": "Mindestens ein Umfang ist erforderlich.",
+ "guardrails.builder.tools-required-error": "Mindestens ein Tool ist erforderlich.",
+ "guardrails.builder.block-reason-required-error": "Blockierungsgrund ist erforderlich",
+ "guardrails.builder.filter-fields-required-error": "Eine Feldauswahl ist erforderlich.",
+ "guardrails.builder.recipient-required-error": "Empfänger ist erforderlich",
+ "guardrails.builder.action-app-required-error": "Aktions-App ist erforderlich",
+ "guardrails.definitions.pii_detection.display-name": "PII-Erkennung",
+ "guardrails.definitions.pii_detection.description": "Erkennen Sie personenbezogene Daten mithilfe von Azure Cognitive Services.",
+ "guardrails.definitions.pii_detection.param.entities.label": "Zu erkennende Entitäten",
+ "guardrails.definitions.pii_detection.param.entityThresholds.label": "Erkennungsschwellen",
+ "guardrails.definitions.pii_detection.param.entityThresholds.tooltip": "Wert zwischen 0 und 1. Die Empfindlichkeitsstufe für die PII-Erkennung. Höhere Schwellenwerte erkennen mehr potenzielle PII, können jedoch zu mehr falsch positiven Ergebnissen führen.",
+ "guardrails.definitions.pii_detection.option.entities.Person": "Person",
+ "guardrails.definitions.pii_detection.option.entities.Address": "Adresse",
+ "guardrails.definitions.pii_detection.option.entities.Date": "Datum",
+ "guardrails.definitions.pii_detection.option.entities.PhoneNumber": "Telefonnummer",
+ "guardrails.definitions.pii_detection.option.entities.EugpsCoordinates": "EU-GPS-Koordinaten",
+ "guardrails.definitions.pii_detection.option.entities.Email": "E-Mail",
+ "guardrails.definitions.pii_detection.option.entities.CreditCardNumber": "Kreditkartennummer",
+ "guardrails.definitions.pii_detection.option.entities.InternationalBankingAccountNumber": "Internationale Bankkontonummer (IBAN)",
+ "guardrails.definitions.pii_detection.option.entities.SwiftCode": "Swift-Code",
+ "guardrails.definitions.pii_detection.option.entities.ABARoutingNumber": "ABA-Bankleitzahl",
+ "guardrails.definitions.pii_detection.option.entities.USDriversLicenseNumber": "US Führerscheinnummer",
+ "guardrails.definitions.pii_detection.option.entities.UKDriversLicenseNumber": "UK Führerscheinnummer",
+ "guardrails.definitions.pii_detection.option.entities.USIndividualTaxpayerIdentification": "USA Persönliche Steueridentifikationsnummer (ITIN)",
+ "guardrails.definitions.pii_detection.option.entities.UKUniqueTaxpayerNumber": "UK Eindeutige Steuernummer (UTR)",
+ "guardrails.definitions.pii_detection.option.entities.USBankAccountNumber": "US Bankkontonummer",
+ "guardrails.definitions.pii_detection.option.entities.USSocialSecurityNumber": "USA Sozialversicherungsnummer (SSN)",
+ "guardrails.definitions.pii_detection.option.entities.UsukPassportNumber": "US/UK Passnummer",
+ "guardrails.definitions.pii_detection.option.entities.NOIdentityNumber": "Norwegische Identitätsnummer",
+ "guardrails.definitions.pii_detection.option.entities.FINationalID": "Finnische Personalausweisnummer",
+ "guardrails.definitions.pii_detection.option.entities.SENationalID": "Schwedische Personennummer",
+ "guardrails.definitions.pii_detection.option.entities.DKPersonalIdentificationNumber": "Dänische Personenidentifikationsnummer",
+ "guardrails.definitions.pii_detection.option.entities.NLCitizensServiceNumber": "Niederländische Bürgerservicenummer",
+ "guardrails.definitions.pii_detection.option.entities.URL": "URL",
+ "guardrails.definitions.pii_detection.option.entities.IPAddress": "IP-Adresse",
+ "guardrails.definitions.prompt_injection.display-name": "Prompt-Injection",
+ "guardrails.definitions.prompt_injection.description": "Dieser Validierer wird von Noma Security bereitgestellt und ist darauf ausgelegt, böswillige Angriffsversuche (z. B. Prompt-Injection, Jailbreak) in LLM-Aufrufen zu erkennen.",
+ "guardrails.definitions.prompt_injection.param.threshold.label": "Erkennungsschwelle",
+ "guardrails.definitions.prompt_injection.param.threshold.tooltip": "Wert zwischen 0 und 1. Die Empfindlichkeitsstufe für die Erkennung von Prompt-Injection. Höhere Schwellenwerte erkennen mehr potenzielle Prompt-Injection-Angriffe, können jedoch zu mehr falsch positiven Ergebnissen führen.",
+ "guardrails.definitions.harmful_content.display-name": "Schädlicher Inhalt",
+ "guardrails.definitions.harmful_content.description": "Erkennen Sie schädliche Inhalte (z. B. Hass, Gewalt) mithilfe von Azure AI Content Safety.",
+ "guardrails.definitions.harmful_content.param.harmfulContentEntities.label": "Inhaltskategorien",
+ "guardrails.definitions.harmful_content.param.harmfulContentEntityThresholds.label": "Schwellenwerte für den Schweregrad",
+ "guardrails.definitions.harmful_content.param.harmfulContentEntityThresholds.tooltip": "Integer-Wert zwischen 0 und 6 (Schritt 2). Der Schweregradschwellenwert für die Erkennung schädlicher Inhalte. Höhere Werte erfordern strengere Inhalte vor dem Auslösen.",
+ "guardrails.definitions.harmful_content.option.harmfulContentEntities.Hate": "Hass",
+ "guardrails.definitions.harmful_content.option.harmfulContentEntities.SelfHarm": "Selbstverletzung",
+ "guardrails.definitions.harmful_content.option.harmfulContentEntities.Sexual": "Sexuell",
+ "guardrails.definitions.harmful_content.option.harmfulContentEntities.Violence": "Gewalt",
+ "guardrails.definitions.user_prompt_attacks.display-name": "Angriffe mit Benutzerprompts",
+ "guardrails.definitions.user_prompt_attacks.description": "Erkennen Sie Angriffe über Benutzer-Prompts, die versuchen, Systemanweisungen zu umgehen, mithilfe von Azure AI Content Safety.",
+ "guardrails.definitions.intellectual_property.display-name": "Geistiges Eigentum",
+ "guardrails.definitions.intellectual_property.description": "Erkennen Sie potenzielle Verstöße gegen Urheberrecht in Text und Code mithilfe von Azure AI Content Security.",
+ "guardrails.definitions.intellectual_property.param.ipEntities.label": "Inhaltstypen",
+ "guardrails.definitions.intellectual_property.option.ipEntities.Text": "Text",
+ "guardrails.definitions.intellectual_property.option.ipEntities.Code": "Code",
+ "guardrails.definitions.llm_as_judge.display-name": "LLM-as-a-Judge",
+ "guardrails.definitions.llm_as_judge.description": "Erkennen Sie Verstöße gegen eine von Ihnen definierte Regel mithilfe eines LLM als „Richter“ (LLM-as-a-Judge).",
+ "guardrails.definitions.llm_as_judge.usage-note": "Richtermodellaufrufe verbrauchen Agent Units auf die gleiche Weise wie die eigenen LLM-Aufrufe des Agents. Wenden Sie sie selektiv an, um die Kosten vorhersehbar zu halten, und wählen Sie das Richtermodell unter Berücksichtigung der Kosten – die Raten variieren je nach Modell.",
+ "guardrails.definitions.llm_as_judge.param.guardrailText.label": "Regel-Prompt",
+ "guardrails.definitions.llm_as_judge.param.model.label": "Richtermodell",
+ "guardrails.definitions.llm_as_judge.param.positiveExamples.label": "Positive Beispiele",
+ "guardrails.definitions.llm_as_judge.param.negativeExamples.label": "Negative Beispiele",
+ "guardrails.definitions.llm_as_judge.param.threshold.label": "Striktheit",
+ "guardrails.definitions.llm_as_judge.param.guardrailText.tooltip": "Beschreiben Sie die Regel, das Richtermodell erzwingen wird. Legen Sie genau fest, was erfolgreich sein und was fehlschlagen soll.",
+ "guardrails.definitions.llm_as_judge.param.model.tooltip": "Das Modell, das zum Auswerten der Richtlinie für jede Nutzlast verwendet wird.",
+ "guardrails.definitions.llm_as_judge.param.positiveExamples.tooltip": "Optionale Nutzlasten, bei denen die Richtlinie erfolgreich soll. Wird vom Richtermodell als Kalibrierungsanker verwendet.",
+ "guardrails.definitions.llm_as_judge.param.negativeExamples.tooltip": "Optionale Nutzlasten, bei denen die Richtlinie fehlschlagen soll. Wird vom Richtermodell als Kalibrierungsanker verwendet.",
+ "guardrails.definitions.llm_as_judge.param.threshold.tooltip": "Striktheit auf einer Skala von 0–6. Niedrigere Werte bedeuten eine striktere Bewertung – der Richter kennzeichnet alles, was auf einen Verstoß hindeutet. Höhere Werte bedeuten eine großzügigere Bewertung – nur eindeutige, unmissverständliche Verstöße werden gekennzeichnet.",
+ "guardrails.list.title": "Leitlinien",
+ "guardrails.list.add": "Hinzufügen",
+ "guardrails.list.empty": "Keine Leitplanken konfiguriert",
+ "guardrails.list.reorder-item": "Leitplanke {name} neu anordnen",
+ "guardrails.list.edit-item": "Leitplanke bearbeiten",
+ "guardrails.list.remove-item": "Leitplanke entfernen",
+ "guardrails.list.preview": "Vorschau",
+ "guardrails.list.provider": "Provider",
+ "guardrails.list.action-unknown": "Unbekannt",
+ "guardrails.list.byo-disabled-notice": "Die Konfiguration dieser Leitplanke wurde deaktiviert und kann nicht mehr verwendet werden. Wenden Sie sich an Ihren Administrator, um die Konfiguration wieder zu aktivieren oder diese Leitplanke zu ersetzen, bevor Sie den Agent ausführen.",
+ "guardrails.list.byo-unavailable-notice": "Die Konfiguration dieser Leitplanke ist nicht mehr verfügbar. Ersetzen Sie sie, bevor Sie den Agent ausführen.",
+ "guardrails.list.status-unauthorized": "Nicht autorisiert",
+ "guardrails.palette.list-aria-label": "Verfügbare Leitlinien",
+ "guardrails.palette.create-custom": "Benutzerdefinierte Leitplanke",
+ "guardrails.palette.create-custom-description": "Leitplanke mit benutzerdefinierten Regeln erstellen",
+ "guardrails.palette.empty": "Keine Leitplanken verfügbar",
+ "guardrails.palette.loading": "Wird geladen…",
+ "guardrails.palette.load-error": "Fehler beim Laden der integrierten Validierer",
+ "guardrails.palette.uipath-group": "UiPath-Leitplanken",
+ "guardrails.palette.preview": "Vorschau",
+ "guardrails.palette.status-unauthorized": "Nicht autorisiert"
}
diff --git a/packages/apollo-react/src/canvas/locales/en.json b/packages/apollo-react/src/canvas/locales/en.json
index 5ba3425b88..58f1e42ad9 100644
--- a/packages/apollo-react/src/canvas/locales/en.json
+++ b/packages/apollo-react/src/canvas/locales/en.json
@@ -155,5 +155,154 @@
"canvas.node_property_panel.name_placeholder": "Name",
"canvas.node_property_panel.description_placeholder": "Description",
"canvas.node_property_panel.node_name": "Node name",
- "canvas.node_property_panel.node_description": "Node description"
+ "canvas.node_property_panel.node_description": "Node description",
+ "guardrails.form.more-information": "More information",
+ "guardrails.form.enum-placeholder": "Select...",
+ "guardrails.form.enum-list-placeholder": "Select options...",
+ "guardrails.form.add-item": "Add",
+ "guardrails.form.remove-item": "Remove {label} {position}",
+ "guardrails.builder.edit-title": "Edit {name} guardrail",
+ "guardrails.builder.add-title": "Add {name} guardrail",
+ "guardrails.builder.type-label": "Guardrail type",
+ "guardrails.builder.name-label": "Guardrail name",
+ "guardrails.builder.name-placeholder": "Enter guardrail name",
+ "guardrails.builder.description-label": "Guardrail description",
+ "guardrails.builder.description-placeholder": "Enter guardrail description",
+ "guardrails.builder.evals-label": "Enable guardrail for evaluations",
+ "guardrails.builder.evals-info-aria-label": "More information",
+ "guardrails.builder.evals-tooltip": "When enabled, this guardrail will be applied during evaluation runs.",
+ "guardrails.builder.save-as-new": "Save as new",
+ "guardrails.builder.cancel": "Cancel",
+ "guardrails.builder.save": "Save",
+ "guardrails.builder.byo-disabled-message": "This guardrail's configuration has been disabled and can no longer be used. Contact your administrator to re-enable the configuration or replace this guardrail before running the agent.",
+ "guardrails.builder.unauthorized-message": "You are not entitled to use guardrails. You can access the configuration settings, but modifications cannot be saved.",
+ "guardrails.builder.feature-disabled-message": "This guardrail feature is disabled. You can access the configuration settings, but modifications cannot be saved. It is best to remove this guardrail as long as the feature is disabled.",
+ "guardrails.builder.scopes-label": "Scopes",
+ "guardrails.builder.tools-label": "Tools",
+ "guardrails.builder.scope-agent-label": "Agent",
+ "guardrails.builder.scope-llm-label": "LLM calls",
+ "guardrails.builder.scope-tool-label": "Tools",
+ "guardrails.builder.action-type-label": "Action type",
+ "guardrails.builder.action-log-label": "Log",
+ "guardrails.builder.action-block-label": "Block",
+ "guardrails.builder.action-filter-label": "Filter",
+ "guardrails.builder.action-escalate-label": "Escalate",
+ "guardrails.builder.severity-label": "Severity level",
+ "guardrails.builder.severity-info-label": "Info",
+ "guardrails.builder.severity-warning-label": "Warning",
+ "guardrails.builder.severity-error-label": "Error",
+ "guardrails.builder.block-reason-label": "Blocking reason",
+ "guardrails.builder.block-reason-placeholder": "Enter reason for blocking",
+ "guardrails.builder.assign-to-label": "Assign to",
+ "guardrails.builder.recipient-user-label": "User",
+ "guardrails.builder.recipient-group-label": "Group",
+ "guardrails.builder.recipient-email-label": "Email address",
+ "guardrails.builder.recipient-group-name-label": "Group name",
+ "guardrails.builder.recipient-fallback-label": "Recipient",
+ "guardrails.builder.user-search-placeholder": "Search for a user...",
+ "guardrails.builder.group-search-placeholder": "Search for a group...",
+ "guardrails.builder.email-placeholder": "Enter email address",
+ "guardrails.builder.group-name-placeholder": "Enter group name",
+ "guardrails.builder.action-app-label": "Action App",
+ "guardrails.builder.app-picker-unavailable": "App picker unavailable — requires Studio Web host.",
+ "guardrails.builder.mixed-scopes-also-applied": "This guardrail is also applied to:",
+ "guardrails.builder.mixed-scopes-save-as-new-hint": "Use \"Save as new\" to create a separate copy for this tool only.",
+ "guardrails.builder.name-required-error": "Guardrail name is required",
+ "guardrails.builder.name-duplicate-error": "A guardrail with this name already exists",
+ "guardrails.builder.parameter-required-error": "Value is required",
+ "guardrails.builder.scopes-required-error": "At least one scope is required",
+ "guardrails.builder.tools-required-error": "At least one tool is required",
+ "guardrails.builder.block-reason-required-error": "Block reason is required",
+ "guardrails.builder.filter-fields-required-error": "Fields selection is required",
+ "guardrails.builder.recipient-required-error": "Recipient is required",
+ "guardrails.builder.action-app-required-error": "Action app is required",
+ "guardrails.definitions.pii_detection.display-name": "PII detection",
+ "guardrails.definitions.pii_detection.description": "Detect personally identifiable information using Azure Cognitive Services.",
+ "guardrails.definitions.pii_detection.param.entities.label": "Entities to detect",
+ "guardrails.definitions.pii_detection.param.entityThresholds.label": "Detection thresholds",
+ "guardrails.definitions.pii_detection.param.entityThresholds.tooltip": "Value between 0 and 1. The sensitivity level for PII detection. Higher thresholds detect more potential PII but may result in more false positives.",
+ "guardrails.definitions.pii_detection.option.entities.Person": "Person",
+ "guardrails.definitions.pii_detection.option.entities.Address": "Address",
+ "guardrails.definitions.pii_detection.option.entities.Date": "Date",
+ "guardrails.definitions.pii_detection.option.entities.PhoneNumber": "Phone Number",
+ "guardrails.definitions.pii_detection.option.entities.EugpsCoordinates": "EU GPS Coordinates",
+ "guardrails.definitions.pii_detection.option.entities.Email": "Email",
+ "guardrails.definitions.pii_detection.option.entities.CreditCardNumber": "Credit Card Number",
+ "guardrails.definitions.pii_detection.option.entities.InternationalBankingAccountNumber": "International Banking Account Number (IBAN)",
+ "guardrails.definitions.pii_detection.option.entities.SwiftCode": "SWIFT Code",
+ "guardrails.definitions.pii_detection.option.entities.ABARoutingNumber": "ABA Routing Number",
+ "guardrails.definitions.pii_detection.option.entities.USDriversLicenseNumber": "US Driver's License Number",
+ "guardrails.definitions.pii_detection.option.entities.UKDriversLicenseNumber": "UK Driver's License Number",
+ "guardrails.definitions.pii_detection.option.entities.USIndividualTaxpayerIdentification": "US Individual Taxpayer Identification Number (ITIN)",
+ "guardrails.definitions.pii_detection.option.entities.UKUniqueTaxpayerNumber": "UK Unique Taxpayer Number (UTR)",
+ "guardrails.definitions.pii_detection.option.entities.USBankAccountNumber": "US Bank Account Number",
+ "guardrails.definitions.pii_detection.option.entities.USSocialSecurityNumber": "US Social Security Number (SSN)",
+ "guardrails.definitions.pii_detection.option.entities.UsukPassportNumber": "US/UK Passport Number",
+ "guardrails.definitions.pii_detection.option.entities.NOIdentityNumber": "Norway Identity Number",
+ "guardrails.definitions.pii_detection.option.entities.FINationalID": "Finland National ID",
+ "guardrails.definitions.pii_detection.option.entities.FIPassportNumber": "Finland Passport Number",
+ "guardrails.definitions.pii_detection.option.entities.SENationalID": "Sweden National ID",
+ "guardrails.definitions.pii_detection.option.entities.DKPersonalIdentificationNumber": "Danish Personal Identification Number",
+ "guardrails.definitions.pii_detection.option.entities.NLCitizensServiceNumber": "Netherlands Citizens Service Number",
+ "guardrails.definitions.pii_detection.option.entities.URL": "URL",
+ "guardrails.definitions.pii_detection.option.entities.IPAddress": "IP Address",
+ "guardrails.definitions.prompt_injection.display-name": "Prompt injection",
+ "guardrails.definitions.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.",
+ "guardrails.definitions.prompt_injection.param.threshold.label": "Detection threshold",
+ "guardrails.definitions.prompt_injection.param.threshold.tooltip": "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.",
+ "guardrails.definitions.harmful_content.display-name": "Harmful content",
+ "guardrails.definitions.harmful_content.description": "Detect harmful content (e.g. hate, violence) using Azure AI Content Safety.",
+ "guardrails.definitions.harmful_content.param.harmfulContentEntities.label": "Content categories",
+ "guardrails.definitions.harmful_content.param.harmfulContentEntityThresholds.label": "Severity thresholds",
+ "guardrails.definitions.harmful_content.param.harmfulContentEntityThresholds.tooltip": "Integer value between 0 and 6 (step 2). The severity threshold for harmful content detection. Higher values require more severe content before triggering.",
+ "guardrails.definitions.harmful_content.option.harmfulContentEntities.Hate": "Hate",
+ "guardrails.definitions.harmful_content.option.harmfulContentEntities.SelfHarm": "Self-harm",
+ "guardrails.definitions.harmful_content.option.harmfulContentEntities.Sexual": "Sexual",
+ "guardrails.definitions.harmful_content.option.harmfulContentEntities.Violence": "Violence",
+ "guardrails.definitions.user_prompt_attacks.display-name": "User prompt attacks",
+ "guardrails.definitions.user_prompt_attacks.description": "Detect user prompt attacks that attempt to bypass system instructions using Azure AI Content Safety.",
+ "guardrails.definitions.intellectual_property.display-name": "Intellectual property",
+ "guardrails.definitions.intellectual_property.description": "Detect potential intellectual property violations in text and code using Azure AI Content Safety.",
+ "guardrails.definitions.intellectual_property.param.ipEntities.label": "Content types",
+ "guardrails.definitions.intellectual_property.option.ipEntities.Text": "Text",
+ "guardrails.definitions.intellectual_property.option.ipEntities.Code": "Code",
+ "guardrails.definitions.llm_as_judge.display-name": "LLM as Judge",
+ "guardrails.definitions.llm_as_judge.description": "Detect violations of a rule you define, using an LLM as the judge.",
+ "guardrails.definitions.llm_as_judge.usage-note": "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.",
+ "guardrails.definitions.llm_as_judge.param.guardrailText.label": "Rule prompt",
+ "guardrails.definitions.llm_as_judge.param.model.label": "Judge model",
+ "guardrails.definitions.llm_as_judge.param.positiveExamples.label": "Positive examples",
+ "guardrails.definitions.llm_as_judge.param.negativeExamples.label": "Negative examples",
+ "guardrails.definitions.llm_as_judge.param.threshold.label": "Strictness",
+ "guardrails.definitions.llm_as_judge.param.guardrailText.tooltip": "Describe the rule the judge will enforce. Be specific about what should pass and what should fail.",
+ "guardrails.definitions.llm_as_judge.param.model.tooltip": "The model used to evaluate the policy against each payload.",
+ "guardrails.definitions.llm_as_judge.param.positiveExamples.tooltip": "Optional payloads that should pass the policy. Used by the judge as calibration anchors.",
+ "guardrails.definitions.llm_as_judge.param.negativeExamples.tooltip": "Optional payloads that should fail the policy. Used by the judge as calibration anchors.",
+ "guardrails.definitions.llm_as_judge.param.threshold.tooltip": "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.",
+ "guardrails.list.title": "Guardrails",
+ "guardrails.list.add": "Add",
+ "guardrails.list.empty": "No guardrails configured",
+ "guardrails.list.reorder-item": "Reorder guardrail {name}",
+ "guardrails.list.edit-item": "Edit guardrail",
+ "guardrails.list.edit-row": "Edit {name}",
+ "guardrails.list.remove-item": "Remove guardrail",
+ "guardrails.list.preview": "Preview",
+ "guardrails.list.provider": "Provider",
+ "guardrails.list.action-unknown": "Unknown",
+ "guardrails.list.byo-disabled-notice": "This guardrail's configuration has been disabled and can no longer be used. Contact your administrator to re-enable the configuration or replace this guardrail before running the agent.",
+ "guardrails.list.byo-unavailable-notice": "This guardrail's configuration is no longer available. Replace it before running the agent.",
+ "guardrails.list.status-feature-disabled": "Feature disabled",
+ "guardrails.list.status-unauthorized": "Unauthorized",
+ "guardrails.list.status-disabled": "Disabled",
+ "guardrails.list.status-unavailable": "Unavailable",
+ "guardrails.list.administration-governance": "Governance managed",
+ "guardrails.palette.list-aria-label": "Available guardrails",
+ "guardrails.palette.create-custom": "Custom guardrail",
+ "guardrails.palette.create-custom-description": "Create a guardrail with custom rules",
+ "guardrails.palette.empty": "No guardrails available",
+ "guardrails.palette.loading": "Loading...",
+ "guardrails.palette.load-error": "Failed to load built-in validators",
+ "guardrails.palette.uipath-group": "UiPath guardrails",
+ "guardrails.palette.preview": "Preview",
+ "guardrails.palette.status-unauthorized": "Unauthorized"
}
diff --git a/packages/apollo-react/src/canvas/locales/es-MX.json b/packages/apollo-react/src/canvas/locales/es-MX.json
index 4bf452da23..72ebc2d7dd 100644
--- a/packages/apollo-react/src/canvas/locales/es-MX.json
+++ b/packages/apollo-react/src/canvas/locales/es-MX.json
@@ -150,5 +150,148 @@
"canvas.node_mode_select.simulated_description": "Generar una respuesta de forma dinámica utilizando un LLM",
"canvas.node_mode_select.simulated_label": "Simulado",
"canvas.node_mode_select.static_description": "Devolver siempre un valor que defina usted",
- "canvas.node_mode_select.static_label": "Simulacro estático"
+ "canvas.node_mode_select.static_label": "Simulacro estático",
+ "guardrails.form.more-information": "Más información",
+ "guardrails.form.enum-placeholder": "Seleccionar...",
+ "guardrails.form.enum-list-placeholder": "Seleccione opciones…",
+ "guardrails.form.add-item": "Agregar",
+ "guardrails.form.remove-item": "Eliminar {label} {position}",
+ "guardrails.builder.edit-title": "Editar medida de seguridad {name}",
+ "guardrails.builder.add-title": "Agregar medida de seguridad {name}",
+ "guardrails.builder.type-label": "Tipo de medida de seguridad",
+ "guardrails.builder.name-label": "Nombre de la protección",
+ "guardrails.builder.name-placeholder": "Introduzca el nombre de la medida de seguridad",
+ "guardrails.builder.description-label": "Descripción de la medida de seguridad",
+ "guardrails.builder.description-placeholder": "Introduzca la descripción de la medida de seguridad",
+ "guardrails.builder.evals-label": "Habilitar protección para las evaluaciones",
+ "guardrails.builder.evals-info-aria-label": "Más información",
+ "guardrails.builder.evals-tooltip": "Cuando se habilite, esta medida de seguridad se aplicará durante las ejecuciones de evaluación.",
+ "guardrails.builder.save-as-new": "Guardar como nuevo",
+ "guardrails.builder.cancel": "Cancelar",
+ "guardrails.builder.save": "Guardar",
+ "guardrails.builder.byo-disabled-message": "La configuración de esta protección se ha deshabilitado y ya no se puede utilizar. Comuníquese con el administrador para volver a habilitar la configuración o reemplazar esta protección antes de ejecutar el agente.",
+ "guardrails.builder.unauthorized-message": "No tiene derecho a utilizar medidas de seguridad. Puede acceder a los ajustes de configuración, pero las modificaciones no se pueden guardar.",
+ "guardrails.builder.feature-disabled-message": "Esta característica de protección está deshabilitada. Puede acceder a los ajustes de configuración, pero las modificaciones no se pueden guardar. Es mejor eliminar esta medida de seguridad siempre que la característica esté deshabilitada.",
+ "guardrails.builder.scopes-label": "Scopes",
+ "guardrails.builder.tools-label": "Herramientas",
+ "guardrails.builder.scope-agent-label": "Agente",
+ "guardrails.builder.scope-llm-label": "Llamadas de LLM",
+ "guardrails.builder.scope-tool-label": "Herramientas",
+ "guardrails.builder.action-type-label": "Tipo de acción",
+ "guardrails.builder.action-log-label": "Registro",
+ "guardrails.builder.action-block-label": "Bloque",
+ "guardrails.builder.action-filter-label": "Filtro",
+ "guardrails.builder.action-escalate-label": "Transferir",
+ "guardrails.builder.severity-label": "Nivel de gravedad",
+ "guardrails.builder.severity-info-label": "Info",
+ "guardrails.builder.severity-warning-label": "Advertencia",
+ "guardrails.builder.severity-error-label": "Error",
+ "guardrails.builder.block-reason-label": "Motivo del bloqueo",
+ "guardrails.builder.block-reason-placeholder": "Ingrese el motivo del bloqueo",
+ "guardrails.builder.assign-to-label": "Asignar a",
+ "guardrails.builder.recipient-user-label": "Usuario",
+ "guardrails.builder.recipient-group-label": "Grupo",
+ "guardrails.builder.recipient-email-label": "Dirección de correo electrónico",
+ "guardrails.builder.recipient-group-name-label": "Nombre del grupo",
+ "guardrails.builder.recipient-fallback-label": "Destinatario",
+ "guardrails.builder.user-search-placeholder": "Busque un usuario...",
+ "guardrails.builder.group-search-placeholder": "Busque un grupo…",
+ "guardrails.builder.email-placeholder": "Introduzca la dirección de correo electrónico",
+ "guardrails.builder.group-name-placeholder": "Introduzca el nombre del grupo",
+ "guardrails.builder.action-app-label": "Aplicación de acción",
+ "guardrails.builder.app-picker-unavailable": "Selector de aplicaciones no disponible: se requiere un host de Studio Web.",
+ "guardrails.builder.mixed-scopes-also-applied": "Esta medida de seguridad también se aplica a:",
+ "guardrails.builder.mixed-scopes-save-as-new-hint": "Utilice \"Guardar como nuevo\" para crear una copia independiente solo para esta herramienta.",
+ "guardrails.builder.name-required-error": "El nombre de la protección es necesario.",
+ "guardrails.builder.name-duplicate-error": "Ya existe una protección con este nombre.",
+ "guardrails.builder.parameter-required-error": "Se requiere valor.",
+ "guardrails.builder.scopes-required-error": "Se requiere al menos un alcance",
+ "guardrails.builder.tools-required-error": "Se requiere al menos una herramienta",
+ "guardrails.builder.block-reason-required-error": "La razón del bloqueo es necesaria.",
+ "guardrails.builder.filter-fields-required-error": "Se requiere la selección de campos",
+ "guardrails.builder.recipient-required-error": "El destinatario es necesario.",
+ "guardrails.builder.action-app-required-error": "La aplicación de acción es necesaria.",
+ "guardrails.definitions.pii_detection.display-name": "Detección de PII",
+ "guardrails.definitions.pii_detection.description": "Detecte información de identificación personal mediante Azure Cognitive Services.",
+ "guardrails.definitions.pii_detection.param.entities.label": "Entidades para encontrar",
+ "guardrails.definitions.pii_detection.param.entityThresholds.label": "Umbrales de detección",
+ "guardrails.definitions.pii_detection.param.entityThresholds.tooltip": "Un valor entre 0 y 1. El nivel de sensibilidad para la detección de PII. Los umbrales más altos encuentran más PII potencial, pero pueden dar lugar a más falsos positivos.",
+ "guardrails.definitions.pii_detection.option.entities.Person": "Persona",
+ "guardrails.definitions.pii_detection.option.entities.Address": "Dirección",
+ "guardrails.definitions.pii_detection.option.entities.Date": "Fecha",
+ "guardrails.definitions.pii_detection.option.entities.PhoneNumber": "Número de teléfono",
+ "guardrails.definitions.pii_detection.option.entities.EugpsCoordinates": "Coordenadas GPS para la UE",
+ "guardrails.definitions.pii_detection.option.entities.Email": "Correo electrónico",
+ "guardrails.definitions.pii_detection.option.entities.CreditCardNumber": "Número de tarjeta de crédito",
+ "guardrails.definitions.pii_detection.option.entities.InternationalBankingAccountNumber": "Número de cuenta bancaria internacional (IBAN)",
+ "guardrails.definitions.pii_detection.option.entities.SwiftCode": "Código SWIFT",
+ "guardrails.definitions.pii_detection.option.entities.ABARoutingNumber": "Número de ruta ABA",
+ "guardrails.definitions.pii_detection.option.entities.USDriversLicenseNumber": "Número de licencia de conducir de EE. UU.",
+ "guardrails.definitions.pii_detection.option.entities.UKDriversLicenseNumber": "Número de licencia de conducir del Reino Unido",
+ "guardrails.definitions.pii_detection.option.entities.USIndividualTaxpayerIdentification": "Número de identificación del contribuyente individual (ITIN) de EE. UU.",
+ "guardrails.definitions.pii_detection.option.entities.UKUniqueTaxpayerNumber": "Número de contribuyente único (UTR) del Reino Unido",
+ "guardrails.definitions.pii_detection.option.entities.USBankAccountNumber": "Número de cuenta bancaria de EE. UU.",
+ "guardrails.definitions.pii_detection.option.entities.USSocialSecurityNumber": "Número de la seguridad social (SSN) de EE. UU.",
+ "guardrails.definitions.pii_detection.option.entities.UsukPassportNumber": "Número de pasaporte de EE. UU./Reino Unido",
+ "guardrails.definitions.pii_detection.option.entities.NOIdentityNumber": "Número de identidad de Noruega",
+ "guardrails.definitions.pii_detection.option.entities.FINationalID": "Documento de identidad de Finlandia",
+ "guardrails.definitions.pii_detection.option.entities.SENationalID": "Documento de identidad de Suecia",
+ "guardrails.definitions.pii_detection.option.entities.DKPersonalIdentificationNumber": "Número de identificación personal danés",
+ "guardrails.definitions.pii_detection.option.entities.NLCitizensServiceNumber": "Número del Servicio de Ciudadanos de los Países Bajos",
+ "guardrails.definitions.pii_detection.option.entities.URL": "URL",
+ "guardrails.definitions.pii_detection.option.entities.IPAddress": "Dirección IP",
+ "guardrails.definitions.prompt_injection.display-name": "Inyección de solicitud",
+ "guardrails.definitions.prompt_injection.description": "Noma Security proporciona este validador y está diseñado para encontrar intentos de ataque maliciosos (p. ej. inyección de solicitud, fuga) en llamadas de LLM.",
+ "guardrails.definitions.prompt_injection.param.threshold.label": "Umbral de detección",
+ "guardrails.definitions.prompt_injection.param.threshold.tooltip": "Un valor entre 0 y 1. El nivel de sensibilidad para la detección de inyección de solicitud. Los umbrales más altos encuentran más inyección de solicitud potencial, pero pueden dar lugar a más falsos positivos.",
+ "guardrails.definitions.harmful_content.display-name": "Contenido dañino",
+ "guardrails.definitions.harmful_content.description": "Detectar contenido dañino (p. ej. odio, violencia) con Azure AI Content Safety.",
+ "guardrails.definitions.harmful_content.param.harmfulContentEntities.label": "Categorías de contenido",
+ "guardrails.definitions.harmful_content.param.harmfulContentEntityThresholds.label": "Umbrales de gravedad",
+ "guardrails.definitions.harmful_content.param.harmfulContentEntityThresholds.tooltip": "Valor entero entre 0 y 6 (paso 2). El umbral de gravedad para la detección de contenido dañino. Los valores más altos requieren contenido más grave antes de la activación.",
+ "guardrails.definitions.harmful_content.option.harmfulContentEntities.Hate": "Odio",
+ "guardrails.definitions.harmful_content.option.harmfulContentEntities.SelfHarm": "Autolesión",
+ "guardrails.definitions.harmful_content.option.harmfulContentEntities.Sexual": "Violencia",
+ "guardrails.definitions.harmful_content.option.harmfulContentEntities.Violence": "sexual",
+ "guardrails.definitions.user_prompt_attacks.display-name": "Ataques de solicitud del usuario",
+ "guardrails.definitions.user_prompt_attacks.description": "Detecte ataques de solicitudes de usuarios que intentan eludir las instrucciones del sistema mediante Azure AI Content Safety.",
+ "guardrails.definitions.intellectual_property.display-name": "Propiedad intelectual",
+ "guardrails.definitions.intellectual_property.description": "Detecte posibles infracciones de propiedad intelectual en texto y código mediante Azure AI Content Safety.",
+ "guardrails.definitions.intellectual_property.param.ipEntities.label": "Tipos de contenido",
+ "guardrails.definitions.intellectual_property.option.ipEntities.Text": "Texto",
+ "guardrails.definitions.intellectual_property.option.ipEntities.Code": "Código",
+ "guardrails.definitions.llm_as_judge.display-name": "LLM como juez",
+ "guardrails.definitions.llm_as_judge.description": "Detecte infracciones de una regla definida por usted, utilizando un LLM como juez.",
+ "guardrails.definitions.llm_as_judge.usage-note": "Las llamadas al modelo de juez consumen Agent Units de la misma manera que las propias llamadas de LLM del agente. Aplíquelas de forma selectiva para mantener los costos predecibles y elija el modelo de juez teniendo en cuenta el costo; las tarifas varían según el modelo.",
+ "guardrails.definitions.llm_as_judge.param.guardrailText.label": "Solicitud de regla",
+ "guardrails.definitions.llm_as_judge.param.model.label": "Modelo de juez",
+ "guardrails.definitions.llm_as_judge.param.positiveExamples.label": "Ejemplos positivos",
+ "guardrails.definitions.llm_as_judge.param.negativeExamples.label": "Ejemplos negativos",
+ "guardrails.definitions.llm_as_judge.param.threshold.label": "Rigor",
+ "guardrails.definitions.llm_as_judge.param.guardrailText.tooltip": "Describa la regla que el juez aplicará. Especifique qué debe aprobarse y qué rechazarse.",
+ "guardrails.definitions.llm_as_judge.param.model.tooltip": "El modelo utilizado para evaluar la política en función de cada carga útil.",
+ "guardrails.definitions.llm_as_judge.param.positiveExamples.tooltip": "Cargas útiles opcionales que deberían aprobar la política. El juez las usa como anclajes de calibración.",
+ "guardrails.definitions.llm_as_judge.param.negativeExamples.tooltip": "Cargas útiles opcionales que deberían incumplir la política. El juez las usa como anclajes de calibración.",
+ "guardrails.definitions.llm_as_judge.param.threshold.tooltip": "Rigor en una escala de 0 a 6. Los valores más bajos son más estrictos: el juez marca lo que sea que sugiera una infracción. Los valores más altos son más flexibles: solo se marcan las infracciones claras e inequívocas.",
+ "guardrails.list.title": "Protecciones",
+ "guardrails.list.add": "Agregar",
+ "guardrails.list.empty": "No hay medidas de seguridad configuradas",
+ "guardrails.list.reorder-item": "Reordenar la medida de seguridad {name}",
+ "guardrails.list.edit-item": "Editar protección",
+ "guardrails.list.remove-item": "Eliminar protección",
+ "guardrails.list.preview": "Vista previa",
+ "guardrails.list.provider": "Proveedor",
+ "guardrails.list.action-unknown": "Desconocido",
+ "guardrails.list.byo-disabled-notice": "La configuración de esta protección se ha deshabilitado y ya no se puede utilizar. Comuníquese con el administrador para volver a habilitar la configuración o reemplazar esta protección antes de ejecutar el agente.",
+ "guardrails.list.byo-unavailable-notice": "La configuración de esta protección ya no está disponible. Reemplácela antes de ejecutar el agente.",
+ "guardrails.list.status-unauthorized": "Sin autorizar",
+ "guardrails.palette.list-aria-label": "Medidas de seguridad disponibles",
+ "guardrails.palette.create-custom": "Medida de seguridad personalizada",
+ "guardrails.palette.create-custom-description": "Cree una medida de seguridad con reglas personalizadas",
+ "guardrails.palette.empty": "No hay protecciones disponibles",
+ "guardrails.palette.loading": "Cargando...",
+ "guardrails.palette.load-error": "Se produjo un error al cargar los validadores integrados",
+ "guardrails.palette.uipath-group": "Protecciones de UiPath",
+ "guardrails.palette.preview": "Vista previa",
+ "guardrails.palette.status-unauthorized": "Sin autorizar"
}
diff --git a/packages/apollo-react/src/canvas/locales/es.json b/packages/apollo-react/src/canvas/locales/es.json
index 3c7ac3df84..67a9d11521 100644
--- a/packages/apollo-react/src/canvas/locales/es.json
+++ b/packages/apollo-react/src/canvas/locales/es.json
@@ -150,5 +150,148 @@
"canvas.node_mode_select.simulated_description": "Generar una respuesta de forma dinámica utilizando un LLM",
"canvas.node_mode_select.simulated_label": "Simulado",
"canvas.node_mode_select.static_description": "Devolver siempre un valor que defina",
- "canvas.node_mode_select.static_label": "Simulacro estático"
+ "canvas.node_mode_select.static_label": "Simulacro estático",
+ "guardrails.form.more-information": "Más información",
+ "guardrails.form.enum-placeholder": "Seleccionar...",
+ "guardrails.form.enum-list-placeholder": "Seleccionar opciones...",
+ "guardrails.form.add-item": "Añadir",
+ "guardrails.form.remove-item": "Eliminar {label} {position}",
+ "guardrails.builder.edit-title": "Editar barrera de seguridad {name}",
+ "guardrails.builder.add-title": "Añadir barrera de seguridad {name}",
+ "guardrails.builder.type-label": "Tipo de barrera de seguridad",
+ "guardrails.builder.name-label": "Nombre de la barrera de seguridad",
+ "guardrails.builder.name-placeholder": "Introduzca el nombre de la barrera de seguridad",
+ "guardrails.builder.description-label": "Descripción de la barrera de seguridad",
+ "guardrails.builder.description-placeholder": "Introduzca la descripción de la barrera de seguridad",
+ "guardrails.builder.evals-label": "Habilitar barrera de seguridad para evaluaciones",
+ "guardrails.builder.evals-info-aria-label": "Más información",
+ "guardrails.builder.evals-tooltip": "Cuando está habilitada, esta barrera de seguridad se aplicará durante las ejecuciones de evaluación.",
+ "guardrails.builder.save-as-new": "Guardar como nuevo",
+ "guardrails.builder.cancel": "Cancelar",
+ "guardrails.builder.save": "Guardar",
+ "guardrails.builder.byo-disabled-message": "La configuración de esta barrera de seguridad se ha deshabilitado y ya no se puede utilizar. Póngase en contacto con su administrador para volver a habilitar la configuración o reemplazar esta barrera de seguridad antes de ejecutar el agente.",
+ "guardrails.builder.unauthorized-message": "No tiene derecho a utilizar barreras de seguridad. Puede acceder a los ajustes de configuración, pero las modificaciones no se pueden guardar.",
+ "guardrails.builder.feature-disabled-message": "Esta función de barrera de seguridad está deshabilitada. Puede acceder a los ajustes de configuración, pero las modificaciones no se pueden guardar. Lo mejor es eliminar esta barrera de seguridad mientras la función esté deshabilitada.",
+ "guardrails.builder.scopes-label": "Ámbitos",
+ "guardrails.builder.tools-label": "Herramientas",
+ "guardrails.builder.scope-agent-label": "Agente",
+ "guardrails.builder.scope-llm-label": "Llamadas de LLM",
+ "guardrails.builder.scope-tool-label": "Herramientas",
+ "guardrails.builder.action-type-label": "Tipo de acción",
+ "guardrails.builder.action-log-label": "Acceso",
+ "guardrails.builder.action-block-label": "Bloquear",
+ "guardrails.builder.action-filter-label": "Filtro",
+ "guardrails.builder.action-escalate-label": "Escalar",
+ "guardrails.builder.severity-label": "Nivel de gravedad",
+ "guardrails.builder.severity-info-label": "Info",
+ "guardrails.builder.severity-warning-label": "Advertencia",
+ "guardrails.builder.severity-error-label": "Error",
+ "guardrails.builder.block-reason-label": "Motivo del bloqueo",
+ "guardrails.builder.block-reason-placeholder": "Introduzca el motivo del bloqueo",
+ "guardrails.builder.assign-to-label": "Asignar a",
+ "guardrails.builder.recipient-user-label": "Usuario",
+ "guardrails.builder.recipient-group-label": "Grupo",
+ "guardrails.builder.recipient-email-label": "Dirección de correo electrónico",
+ "guardrails.builder.recipient-group-name-label": "Nombre de grupo",
+ "guardrails.builder.recipient-fallback-label": "Destinatario",
+ "guardrails.builder.user-search-placeholder": "Buscar un usuario...",
+ "guardrails.builder.group-search-placeholder": "Buscar un grupo...",
+ "guardrails.builder.email-placeholder": "Introducir dirección de correo electrónico",
+ "guardrails.builder.group-name-placeholder": "Introducir nombre del grupo",
+ "guardrails.builder.action-app-label": "Aplicación de acción",
+ "guardrails.builder.app-picker-unavailable": "Selector de aplicaciones no disponible: requiere el host de Studio Web.",
+ "guardrails.builder.mixed-scopes-also-applied": "Esta barrera de seguridad también se aplica a:",
+ "guardrails.builder.mixed-scopes-save-as-new-hint": "Utilice \"Guardar como nuevo\" para crear una copia independiente solo para esta herramienta.",
+ "guardrails.builder.name-required-error": "El nombre de la barrera de seguridad es obligatorio",
+ "guardrails.builder.name-duplicate-error": "Ya existe una barrera de seguridad con este nombre",
+ "guardrails.builder.parameter-required-error": "Valor obligatorio",
+ "guardrails.builder.scopes-required-error": "Se requiere al menos un ámbito",
+ "guardrails.builder.tools-required-error": "Se requiere al menos una herramienta",
+ "guardrails.builder.block-reason-required-error": "El motivo del bloqueo es obligatorio",
+ "guardrails.builder.filter-fields-required-error": "La selección de campos es obligatoria",
+ "guardrails.builder.recipient-required-error": "El destinatario es obligatorio",
+ "guardrails.builder.action-app-required-error": "La aplicación de acción es obligatoria",
+ "guardrails.definitions.pii_detection.display-name": "Detección de PII",
+ "guardrails.definitions.pii_detection.description": "Detectar información de identificación personal utilizando Azure Cognitive Services.",
+ "guardrails.definitions.pii_detection.param.entities.label": "Entidades a detectar",
+ "guardrails.definitions.pii_detection.param.entityThresholds.label": "Umbrales de detección",
+ "guardrails.definitions.pii_detection.param.entityThresholds.tooltip": "Valor entre 0 y 1. El nivel de sensibilidad para la Detección de PII. Los umbrales más altos detectan más posibles PII, pero pueden generar más falsos positivos.",
+ "guardrails.definitions.pii_detection.option.entities.Person": "Persona",
+ "guardrails.definitions.pii_detection.option.entities.Address": "Dirección",
+ "guardrails.definitions.pii_detection.option.entities.Date": "Fecha",
+ "guardrails.definitions.pii_detection.option.entities.PhoneNumber": "Número de teléfono",
+ "guardrails.definitions.pii_detection.option.entities.EugpsCoordinates": "Coordenadas GPS de la UE",
+ "guardrails.definitions.pii_detection.option.entities.Email": "Correo electrónico",
+ "guardrails.definitions.pii_detection.option.entities.CreditCardNumber": "Número de tarjeta de crédito",
+ "guardrails.definitions.pii_detection.option.entities.InternationalBankingAccountNumber": "Número de cuenta bancaria internacional (IBAN)",
+ "guardrails.definitions.pii_detection.option.entities.SwiftCode": "Código SWIFT",
+ "guardrails.definitions.pii_detection.option.entities.ABARoutingNumber": "Número de enrutamiento ABA",
+ "guardrails.definitions.pii_detection.option.entities.USDriversLicenseNumber": "Número de permiso de conducir de EE. UU.",
+ "guardrails.definitions.pii_detection.option.entities.UKDriversLicenseNumber": "Número de permiso de conducir del Reino Unido",
+ "guardrails.definitions.pii_detection.option.entities.USIndividualTaxpayerIdentification": "Número de identificación personal del contribuyente (ITIN) de EE. UU.",
+ "guardrails.definitions.pii_detection.option.entities.UKUniqueTaxpayerNumber": "Número de contribuyente único del Reino Unido (UTR)",
+ "guardrails.definitions.pii_detection.option.entities.USBankAccountNumber": "Número de cuenta bancaria de EE. UU.",
+ "guardrails.definitions.pii_detection.option.entities.USSocialSecurityNumber": "Número de la Seguridad Social (SSN) de EE. UU.",
+ "guardrails.definitions.pii_detection.option.entities.UsukPassportNumber": "Número de pasaporte de EE. UU./Reino Unido",
+ "guardrails.definitions.pii_detection.option.entities.NOIdentityNumber": "Número de identidad de Noruega",
+ "guardrails.definitions.pii_detection.option.entities.FINationalID": "Documento de identidad de Finlandia",
+ "guardrails.definitions.pii_detection.option.entities.SENationalID": "Documento de identidad de Suecia",
+ "guardrails.definitions.pii_detection.option.entities.DKPersonalIdentificationNumber": "Número de identificación personal danés",
+ "guardrails.definitions.pii_detection.option.entities.NLCitizensServiceNumber": "Número de identificación personal de los Países Bajos",
+ "guardrails.definitions.pii_detection.option.entities.URL": "URL",
+ "guardrails.definitions.pii_detection.option.entities.IPAddress": "Dirección IP",
+ "guardrails.definitions.prompt_injection.display-name": "Inyección de solicitud",
+ "guardrails.definitions.prompt_injection.description": "Este validador lo proporciona Noma Security y está diseñado para detectar intentos de ataque maliciosos (por ejemplo inyección de solicitud, fuga) en llamadas de LLM.",
+ "guardrails.definitions.prompt_injection.param.threshold.label": "Umbral de detección",
+ "guardrails.definitions.prompt_injection.param.threshold.tooltip": "Valor entre 0 y 1. El nivel de sensibilidad para la Detección de inyección de solicitud. Los umbrales más altos detectan más posibles inyecciones de solicitud, pero pueden generar más falsos positivos.",
+ "guardrails.definitions.harmful_content.display-name": "Contenido dañino",
+ "guardrails.definitions.harmful_content.description": "Detectar contenido dañino (p. ej. odio, violencia) utilizando Azure AI Content Safety.",
+ "guardrails.definitions.harmful_content.param.harmfulContentEntities.label": "Categorías de contenido",
+ "guardrails.definitions.harmful_content.param.harmfulContentEntityThresholds.label": "Umbrales de gravedad",
+ "guardrails.definitions.harmful_content.param.harmfulContentEntityThresholds.tooltip": "Valor integer entre 0 y 6 (paso 2). El umbral de gravedad para la detección de contenido perjudicial. Los valores más altos requieren contenido más grave antes de la activación.",
+ "guardrails.definitions.harmful_content.option.harmfulContentEntities.Hate": "Odio",
+ "guardrails.definitions.harmful_content.option.harmfulContentEntities.SelfHarm": "Autolesión",
+ "guardrails.definitions.harmful_content.option.harmfulContentEntities.Sexual": "Sexual",
+ "guardrails.definitions.harmful_content.option.harmfulContentEntities.Violence": "Violencia",
+ "guardrails.definitions.user_prompt_attacks.display-name": "Ataques de solicitud de usuario",
+ "guardrails.definitions.user_prompt_attacks.description": "Detectar ataques de solicitud de usuario que intentan eludir las instrucciones del sistema utilizando Azure AI Content Safety.",
+ "guardrails.definitions.intellectual_property.display-name": "Propiedad intelectual",
+ "guardrails.definitions.intellectual_property.description": "Detecta posibles violaciones de la propiedad intelectual en texto y código utilizando Azure AI Content Safety.",
+ "guardrails.definitions.intellectual_property.param.ipEntities.label": "Tipos de contenido",
+ "guardrails.definitions.intellectual_property.option.ipEntities.Text": "Texto",
+ "guardrails.definitions.intellectual_property.option.ipEntities.Code": "Código",
+ "guardrails.definitions.llm_as_judge.display-name": "LLM como juez",
+ "guardrails.definitions.llm_as_judge.description": "Detecte infracciones de una regla que defina, utilizando un LLM como juez.",
+ "guardrails.definitions.llm_as_judge.usage-note": "Las llamadas al modelo de juez consumen Agent Units de la misma manera que las propias llamadas de LLM del agente. Aplíquelas de forma selectiva para mantener el coste predecible y elija el modelo de juez teniendo en cuenta el coste: las tarifas varían según el modelo.",
+ "guardrails.definitions.llm_as_judge.param.guardrailText.label": "Solicitud de regla",
+ "guardrails.definitions.llm_as_judge.param.model.label": "Modelo de juez",
+ "guardrails.definitions.llm_as_judge.param.positiveExamples.label": "Ejemplos positivos",
+ "guardrails.definitions.llm_as_judge.param.negativeExamples.label": "Ejemplos negativos",
+ "guardrails.definitions.llm_as_judge.param.threshold.label": "Nivel de rigor",
+ "guardrails.definitions.llm_as_judge.param.guardrailText.tooltip": "Describa la regla que aplicará el juez. Sea específico sobre lo que debe aprobar y lo que debe rechazar.",
+ "guardrails.definitions.llm_as_judge.param.model.tooltip": "El modelo utilizado para evaluar la política frente a cada carga útil.",
+ "guardrails.definitions.llm_as_judge.param.positiveExamples.tooltip": "Cargas útiles opcionales que deben aprobar la política. Utilizadas por el juez como anclajes de calibración.",
+ "guardrails.definitions.llm_as_judge.param.negativeExamples.tooltip": "Cargas útiles opcionales que deben rechazar la política. Utilizadas por el juez como anclajes de calibración.",
+ "guardrails.definitions.llm_as_judge.param.threshold.tooltip": "Nivel de rigor en una escala de 0 a 6. Los valores más bajos son más estrictos: el juez marca cualquier indicio de incumplimiento. Los valores más altos son más tolerantes: solo se marcan las infracciones claras e inequívocas.",
+ "guardrails.list.title": "Barandillas",
+ "guardrails.list.add": "Añadir",
+ "guardrails.list.empty": "No hay barreras de seguridad configuradas",
+ "guardrails.list.reorder-item": "Reordenar barrera de seguridad {name}",
+ "guardrails.list.edit-item": "Editar barrera de seguridad",
+ "guardrails.list.remove-item": "Eliminar barrera de seguridad",
+ "guardrails.list.preview": "Vista previa",
+ "guardrails.list.provider": "Proveedor",
+ "guardrails.list.action-unknown": "Desconocido",
+ "guardrails.list.byo-disabled-notice": "La configuración de esta barrera de seguridad se ha deshabilitado y ya no se puede utilizar. Póngase en contacto con su administrador para volver a habilitar la configuración o reemplazar esta barrera de seguridad antes de ejecutar el agente.",
+ "guardrails.list.byo-unavailable-notice": "La configuración de esta barrera de seguridad ya no está disponible. Reemplázalo antes de ejecutar el agente.",
+ "guardrails.list.status-unauthorized": "Sin autorización",
+ "guardrails.palette.list-aria-label": "Medidas de seguridad disponibles",
+ "guardrails.palette.create-custom": "Barrera de seguridad personalizada",
+ "guardrails.palette.create-custom-description": "Crear una barrera de seguridad con reglas personalizadas",
+ "guardrails.palette.empty": "No hay barreras de seguridad disponibles",
+ "guardrails.palette.loading": "Cargando...",
+ "guardrails.palette.load-error": "Error al cargar los validadores integrados",
+ "guardrails.palette.uipath-group": "Barreras de seguridad de UiPath",
+ "guardrails.palette.preview": "Vista previa",
+ "guardrails.palette.status-unauthorized": "Sin autorización"
}
diff --git a/packages/apollo-react/src/canvas/locales/fr.json b/packages/apollo-react/src/canvas/locales/fr.json
index 9d7bf440b0..529f855e12 100644
--- a/packages/apollo-react/src/canvas/locales/fr.json
+++ b/packages/apollo-react/src/canvas/locales/fr.json
@@ -150,5 +150,148 @@
"canvas.node_mode_select.simulated_description": "Génère une réponse de manière dynamique à l’aide d’un LLM.",
"canvas.node_mode_select.simulated_label": "Simulé",
"canvas.node_mode_select.static_description": "Retourne toujours une valeur que vous avez définie.",
- "canvas.node_mode_select.static_label": "Simulation statique"
+ "canvas.node_mode_select.static_label": "Simulation statique",
+ "guardrails.form.more-information": "Plus d’informations",
+ "guardrails.form.enum-placeholder": "Sélectionner...",
+ "guardrails.form.enum-list-placeholder": "Sélectionnez des options…",
+ "guardrails.form.add-item": "Ajouter",
+ "guardrails.form.remove-item": "Supprimer {label} {position}",
+ "guardrails.builder.edit-title": "Modifier le garde-fou {name}",
+ "guardrails.builder.add-title": "Ajouter le garde-fou {name}",
+ "guardrails.builder.type-label": "Type de garde-fou",
+ "guardrails.builder.name-label": "Nom du garde-fou",
+ "guardrails.builder.name-placeholder": "Saisissez le nom du garde-fou",
+ "guardrails.builder.description-label": "Description du garde-fou",
+ "guardrails.builder.description-placeholder": "Saisissez la description du garde-fou",
+ "guardrails.builder.evals-label": "Activer le garde-fou pour les évaluations",
+ "guardrails.builder.evals-info-aria-label": "Plus d’informations",
+ "guardrails.builder.evals-tooltip": "Une fois activé, ce garde-fou sera appliqué lors des exécutions d’évaluation.",
+ "guardrails.builder.save-as-new": "Enregistrer en tant que nouveau",
+ "guardrails.builder.cancel": "Annuler",
+ "guardrails.builder.save": "Enregistrer",
+ "guardrails.builder.byo-disabled-message": "La configuration de ce garde-fou a été désactivée et ne peut plus être utilisée. Avant d’exécuter l’agent, contactez votre administrateur pour réactiver la configuration ou remplacer ce garde-fou.",
+ "guardrails.builder.unauthorized-message": "Vous n’êtes pas autorisé à utiliser des garde-fous. Vous pouvez accéder aux paramètres de configuration, mais les modifications ne seront pas enregistrées.",
+ "guardrails.builder.feature-disabled-message": "Cette fonctionnalité de garde-fou est désactivée. Vous pouvez accéder aux paramètres de configuration, mais les modifications ne seront pas enregistrées. Nous vous conseillons de supprimer ce garde-fou tant que la fonctionnalité est désactivée.",
+ "guardrails.builder.scopes-label": "Étendues",
+ "guardrails.builder.tools-label": "Outils",
+ "guardrails.builder.scope-agent-label": "Agent",
+ "guardrails.builder.scope-llm-label": "Appels LLM",
+ "guardrails.builder.scope-tool-label": "Outils",
+ "guardrails.builder.action-type-label": "Type d'action",
+ "guardrails.builder.action-log-label": "Journal",
+ "guardrails.builder.action-block-label": "Bloc",
+ "guardrails.builder.action-filter-label": "Filtre",
+ "guardrails.builder.action-escalate-label": "Escalader",
+ "guardrails.builder.severity-label": "Niveau de gravité",
+ "guardrails.builder.severity-info-label": "Info",
+ "guardrails.builder.severity-warning-label": "Warning",
+ "guardrails.builder.severity-error-label": "Erreur",
+ "guardrails.builder.block-reason-label": "Motif du blocage",
+ "guardrails.builder.block-reason-placeholder": "Saisissez le motif du blocage",
+ "guardrails.builder.assign-to-label": "Affecter à",
+ "guardrails.builder.recipient-user-label": "Utilisateur",
+ "guardrails.builder.recipient-group-label": "Groupe",
+ "guardrails.builder.recipient-email-label": "Adresse e-mail",
+ "guardrails.builder.recipient-group-name-label": "Nom de groupe",
+ "guardrails.builder.recipient-fallback-label": "Destinataires",
+ "guardrails.builder.user-search-placeholder": "Rechercher un utilisateur…",
+ "guardrails.builder.group-search-placeholder": "Rechercher un groupe…",
+ "guardrails.builder.email-placeholder": "Saisissez votre adresse e-mail",
+ "guardrails.builder.group-name-placeholder": "Saisissez le nom du groupe",
+ "guardrails.builder.action-app-label": "Application d’action",
+ "guardrails.builder.app-picker-unavailable": "Sélecteur d’applications non disponible, nécessite un hôte Studio Web.",
+ "guardrails.builder.mixed-scopes-also-applied": "Ce garde-fou est également appliqué aux éléments suivants :",
+ "guardrails.builder.mixed-scopes-save-as-new-hint": "Utilisez « Enregistrer en tant que nouveau » pour créer une copie distincte propre à cet outil.",
+ "guardrails.builder.name-required-error": "Le nom du garde-fou est requis",
+ "guardrails.builder.name-duplicate-error": "Un garde-fou portant ce nom existe déjà",
+ "guardrails.builder.parameter-required-error": "Valeur requise",
+ "guardrails.builder.scopes-required-error": "Au moins une portée est requise",
+ "guardrails.builder.tools-required-error": "Au moins un outil est requis",
+ "guardrails.builder.block-reason-required-error": "La raison du blocage est requise",
+ "guardrails.builder.filter-fields-required-error": "La sélection des champs est requise",
+ "guardrails.builder.recipient-required-error": "Le destinataire est requis",
+ "guardrails.builder.action-app-required-error": "Une application Action est requise",
+ "guardrails.definitions.pii_detection.display-name": "Détection des PII",
+ "guardrails.definitions.pii_detection.description": "Détecte les informations personnelles identifiantes à l’aide d’Azure Cognitive Services.",
+ "guardrails.definitions.pii_detection.param.entities.label": "Entités à détecter",
+ "guardrails.definitions.pii_detection.param.entityThresholds.label": "Seuils de détection",
+ "guardrails.definitions.pii_detection.param.entityThresholds.tooltip": "Une valeur comprise entre 0 et 1. Le niveau de sensibilité pour la détection des PII. Des seuils plus élevés détectent plus de PII potentielles, mais peuvent entraîner davantage de faux positifs.",
+ "guardrails.definitions.pii_detection.option.entities.Person": "Personne",
+ "guardrails.definitions.pii_detection.option.entities.Address": "Adresse",
+ "guardrails.definitions.pii_detection.option.entities.Date": "Date",
+ "guardrails.definitions.pii_detection.option.entities.PhoneNumber": "Numéro de téléphone",
+ "guardrails.definitions.pii_detection.option.entities.EugpsCoordinates": "Coordonnées GPS dans l’UE",
+ "guardrails.definitions.pii_detection.option.entities.Email": "E-mail",
+ "guardrails.definitions.pii_detection.option.entities.CreditCardNumber": "Numéro de carte bancaire",
+ "guardrails.definitions.pii_detection.option.entities.InternationalBankingAccountNumber": "Numéro IBAN",
+ "guardrails.definitions.pii_detection.option.entities.SwiftCode": "Code SWIFT",
+ "guardrails.definitions.pii_detection.option.entities.ABARoutingNumber": "Numéro de routage ABA",
+ "guardrails.definitions.pii_detection.option.entities.USDriversLicenseNumber": "Numéro de permis de conduire (US)",
+ "guardrails.definitions.pii_detection.option.entities.UKDriversLicenseNumber": "Numéro de permis de conduire (GB)",
+ "guardrails.definitions.pii_detection.option.entities.USIndividualTaxpayerIdentification": "Numéro d’identification fiscale individuelle aux États-Unis (ITIN)",
+ "guardrails.definitions.pii_detection.option.entities.UKUniqueTaxpayerNumber": "Numéro d’identification fiscale (GB, numéro UTR)",
+ "guardrails.definitions.pii_detection.option.entities.USBankAccountNumber": "Numéro de compte bancaire (US)",
+ "guardrails.definitions.pii_detection.option.entities.USSocialSecurityNumber": "Numéro de sécurité sociale aux États-Unis (SSN)",
+ "guardrails.definitions.pii_detection.option.entities.UsukPassportNumber": "Numéro de passeport (US/GB)",
+ "guardrails.definitions.pii_detection.option.entities.NOIdentityNumber": "Numéro d’identité norvégien",
+ "guardrails.definitions.pii_detection.option.entities.FINationalID": "Carte d’identité nationale finlandaise",
+ "guardrails.definitions.pii_detection.option.entities.SENationalID": "Carte d’identité suédoise",
+ "guardrails.definitions.pii_detection.option.entities.DKPersonalIdentificationNumber": "Numéro d’identification personnel danois",
+ "guardrails.definitions.pii_detection.option.entities.NLCitizensServiceNumber": "Numéro d’identification du citoyen néerlandais",
+ "guardrails.definitions.pii_detection.option.entities.URL": "URL",
+ "guardrails.definitions.pii_detection.option.entities.IPAddress": "Adresse IP",
+ "guardrails.definitions.prompt_injection.display-name": "Injection d’invite",
+ "guardrails.definitions.prompt_injection.description": "Ce validateur est fourni par Noma Security et est conçu pour détecter les tentatives d’attaque malveillantes (injection d’invite, détection des risques de jailbreak) dans les appels LLM.",
+ "guardrails.definitions.prompt_injection.param.threshold.label": "Seuil de détection",
+ "guardrails.definitions.prompt_injection.param.threshold.tooltip": "Une valeur comprise entre 0 et 1. Le niveau de sensibilité pour la détection d’injections d’invite. Des seuils plus élevés détectent plus d’injections d’invite potentielles, mais peuvent entraîner davantage de faux positifs.",
+ "guardrails.definitions.harmful_content.display-name": "Contenus malveillants",
+ "guardrails.definitions.harmful_content.description": "Détecter le contenu malveillant (p. ex. : contenu haineux ou violent) à l’aide d’Azure AI Content Safety.",
+ "guardrails.definitions.harmful_content.param.harmfulContentEntities.label": "Catégories de contenu",
+ "guardrails.definitions.harmful_content.param.harmfulContentEntityThresholds.label": "Seuils de gravité",
+ "guardrails.definitions.harmful_content.param.harmfulContentEntityThresholds.tooltip": "Valeur entière comprise entre 0 et 6 (étape 2). Le seuil de gravité pour la détection de contenu dangereux. Des valeurs plus élevées nécessitent un contenu plus grave pour le déclenchement.",
+ "guardrails.definitions.harmful_content.option.harmfulContentEntities.Hate": "Contenu haineux",
+ "guardrails.definitions.harmful_content.option.harmfulContentEntities.SelfHarm": "Automutilation",
+ "guardrails.definitions.harmful_content.option.harmfulContentEntities.Sexual": "Caractère sexuel",
+ "guardrails.definitions.harmful_content.option.harmfulContentEntities.Violence": "Violence",
+ "guardrails.definitions.user_prompt_attacks.display-name": "Attaques dans les invites d’utilisateurs",
+ "guardrails.definitions.user_prompt_attacks.description": "Détecte les attaques, à l’aide d’Azure AI Content Safety, dans les invites d’utilisateurs qui tentent de contourner les instructions du système.",
+ "guardrails.definitions.intellectual_property.display-name": "Propriété intellectuelle",
+ "guardrails.definitions.intellectual_property.description": "Détecte les violations potentielles de la propriété intellectuelle dans du texte et du code à l’aide d’Azure AI Content Safety.",
+ "guardrails.definitions.intellectual_property.param.ipEntities.label": "Types de contenu",
+ "guardrails.definitions.intellectual_property.option.ipEntities.Text": "Texte",
+ "guardrails.definitions.intellectual_property.option.ipEntities.Code": "Code",
+ "guardrails.definitions.llm_as_judge.display-name": "LLM as judge",
+ "guardrails.definitions.llm_as_judge.description": "Détectez les violations d’une règle que vous définissez. Un LLM s’occupera de les traiter.",
+ "guardrails.definitions.llm_as_judge.usage-note": "Les appels du modèle juge consomment autant d’Agent Units que les appels LLM de l’agent. Attribuez les Agents Units de manière sélective pour garantir des coûts prévisibles et choisissez le modèle de juge en tenant compte des coûts. Les taux varient selon le modèle.",
+ "guardrails.definitions.llm_as_judge.param.guardrailText.label": "Invite de règle",
+ "guardrails.definitions.llm_as_judge.param.model.label": "Modèle de juge",
+ "guardrails.definitions.llm_as_judge.param.positiveExamples.label": "Exemples positifs",
+ "guardrails.definitions.llm_as_judge.param.negativeExamples.label": "Exemples négatifs",
+ "guardrails.definitions.llm_as_judge.param.threshold.label": "Rigueur",
+ "guardrails.definitions.llm_as_judge.param.guardrailText.tooltip": "Décrivez la règle que le juge appliquera. Soyez précis quant aux conditions acceptées et celles refusées.",
+ "guardrails.definitions.llm_as_judge.param.model.tooltip": "Le modèle utilisé pour évaluer la politique par rapport à chaque charge utile.",
+ "guardrails.definitions.llm_as_judge.param.positiveExamples.tooltip": "Charges utiles facultatives qui sont supposées respecter la politique. Utilisé par le juge comme base pour s’ajuster.",
+ "guardrails.definitions.llm_as_judge.param.negativeExamples.tooltip": "Charges utiles facultatives qui sont supposées constituer une violation de la politique. Utilisé par le juge comme base pour s’ajuster.",
+ "guardrails.definitions.llm_as_judge.param.threshold.tooltip": "Rigueur sur une échelle 0 à 6. Des valeurs plus faibles sont plus strictes. Le juge signale tout ce qui suggère une violation. Des valeurs plus élevées sont plus souples. Seules les violations claires et sans ambiguïté sont signalées.",
+ "guardrails.list.title": "Garde-fous",
+ "guardrails.list.add": "Ajouter",
+ "guardrails.list.empty": "Aucun garde-fou n’est configuré",
+ "guardrails.list.reorder-item": "Réorganiser le garde-fou {name}",
+ "guardrails.list.edit-item": "Modifier le garde-fou",
+ "guardrails.list.remove-item": "Supprimer le garde-fou",
+ "guardrails.list.preview": "Aperçu",
+ "guardrails.list.provider": "Fournisseur",
+ "guardrails.list.action-unknown": "Inconnu",
+ "guardrails.list.byo-disabled-notice": "La configuration de ce garde-fou a été désactivée et ne peut plus être utilisée. Avant d’exécuter l’agent, contactez votre administrateur pour réactiver la configuration ou remplacer ce garde-fou.",
+ "guardrails.list.byo-unavailable-notice": "La configuration de ce garde-fou n’est plus disponible. Remplacez-la avant d’exécuter l’agent.",
+ "guardrails.list.status-unauthorized": "Non autorisé",
+ "guardrails.palette.list-aria-label": "Garde-fous disponibles",
+ "guardrails.palette.create-custom": "Garde-fou personnalisé",
+ "guardrails.palette.create-custom-description": "Crée un garde-fou avec des règles personnalisées",
+ "guardrails.palette.empty": "Aucun garde-fou disponible",
+ "guardrails.palette.loading": "Chargement en cours...",
+ "guardrails.palette.load-error": "Échec du chargement des validateurs intégrés",
+ "guardrails.palette.uipath-group": "Garde-fous UiPath",
+ "guardrails.palette.preview": "Aperçu",
+ "guardrails.palette.status-unauthorized": "Non autorisé"
}
diff --git a/packages/apollo-react/src/canvas/locales/ja.json b/packages/apollo-react/src/canvas/locales/ja.json
index 811ea7e0de..293608e8fc 100644
--- a/packages/apollo-react/src/canvas/locales/ja.json
+++ b/packages/apollo-react/src/canvas/locales/ja.json
@@ -150,5 +150,148 @@
"canvas.node_mode_select.simulated_description": "LLM を使用して動的に応答を生成します。",
"canvas.node_mode_select.simulated_label": "シミュレーション",
"canvas.node_mode_select.static_description": "常に定義した値を返します。",
- "canvas.node_mode_select.static_label": "静的モック"
+ "canvas.node_mode_select.static_label": "静的モック",
+ "guardrails.form.more-information": "詳細情報",
+ "guardrails.form.enum-placeholder": "選択...",
+ "guardrails.form.enum-list-placeholder": "オプションを選択...",
+ "guardrails.form.add-item": "追加",
+ "guardrails.form.remove-item": "{label} {position} を削除",
+ "guardrails.builder.edit-title": "{name} のガードレールを編集",
+ "guardrails.builder.add-title": "{name} のガードレールを追加",
+ "guardrails.builder.type-label": "ガードレールの種類",
+ "guardrails.builder.name-label": "ガードレール名",
+ "guardrails.builder.name-placeholder": "ガードレール名を入力",
+ "guardrails.builder.description-label": "ガードレールの説明",
+ "guardrails.builder.description-placeholder": "ガードレールの説明を入力",
+ "guardrails.builder.evals-label": "評価用にガードレールを有効化",
+ "guardrails.builder.evals-info-aria-label": "詳細情報",
+ "guardrails.builder.evals-tooltip": "有効化すると、このガードレールは評価の実行中に適用されます。",
+ "guardrails.builder.save-as-new": "新規として保存",
+ "guardrails.builder.cancel": "キャンセル",
+ "guardrails.builder.save": "保存",
+ "guardrails.builder.byo-disabled-message": "このガードレールの設定は無効化され、使用できなくなりました。エージェントを実行する前に、管理者に問い合わせて設定を再度有効化するか、このガードレールを置換してください。",
+ "guardrails.builder.unauthorized-message": "ガードレールを使用する権限がありません。設定にアクセスすることはできますが、変更を保存することはできません。",
+ "guardrails.builder.feature-disabled-message": "このガードレール機能は無効化されています。設定にアクセスすることはできますが、変更を保存することはできません。この機能が無効化されている間は、このガードレールを削除することをお勧めします。",
+ "guardrails.builder.scopes-label": "スコープ",
+ "guardrails.builder.tools-label": "ツール",
+ "guardrails.builder.scope-agent-label": "エージェント",
+ "guardrails.builder.scope-llm-label": "LLM の呼び出し",
+ "guardrails.builder.scope-tool-label": "ツール",
+ "guardrails.builder.action-type-label": "アクションの種類",
+ "guardrails.builder.action-log-label": "ログ",
+ "guardrails.builder.action-block-label": "ブロック",
+ "guardrails.builder.action-filter-label": "フィルター",
+ "guardrails.builder.action-escalate-label": "エスカレーション",
+ "guardrails.builder.severity-label": "重大度レベル",
+ "guardrails.builder.severity-info-label": "情報",
+ "guardrails.builder.severity-warning-label": "警告",
+ "guardrails.builder.severity-error-label": "エラー",
+ "guardrails.builder.block-reason-label": "ブロックする理由",
+ "guardrails.builder.block-reason-placeholder": "ブロックする理由を入力",
+ "guardrails.builder.assign-to-label": "割り当て先",
+ "guardrails.builder.recipient-user-label": "ユーザー",
+ "guardrails.builder.recipient-group-label": "グループ",
+ "guardrails.builder.recipient-email-label": "メール アドレス",
+ "guardrails.builder.recipient-group-name-label": "グループ名",
+ "guardrails.builder.recipient-fallback-label": "受信者",
+ "guardrails.builder.user-search-placeholder": "ユーザーを検索...",
+ "guardrails.builder.group-search-placeholder": "グループを検索...",
+ "guardrails.builder.email-placeholder": "メール アドレスを入力",
+ "guardrails.builder.group-name-placeholder": "グループ名を入力",
+ "guardrails.builder.action-app-label": "アクション アプリ",
+ "guardrails.builder.app-picker-unavailable": "アプリ ピッカーは利用できません。— Studio Web のホストが必要です。",
+ "guardrails.builder.mixed-scopes-also-applied": "このガードレールは、次にも適用されます:",
+ "guardrails.builder.mixed-scopes-save-as-new-hint": "このツールのみのために別のコピーを作成するには、「新規として保存」を使用します。",
+ "guardrails.builder.name-required-error": "ガードレール名は必須です。",
+ "guardrails.builder.name-duplicate-error": "この名前のガードレールはすでに存在します。",
+ "guardrails.builder.parameter-required-error": "値は必須です。",
+ "guardrails.builder.scopes-required-error": "少なくとも 1 つのスコープが必要です。",
+ "guardrails.builder.tools-required-error": "少なくとも 1 つのツールが必要です。",
+ "guardrails.builder.block-reason-required-error": "ブロックする理由は必須です。",
+ "guardrails.builder.filter-fields-required-error": "フィールドの選択は必須です。",
+ "guardrails.builder.recipient-required-error": "受信者は必須です。",
+ "guardrails.builder.action-app-required-error": "アクション アプリは必須です。",
+ "guardrails.definitions.pii_detection.display-name": "個人を識別できる情報 (PII) の検出",
+ "guardrails.definitions.pii_detection.description": "Azure Cognitive Services を使用して、個人を識別できる情報を検出します。",
+ "guardrails.definitions.pii_detection.param.entities.label": "検出するエンティティ",
+ "guardrails.definitions.pii_detection.param.entityThresholds.label": "検出しきい値",
+ "guardrails.definitions.pii_detection.param.entityThresholds.tooltip": "0 から 1 の間の値です。個人を識別できる情報 (PII) の検出の感度レベルです。しきい値を高くすると、潜在的な PII をより多く検出できますが、誤検知が増える可能性があります。",
+ "guardrails.definitions.pii_detection.option.entities.Person": "人物",
+ "guardrails.definitions.pii_detection.option.entities.Address": "住所",
+ "guardrails.definitions.pii_detection.option.entities.Date": "日付",
+ "guardrails.definitions.pii_detection.option.entities.PhoneNumber": "電話番号",
+ "guardrails.definitions.pii_detection.option.entities.EugpsCoordinates": "EU の GPS 座標",
+ "guardrails.definitions.pii_detection.option.entities.Email": "メール アドレス",
+ "guardrails.definitions.pii_detection.option.entities.CreditCardNumber": "クレジット カード番号",
+ "guardrails.definitions.pii_detection.option.entities.InternationalBankingAccountNumber": "国際銀行勘定番号 (IBAN)",
+ "guardrails.definitions.pii_detection.option.entities.SwiftCode": "SWIFT コード",
+ "guardrails.definitions.pii_detection.option.entities.ABARoutingNumber": "ABA 銀行支店コード",
+ "guardrails.definitions.pii_detection.option.entities.USDriversLicenseNumber": "米国の運転免許証番号",
+ "guardrails.definitions.pii_detection.option.entities.UKDriversLicenseNumber": "英国の運転免許証番号",
+ "guardrails.definitions.pii_detection.option.entities.USIndividualTaxpayerIdentification": "米国の個人納税者番号 (ITIN)",
+ "guardrails.definitions.pii_detection.option.entities.UKUniqueTaxpayerNumber": "英国の納税者参照番号 (UTR)",
+ "guardrails.definitions.pii_detection.option.entities.USBankAccountNumber": "米国の金融機関の口座番号",
+ "guardrails.definitions.pii_detection.option.entities.USSocialSecurityNumber": "米国の社会保障番号 (SSN)",
+ "guardrails.definitions.pii_detection.option.entities.UsukPassportNumber": "米国/英国のパスポート番号",
+ "guardrails.definitions.pii_detection.option.entities.NOIdentityNumber": "ノルウェーの ID ナンバー",
+ "guardrails.definitions.pii_detection.option.entities.FINationalID": "フィンランドの国民 ID",
+ "guardrails.definitions.pii_detection.option.entities.SENationalID": "スウェーデンの国民 ID",
+ "guardrails.definitions.pii_detection.option.entities.DKPersonalIdentificationNumber": "デンマークの個人識別番号",
+ "guardrails.definitions.pii_detection.option.entities.NLCitizensServiceNumber": "オランダの市民サービス番号",
+ "guardrails.definitions.pii_detection.option.entities.URL": "URL",
+ "guardrails.definitions.pii_detection.option.entities.IPAddress": "IP アドレス",
+ "guardrails.definitions.prompt_injection.display-name": "プロンプト インジェクション",
+ "guardrails.definitions.prompt_injection.description": "Noma Security が提供しているこの検証ツールは、LLM 呼び出しに含まれる悪意のある攻撃の試み (例:プロンプト インジェクション、ジェイル ブレイク) を検出するように設計されています。",
+ "guardrails.definitions.prompt_injection.param.threshold.label": "検出しきい値",
+ "guardrails.definitions.prompt_injection.param.threshold.tooltip": "0 から 1 の間の値です。プロンプト インジェクションの検出の感度レベルです。しきい値を高くすると、潜在的なプロンプト インジェクションをより多く検出できますが、誤検知が増える可能性があります。",
+ "guardrails.definitions.harmful_content.display-name": "有害コンテンツ",
+ "guardrails.definitions.harmful_content.description": "Azure AI Content Safety を使用して、有害なコンテンツ (例: 憎しみ、暴力) を検出します。",
+ "guardrails.definitions.harmful_content.param.harmfulContentEntities.label": "コンテンツのカテゴリ",
+ "guardrails.definitions.harmful_content.param.harmfulContentEntityThresholds.label": "重大度のしきい値",
+ "guardrails.definitions.harmful_content.param.harmfulContentEntityThresholds.tooltip": "0 から 6 までの整数値です (手順 2)。有害なコンテンツの検出に使用される重大度のしきい値です。値が大きいほど、トリガーに必要なコンテンツの重大度が高くなります。",
+ "guardrails.definitions.harmful_content.option.harmfulContentEntities.Hate": "憎しみ",
+ "guardrails.definitions.harmful_content.option.harmfulContentEntities.SelfHarm": "自傷",
+ "guardrails.definitions.harmful_content.option.harmfulContentEntities.Sexual": "性的",
+ "guardrails.definitions.harmful_content.option.harmfulContentEntities.Violence": "暴力",
+ "guardrails.definitions.user_prompt_attacks.display-name": "ユーザー プロンプト攻撃",
+ "guardrails.definitions.user_prompt_attacks.description": "Azure AI Content Safety を使用して、システム命令をバイパスしようとするユーザー プロンプト攻撃を検出します。",
+ "guardrails.definitions.intellectual_property.display-name": "知的財産",
+ "guardrails.definitions.intellectual_property.description": "Azure AI Content Safety を使用して、テキストやコードに含まれる潜在的な知的財産権の侵害を検出します。",
+ "guardrails.definitions.intellectual_property.param.ipEntities.label": "コンテンツの種類",
+ "guardrails.definitions.intellectual_property.option.ipEntities.Text": "テキスト",
+ "guardrails.definitions.intellectual_property.option.ipEntities.Code": "コード",
+ "guardrails.definitions.llm_as_judge.display-name": "LLM を評価者として使用",
+ "guardrails.definitions.llm_as_judge.description": "LLM を評価者として使用して、定義したルールの違反を検出します。",
+ "guardrails.definitions.llm_as_judge.usage-note": "評価者モデルの呼び出しでは、エージェント自身の LLM 呼び出しと同じようにエージェント ユニットが消費されます。コストを予測できるように選択的に適用し、コストを考慮して評価者モデルを選択してください。レートはモデルによって異なります。",
+ "guardrails.definitions.llm_as_judge.param.guardrailText.label": "ルール プロンプト",
+ "guardrails.definitions.llm_as_judge.param.model.label": "評価者モデル",
+ "guardrails.definitions.llm_as_judge.param.positiveExamples.label": "合格例",
+ "guardrails.definitions.llm_as_judge.param.negativeExamples.label": "不合格例",
+ "guardrails.definitions.llm_as_judge.param.threshold.label": "厳密性",
+ "guardrails.definitions.llm_as_judge.param.guardrailText.tooltip": "評価者が適用するルールの説明を入力します。どのような場合に合格とし、どのような場合に不合格とすべきかを具体的に説明します。",
+ "guardrails.definitions.llm_as_judge.param.model.tooltip": "ポリシーを各ペイロードに照らし合わせて評価するのに使用されるモデルです。",
+ "guardrails.definitions.llm_as_judge.param.positiveExamples.tooltip": "任意の、ポリシーに合格するペイロードです。評価者により、キャリブレーション アンカーとして使用されます。",
+ "guardrails.definitions.llm_as_judge.param.negativeExamples.tooltip": "任意の、ポリシーに合格しないペイロードです。評価者により、キャリブレーション アンカーとして使用されます。",
+ "guardrails.definitions.llm_as_judge.param.threshold.tooltip": "0 から 6 のスケールで表される厳密性です。値が低いほど厳格になり、評価者は違反を示唆するものすべてにフラグを立てます。値が大きいほど寛容になり、明確で曖昧さのない違反にのみフラグを立てます。",
+ "guardrails.list.title": "ガードレール",
+ "guardrails.list.add": "追加",
+ "guardrails.list.empty": "ガードレールは設定されていません。",
+ "guardrails.list.reorder-item": "ガードレール {name} を並べ替え",
+ "guardrails.list.edit-item": "ガードレールを編集",
+ "guardrails.list.remove-item": "ガードレールを削除",
+ "guardrails.list.preview": "プレビュー",
+ "guardrails.list.provider": "プロバイダー",
+ "guardrails.list.action-unknown": "不明",
+ "guardrails.list.byo-disabled-notice": "このガードレールの設定は無効化され、使用できなくなりました。エージェントを実行する前に、管理者に問い合わせて設定を再度有効化するか、このガードレールを置換してください。",
+ "guardrails.list.byo-unavailable-notice": "このガードレールの設定は利用できなくなりました。エージェントを実行する前に、置換してください。",
+ "guardrails.list.status-unauthorized": "未認可",
+ "guardrails.palette.list-aria-label": "利用可能なガードレール",
+ "guardrails.palette.create-custom": "カスタム ガードレール",
+ "guardrails.palette.create-custom-description": "カスタム ルールを含むガードレールを作成します。",
+ "guardrails.palette.empty": "利用できるガードレールはありません。",
+ "guardrails.palette.loading": "読み込んでいます...",
+ "guardrails.palette.load-error": "組み込みバリデーターの読み込みに失敗しました。",
+ "guardrails.palette.uipath-group": "UiPath のガードレール",
+ "guardrails.palette.preview": "プレビュー",
+ "guardrails.palette.status-unauthorized": "未認可"
}
diff --git a/packages/apollo-react/src/canvas/locales/ko.json b/packages/apollo-react/src/canvas/locales/ko.json
index b47e20f5c7..8fc769e348 100644
--- a/packages/apollo-react/src/canvas/locales/ko.json
+++ b/packages/apollo-react/src/canvas/locales/ko.json
@@ -150,5 +150,148 @@
"canvas.node_mode_select.simulated_description": "LLM을 사용하여 동적으로 응답 생성",
"canvas.node_mode_select.simulated_label": "시뮬레이션됨",
"canvas.node_mode_select.static_description": "항상 정의한 값 반환",
- "canvas.node_mode_select.static_label": "정적 모의 개체"
+ "canvas.node_mode_select.static_label": "정적 모의 개체",
+ "guardrails.form.more-information": "자세한 정보",
+ "guardrails.form.enum-placeholder": "선택...",
+ "guardrails.form.enum-list-placeholder": "옵션 선택...",
+ "guardrails.form.add-item": "추가",
+ "guardrails.form.remove-item": "{label} {position} 제거",
+ "guardrails.builder.edit-title": "{name} 가드레일 편집",
+ "guardrails.builder.add-title": "{name} 가드레일 추가",
+ "guardrails.builder.type-label": "가드레일 유형",
+ "guardrails.builder.name-label": "가드레일 이름",
+ "guardrails.builder.name-placeholder": "가드레일 이름 입력",
+ "guardrails.builder.description-label": "가드레일 설명",
+ "guardrails.builder.description-placeholder": "가드레일 설명 입력",
+ "guardrails.builder.evals-label": "평가를 위해 가드레일 활성화",
+ "guardrails.builder.evals-info-aria-label": "자세한 정보",
+ "guardrails.builder.evals-tooltip": "활성화되면 평가 실행 중에 이 가드레일이 적용됩니다.",
+ "guardrails.builder.save-as-new": "새로 저장",
+ "guardrails.builder.cancel": "취소",
+ "guardrails.builder.save": "저장",
+ "guardrails.builder.byo-disabled-message": "이 가드레일의 구성이 비활성화되어 더 이상 사용할 수 없습니다. 에이전트를 실행하기 전에 관리자에게 문의하여 구성을 다시 활성화하거나 이 가드레일을 교체하십시오.",
+ "guardrails.builder.unauthorized-message": "가드레일을 사용할 권한이 없습니다. 구성 설정에 액세스할 수는 있지만, 수정 사항을 저장할 수 없습니다.",
+ "guardrails.builder.feature-disabled-message": "이 가드레일 기능은 비활성화되었습니다. 구성 설정에 액세스할 수는 있지만, 수정 사항을 저장할 수 없습니다. 기능이 비활성화된 동안에는 이 가드레일을 제거하는 것이 가장 좋습니다.",
+ "guardrails.builder.scopes-label": "Scopes",
+ "guardrails.builder.tools-label": "도구",
+ "guardrails.builder.scope-agent-label": "에이전트",
+ "guardrails.builder.scope-llm-label": "LLM 통화",
+ "guardrails.builder.scope-tool-label": "도구",
+ "guardrails.builder.action-type-label": "액션 형식",
+ "guardrails.builder.action-log-label": "로그",
+ "guardrails.builder.action-block-label": "차단",
+ "guardrails.builder.action-filter-label": "필터",
+ "guardrails.builder.action-escalate-label": "에스컬레이션",
+ "guardrails.builder.severity-label": "심각도 수준",
+ "guardrails.builder.severity-info-label": "정보",
+ "guardrails.builder.severity-warning-label": "경고",
+ "guardrails.builder.severity-error-label": "오류",
+ "guardrails.builder.block-reason-label": "차단 이유",
+ "guardrails.builder.block-reason-placeholder": "차단 이유 입력",
+ "guardrails.builder.assign-to-label": "할당 대상",
+ "guardrails.builder.recipient-user-label": "사용자",
+ "guardrails.builder.recipient-group-label": "그룹",
+ "guardrails.builder.recipient-email-label": "이메일 주소",
+ "guardrails.builder.recipient-group-name-label": "그룹 이름",
+ "guardrails.builder.recipient-fallback-label": "수신자",
+ "guardrails.builder.user-search-placeholder": "사용자 검색...",
+ "guardrails.builder.group-search-placeholder": "그룹 검색...",
+ "guardrails.builder.email-placeholder": "이메일 주소 입력",
+ "guardrails.builder.group-name-placeholder": "그룹 이름 입력",
+ "guardrails.builder.action-app-label": "액션 앱",
+ "guardrails.builder.app-picker-unavailable": "앱 선택기를 사용할 수 없음 — Studio Web 호스트가 필요합니다.",
+ "guardrails.builder.mixed-scopes-also-applied": "이 가드레일은 다음에도 적용됩니다.",
+ "guardrails.builder.mixed-scopes-save-as-new-hint": "이 도구에서만 사용할 별도의 복사본을 만들려면 \"새로 저장\"을 사용하십시오.",
+ "guardrails.builder.name-required-error": "가드레일 이름은 필수입니다",
+ "guardrails.builder.name-duplicate-error": "이 이름의 가드레일이 이미 존재합니다",
+ "guardrails.builder.parameter-required-error": "값은 필수입니다.",
+ "guardrails.builder.scopes-required-error": "하나 이상의 범위가 필요합니다",
+ "guardrails.builder.tools-required-error": "하나 이상의 도구가 필요합니다",
+ "guardrails.builder.block-reason-required-error": "차단 이유는 필수입니다",
+ "guardrails.builder.filter-fields-required-error": "필드를 선택해야 합니다",
+ "guardrails.builder.recipient-required-error": "수신자는 필수입니다",
+ "guardrails.builder.action-app-required-error": "액션 앱은 필수입니다",
+ "guardrails.definitions.pii_detection.display-name": "PII 감지",
+ "guardrails.definitions.pii_detection.description": "Azure Cognitive Services를 사용하여 개인 식별 정보를 감지합니다.",
+ "guardrails.definitions.pii_detection.param.entities.label": "감지할 엔터티",
+ "guardrails.definitions.pii_detection.param.entityThresholds.label": "검출 임계값",
+ "guardrails.definitions.pii_detection.param.entityThresholds.tooltip": "0~1 사이의 값입니다. PII 탐지를 위한 민감도 수준입니다. 임계값이 높을수록 더 많은 잠재적인 PII를 탐지하지만 그만큼 가양성도 증가할 수 있습니다.",
+ "guardrails.definitions.pii_detection.option.entities.Person": "사람",
+ "guardrails.definitions.pii_detection.option.entities.Address": "주소",
+ "guardrails.definitions.pii_detection.option.entities.Date": "날짜",
+ "guardrails.definitions.pii_detection.option.entities.PhoneNumber": "전화 번호",
+ "guardrails.definitions.pii_detection.option.entities.EugpsCoordinates": "EU GPS 좌표",
+ "guardrails.definitions.pii_detection.option.entities.Email": "이메일",
+ "guardrails.definitions.pii_detection.option.entities.CreditCardNumber": "신용 카드 번호",
+ "guardrails.definitions.pii_detection.option.entities.InternationalBankingAccountNumber": "국제 은행 계좌 번호(IBAN)",
+ "guardrails.definitions.pii_detection.option.entities.SwiftCode": "스위프트 코드",
+ "guardrails.definitions.pii_detection.option.entities.ABARoutingNumber": "ABA 라우팅 번호",
+ "guardrails.definitions.pii_detection.option.entities.USDriversLicenseNumber": "미국 운전면허증 번호",
+ "guardrails.definitions.pii_detection.option.entities.UKDriversLicenseNumber": "영국 운전면허증 번호",
+ "guardrails.definitions.pii_detection.option.entities.USIndividualTaxpayerIdentification": "미국 개인 납세자 식별 번호(ITIN)",
+ "guardrails.definitions.pii_detection.option.entities.UKUniqueTaxpayerNumber": "영국 고유 납세자 번호(UTR)",
+ "guardrails.definitions.pii_detection.option.entities.USBankAccountNumber": "미국 은행 계좌 번호",
+ "guardrails.definitions.pii_detection.option.entities.USSocialSecurityNumber": "미국 사회 보장 번호(SSN)",
+ "guardrails.definitions.pii_detection.option.entities.UsukPassportNumber": "미국/영국 여권 번호",
+ "guardrails.definitions.pii_detection.option.entities.NOIdentityNumber": "노르웨이 ID 번호",
+ "guardrails.definitions.pii_detection.option.entities.FINationalID": "핀란드 국가 ID",
+ "guardrails.definitions.pii_detection.option.entities.SENationalID": "스웨덴 국가 ID",
+ "guardrails.definitions.pii_detection.option.entities.DKPersonalIdentificationNumber": "덴마크 개인 식별 번호",
+ "guardrails.definitions.pii_detection.option.entities.NLCitizensServiceNumber": "네덜란드 시민 서비스 번호",
+ "guardrails.definitions.pii_detection.option.entities.URL": "URL",
+ "guardrails.definitions.pii_detection.option.entities.IPAddress": "IP 주소",
+ "guardrails.definitions.prompt_injection.display-name": "프롬프트 인젝션",
+ "guardrails.definitions.prompt_injection.description": "이 유효성 검사기는 Noma Security에서 제공되며 LLM 호출에서 악의적인 공격 시도(예: 프롬프트 인젝션, 탈옥)을 감지하도록 설계되었습니다.",
+ "guardrails.definitions.prompt_injection.param.threshold.label": "검출 임계값",
+ "guardrails.definitions.prompt_injection.param.threshold.tooltip": "0~1 사이의 값입니다. 프롬프트 인젝션 탐지를 위한 민감도 수준입니다. 임계값이 높을수록 더 많은 잠재적인 프롬프트 인젝션을 탐지하지만 그만큼 가양성도 증가할 수 있습니다.",
+ "guardrails.definitions.harmful_content.display-name": "유해한 콘텐츠",
+ "guardrails.definitions.harmful_content.description": "Azure AI Content Safety를 사용하여 유해한 콘텐츠(예: 혐오, 폭력)를 감지합니다.",
+ "guardrails.definitions.harmful_content.param.harmfulContentEntities.label": "콘텐츠 범주",
+ "guardrails.definitions.harmful_content.param.harmfulContentEntityThresholds.label": "심각도 임계값",
+ "guardrails.definitions.harmful_content.param.harmfulContentEntityThresholds.tooltip": "0에서 6 사이의 정수 값입니다(2단계). 유해 콘텐츠 감지의 심각도 임계값입니다. 값이 클수록 트리거하기 전에 더 심각한 콘텐츠가 필요합니다.",
+ "guardrails.definitions.harmful_content.option.harmfulContentEntities.Hate": "혐오",
+ "guardrails.definitions.harmful_content.option.harmfulContentEntities.SelfHarm": "자해",
+ "guardrails.definitions.harmful_content.option.harmfulContentEntities.Sexual": "성적인",
+ "guardrails.definitions.harmful_content.option.harmfulContentEntities.Violence": "폭력",
+ "guardrails.definitions.user_prompt_attacks.display-name": "사용자 프롬프트 공격",
+ "guardrails.definitions.user_prompt_attacks.description": "Azure AI Content Safety를 사용하여 시스템 지침을 우회하려는 사용자 프롬프트 공격을 감지합니다.",
+ "guardrails.definitions.intellectual_property.display-name": "지적 재산권",
+ "guardrails.definitions.intellectual_property.description": "Azure AI Content Safety를 사용하여 텍스트와 코드에서 잠재적인 지적 재산권 침해를 감지합니다.",
+ "guardrails.definitions.intellectual_property.param.ipEntities.label": "콘텐츠 유형",
+ "guardrails.definitions.intellectual_property.option.ipEntities.Text": "텍스트",
+ "guardrails.definitions.intellectual_property.option.ipEntities.Code": "코드",
+ "guardrails.definitions.llm_as_judge.display-name": "판단자로서의 LLM",
+ "guardrails.definitions.llm_as_judge.description": "LLM을 판단자로 활용하여 사용자가 정의한 규칙 위반을 탐지합니다.",
+ "guardrails.definitions.llm_as_judge.usage-note": "판단자 모델 호출은 에이전트 자체의 LLM 호출과 동일한 방식으로 Agent Unit을 소모합니다. 비용을 예측 가능하게 유지하려면 선택적으로 적용하고, 비용을 염두에 두고 심판 모델을 선택하십시오 — 요금은 모델마다 다릅니다.",
+ "guardrails.definitions.llm_as_judge.param.guardrailText.label": "규칙 프롬프트",
+ "guardrails.definitions.llm_as_judge.param.model.label": "판단자 모델",
+ "guardrails.definitions.llm_as_judge.param.positiveExamples.label": "긍정적인 예시",
+ "guardrails.definitions.llm_as_judge.param.negativeExamples.label": "부정적인 예시",
+ "guardrails.definitions.llm_as_judge.param.threshold.label": "엄격도",
+ "guardrails.definitions.llm_as_judge.param.guardrailText.tooltip": "판단자가 적용할 규칙을 설명하십시오. 통과 기준과 실패 기준을 구체적으로 명시하십시오.",
+ "guardrails.definitions.llm_as_judge.param.model.tooltip": "각 페이로드에 대해 정책을 평가하는 데 사용되는 모델입니다.",
+ "guardrails.definitions.llm_as_judge.param.positiveExamples.tooltip": "정책을 통과해야 하는 선택적 페이로드입니다. 판단자가 보정 기준으로 사용합니다.",
+ "guardrails.definitions.llm_as_judge.param.negativeExamples.tooltip": "정책을 위반해야 하는 선택적 페이로드입니다. 판단자가 보정 기준으로 사용합니다.",
+ "guardrails.definitions.llm_as_judge.param.threshold.tooltip": "0–6 척도의 엄격도입니다. 값이 작을수록 더 엄격하며 판단자는 위반 가능성이 조금이라도 있는 경우를 모두 표시합니다. 값이 클수록 더 관대하며, 명확하고 확실한 위반만 표시합니다.",
+ "guardrails.list.title": "가드레일",
+ "guardrails.list.add": "추가",
+ "guardrails.list.empty": "가드레일이 구성되지 않음",
+ "guardrails.list.reorder-item": "가드레일 {name} 순서 변경",
+ "guardrails.list.edit-item": "가드레일 편집",
+ "guardrails.list.remove-item": "가드레일 제거",
+ "guardrails.list.preview": "미리 보기",
+ "guardrails.list.provider": "공급자",
+ "guardrails.list.action-unknown": "알 수 없음",
+ "guardrails.list.byo-disabled-notice": "이 가드레일의 구성이 비활성화되어 더 이상 사용할 수 없습니다. 에이전트를 실행하기 전에 관리자에게 문의하여 구성을 다시 활성화하거나 이 가드레일을 교체하십시오.",
+ "guardrails.list.byo-unavailable-notice": "이 가드레일의 구성은 더 이상 사용할 수 없습니다. 에이전트를 실행하기 전에 바꾸십시오.",
+ "guardrails.list.status-unauthorized": "권한이 없음",
+ "guardrails.palette.list-aria-label": "사용 가능한 가드레일",
+ "guardrails.palette.create-custom": "사용자 지정 가드레일",
+ "guardrails.palette.create-custom-description": "사용자 지정 규칙으로 가드레일 생성",
+ "guardrails.palette.empty": "사용 가능한 가드레일 없음",
+ "guardrails.palette.loading": "로드하는 중...",
+ "guardrails.palette.load-error": "기본 제공 유효성 검사기를 로드하지 못했습니다",
+ "guardrails.palette.uipath-group": "UiPath 가드레일",
+ "guardrails.palette.preview": "미리 보기",
+ "guardrails.palette.status-unauthorized": "권한이 없음"
}
diff --git a/packages/apollo-react/src/canvas/locales/pt-BR.json b/packages/apollo-react/src/canvas/locales/pt-BR.json
index 9658dfd662..5fabd0dc4a 100644
--- a/packages/apollo-react/src/canvas/locales/pt-BR.json
+++ b/packages/apollo-react/src/canvas/locales/pt-BR.json
@@ -150,5 +150,148 @@
"canvas.node_mode_select.simulated_description": "Gere uma resposta dinamicamente usando um LLM",
"canvas.node_mode_select.simulated_label": "Simulado",
"canvas.node_mode_select.static_description": "Sempre retorne um valor que você define",
- "canvas.node_mode_select.static_label": "Simulação estática"
+ "canvas.node_mode_select.static_label": "Simulação estática",
+ "guardrails.form.more-information": "Mais informações",
+ "guardrails.form.enum-placeholder": "Selecionar...",
+ "guardrails.form.enum-list-placeholder": "Selecionar opções...",
+ "guardrails.form.add-item": "Adicionar",
+ "guardrails.form.remove-item": "Remover {label} {position}",
+ "guardrails.builder.edit-title": "Editar proteção {name}",
+ "guardrails.builder.add-title": "Adicionar proteção {name}",
+ "guardrails.builder.type-label": "Tipo de proteção",
+ "guardrails.builder.name-label": "Nome da proteção",
+ "guardrails.builder.name-placeholder": "Insira o nome da proteção",
+ "guardrails.builder.description-label": "Descrição de proteção",
+ "guardrails.builder.description-placeholder": "Insira a descrição da proteção",
+ "guardrails.builder.evals-label": "Habilitar proteção para avaliações",
+ "guardrails.builder.evals-info-aria-label": "Mais informações",
+ "guardrails.builder.evals-tooltip": "Quando habilitada, esta proteção será aplicada durante as execuções de avaliação.",
+ "guardrails.builder.save-as-new": "Salvar como novo",
+ "guardrails.builder.cancel": "Cancelar",
+ "guardrails.builder.save": "Salvar",
+ "guardrails.builder.byo-disabled-message": "A configuração desta proteção foi desabilitada e não pode mais ser usada. Entre em contato com seu administrador para reativar a configuração ou substitua esta proteção antes de executar o agente.",
+ "guardrails.builder.unauthorized-message": "Você não tem o direito de usar proteções. Você pode acessar as definições de configuração, mas não é possível salvar as modificações.",
+ "guardrails.builder.feature-disabled-message": "Esta funcionalidade de proteção está desabilitada. Você pode acessar as definições de configuração, mas não é possível salvar as modificações. É melhor remover essa proteção enquanto a funcionalidade estiver desabilitada.",
+ "guardrails.builder.scopes-label": "Escopos",
+ "guardrails.builder.tools-label": "Ferramentas",
+ "guardrails.builder.scope-agent-label": "Agente",
+ "guardrails.builder.scope-llm-label": "Chamadas de LLM",
+ "guardrails.builder.scope-tool-label": "Ferramentas",
+ "guardrails.builder.action-type-label": "Tipo de Ação",
+ "guardrails.builder.action-log-label": "Registro",
+ "guardrails.builder.action-block-label": "Bloquear",
+ "guardrails.builder.action-filter-label": "Filtro",
+ "guardrails.builder.action-escalate-label": "Escalar",
+ "guardrails.builder.severity-label": "Nível de severidade",
+ "guardrails.builder.severity-info-label": "Informações",
+ "guardrails.builder.severity-warning-label": "Aviso",
+ "guardrails.builder.severity-error-label": "Erro",
+ "guardrails.builder.block-reason-label": "Motivo do bloqueio",
+ "guardrails.builder.block-reason-placeholder": "Insira o motivo do bloqueio",
+ "guardrails.builder.assign-to-label": "Atribuir a",
+ "guardrails.builder.recipient-user-label": "Usuário",
+ "guardrails.builder.recipient-group-label": "Grupo",
+ "guardrails.builder.recipient-email-label": "Endereço de email",
+ "guardrails.builder.recipient-group-name-label": "Nome do grupo",
+ "guardrails.builder.recipient-fallback-label": "Destinatário",
+ "guardrails.builder.user-search-placeholder": "Pesquisar um usuário...",
+ "guardrails.builder.group-search-placeholder": "Pesquisar um grupo...",
+ "guardrails.builder.email-placeholder": "Inserir endereço de email",
+ "guardrails.builder.group-name-placeholder": "Inserir nome do grupo",
+ "guardrails.builder.action-app-label": "App de ação",
+ "guardrails.builder.app-picker-unavailable": "Seletor de aplicativo indisponível: requer o host do Studio Web.",
+ "guardrails.builder.mixed-scopes-also-applied": "Essa proteção também se aplica a:",
+ "guardrails.builder.mixed-scopes-save-as-new-hint": "Use \"Salvar como novo\" para criar uma cópia separada apenas para esta ferramenta.",
+ "guardrails.builder.name-required-error": "O nome da proteção é necessário",
+ "guardrails.builder.name-duplicate-error": "Já existe uma proteção com esse nome",
+ "guardrails.builder.parameter-required-error": "O valor é necessário",
+ "guardrails.builder.scopes-required-error": "É necessário pelo menos um escopo",
+ "guardrails.builder.tools-required-error": "Pelo menos uma ferramenta é necessária",
+ "guardrails.builder.block-reason-required-error": "É necessário bloquear o motivo",
+ "guardrails.builder.filter-fields-required-error": "A seleção dos campos é necessária",
+ "guardrails.builder.recipient-required-error": "O destinatário é obrigatório",
+ "guardrails.builder.action-app-required-error": "O aplicativo de ação é necessário",
+ "guardrails.definitions.pii_detection.display-name": "Detecção de PII",
+ "guardrails.definitions.pii_detection.description": "Detecte informações de identificação pessoal usando os Serviços Cognitivos do Azure.",
+ "guardrails.definitions.pii_detection.param.entities.label": "Entidades a detectar",
+ "guardrails.definitions.pii_detection.param.entityThresholds.label": "Limites de detecção",
+ "guardrails.definitions.pii_detection.param.entityThresholds.tooltip": "Valor entre 0 e 1. O nível de confidencialidade para detecção de PII. Limites mais altos detectam mais PII em potencial, mas podem resultar em mais falsos positivos.",
+ "guardrails.definitions.pii_detection.option.entities.Person": "Pessoa",
+ "guardrails.definitions.pii_detection.option.entities.Address": "Endereço",
+ "guardrails.definitions.pii_detection.option.entities.Date": "Data",
+ "guardrails.definitions.pii_detection.option.entities.PhoneNumber": "Número de telefone",
+ "guardrails.definitions.pii_detection.option.entities.EugpsCoordinates": "Coordenadas GPS da UE",
+ "guardrails.definitions.pii_detection.option.entities.Email": "Email",
+ "guardrails.definitions.pii_detection.option.entities.CreditCardNumber": "Número do Cartão de Crédito",
+ "guardrails.definitions.pii_detection.option.entities.InternationalBankingAccountNumber": "Número Internacional de Conta Bancária (IBAN)",
+ "guardrails.definitions.pii_detection.option.entities.SwiftCode": "Código Swift",
+ "guardrails.definitions.pii_detection.option.entities.ABARoutingNumber": "Código de identificação ABA",
+ "guardrails.definitions.pii_detection.option.entities.USDriversLicenseNumber": "Número da carteira de motorista dos EUA",
+ "guardrails.definitions.pii_detection.option.entities.UKDriversLicenseNumber": "Número da carteira de motorista do Reino Unido",
+ "guardrails.definitions.pii_detection.option.entities.USIndividualTaxpayerIdentification": "Número de Identificação do Contribuinte Individual dos EUA (ITIN)",
+ "guardrails.definitions.pii_detection.option.entities.UKUniqueTaxpayerNumber": "Número Único de Contribuinte do Reino Unido (UTR)",
+ "guardrails.definitions.pii_detection.option.entities.USBankAccountNumber": "Número da conta bancária dos EUA",
+ "guardrails.definitions.pii_detection.option.entities.USSocialSecurityNumber": "Número do Seguro Social dos EUA (SSN)",
+ "guardrails.definitions.pii_detection.option.entities.UsukPassportNumber": "Número do Passaporte dos EUA/Reino Unido",
+ "guardrails.definitions.pii_detection.option.entities.NOIdentityNumber": "Número de identidade da Noruega",
+ "guardrails.definitions.pii_detection.option.entities.FINationalID": "Documento de identidade da Finlândia",
+ "guardrails.definitions.pii_detection.option.entities.SENationalID": "Documento de identidade da Suécia",
+ "guardrails.definitions.pii_detection.option.entities.DKPersonalIdentificationNumber": "Número de identificação pessoal da Dinamarca",
+ "guardrails.definitions.pii_detection.option.entities.NLCitizensServiceNumber": "Número de atendimento ao cidadão dos Países Baixos",
+ "guardrails.definitions.pii_detection.option.entities.URL": "URL",
+ "guardrails.definitions.pii_detection.option.entities.IPAddress": "Endereço IP",
+ "guardrails.definitions.prompt_injection.display-name": "Injeção de prompt",
+ "guardrails.definitions.prompt_injection.description": "Esse validador é fornecido pelo Noma Security e é criado para detectar tentativas de ataques mal-intencionados (por exemplo, injeção de prompt, jailbreak) nas chamadas de LLM",
+ "guardrails.definitions.prompt_injection.param.threshold.label": "Limite de detecção",
+ "guardrails.definitions.prompt_injection.param.threshold.tooltip": "Valor entre 0 e 1. O nível de confidencialidade para detecção de Injeção de prompt. Limites mais altos detectam mais Injeção de prompt em potencial, mas podem resultar em mais falsos positivos.",
+ "guardrails.definitions.harmful_content.display-name": "Conteúdo prejudicial",
+ "guardrails.definitions.harmful_content.description": "Detecte conteúdo prejudicial (por exemplo, ódio, violência) usando a Segurança de Conteúdo de IA do Azure.",
+ "guardrails.definitions.harmful_content.param.harmfulContentEntities.label": "Categorias de conteúdo",
+ "guardrails.definitions.harmful_content.param.harmfulContentEntityThresholds.label": "Limites de gravidade",
+ "guardrails.definitions.harmful_content.param.harmfulContentEntityThresholds.tooltip": "Valor inteiro entre 0 e 6 (etapa 2). O limite de gravidade para detecção de conteúdo prejudicial. Valores mais altos exigem um conteúdo mais grave antes de serem disparados.",
+ "guardrails.definitions.harmful_content.option.harmfulContentEntities.Hate": "Ódio",
+ "guardrails.definitions.harmful_content.option.harmfulContentEntities.SelfHarm": "Automutilação",
+ "guardrails.definitions.harmful_content.option.harmfulContentEntities.Sexual": "Sexual",
+ "guardrails.definitions.harmful_content.option.harmfulContentEntities.Violence": "Violência",
+ "guardrails.definitions.user_prompt_attacks.display-name": "Ataques de prompt de usuário",
+ "guardrails.definitions.user_prompt_attacks.description": "Detecte ataques de prompt de usuário que tentam ignorar as instruções do sistema usando a Segurança de Conteúdo de IA do Azure.",
+ "guardrails.definitions.intellectual_property.display-name": "Propriedade intelectual",
+ "guardrails.definitions.intellectual_property.description": "Detecte possíveis violações de propriedade intelectual em textos e códigos usando a Segurança de Conteúdo de IA do Azure.",
+ "guardrails.definitions.intellectual_property.param.ipEntities.label": "Tipos de conteúdo",
+ "guardrails.definitions.intellectual_property.option.ipEntities.Text": "Texto",
+ "guardrails.definitions.intellectual_property.option.ipEntities.Code": "Código",
+ "guardrails.definitions.llm_as_judge.display-name": "LLM como juiz",
+ "guardrails.definitions.llm_as_judge.description": "Detecte violações de uma regra que você define, usando um LLM como juiz.",
+ "guardrails.definitions.llm_as_judge.usage-note": "As chamadas de modelo de juiz consomem Agent Units da mesma forma que as chamadas de LLM do próprio agente fazem. Aplique seletivamente para manter o custo previsível e escolha o modelo de juiz com custo em mente — as taxas variam de acordo com o modelo.",
+ "guardrails.definitions.llm_as_judge.param.guardrailText.label": "Prompt de regra",
+ "guardrails.definitions.llm_as_judge.param.model.label": "Modelo de julgamento",
+ "guardrails.definitions.llm_as_judge.param.positiveExamples.label": "Exemplos positivos",
+ "guardrails.definitions.llm_as_judge.param.negativeExamples.label": "Exemplos negativos",
+ "guardrails.definitions.llm_as_judge.param.threshold.label": "Rigor",
+ "guardrails.definitions.llm_as_judge.param.guardrailText.tooltip": "Descreva a regra que o juiz aplicará. Defina com precisão o que deve ser aprovado e o que deve ser reprovado.",
+ "guardrails.definitions.llm_as_judge.param.model.tooltip": "O modelo usado para avaliar a política em relação a cada carga.",
+ "guardrails.definitions.llm_as_judge.param.positiveExamples.tooltip": "Cargas opcionais que devem ser aprovadas na política. Usadas pelo juiz como âncoras de calibração.",
+ "guardrails.definitions.llm_as_judge.param.negativeExamples.tooltip": "Cargas opcionais que devem ser reprovadas na política. Usadas pelo juiz como âncoras de calibração.",
+ "guardrails.definitions.llm_as_judge.param.threshold.tooltip": "Rigor, em uma escala de 0 a 6. Os valores mais baixos são mais rigorosos — o juiz sinaliza qualquer coisa que indica uma violação. Valores mais altos são mais flexíveis — apenas violações claras e inquestionáveis são sinalizadas.",
+ "guardrails.list.title": "Proteções",
+ "guardrails.list.add": "Adicionar",
+ "guardrails.list.empty": "Nenhuma proteção configurada",
+ "guardrails.list.reorder-item": "Reordenar proteção {name}",
+ "guardrails.list.edit-item": "Editar diretriz",
+ "guardrails.list.remove-item": "Remover diretriz",
+ "guardrails.list.preview": "Visualizar",
+ "guardrails.list.provider": "Provedor",
+ "guardrails.list.action-unknown": "Desconhecido",
+ "guardrails.list.byo-disabled-notice": "A configuração desta proteção foi desabilitada e não pode mais ser usada. Entre em contato com seu administrador para reativar a configuração ou substitua esta proteção antes de executar o agente.",
+ "guardrails.list.byo-unavailable-notice": "A configuração desta proteção não está mais disponível. Substitua-a antes de executar o agente.",
+ "guardrails.list.status-unauthorized": "Não autorizado",
+ "guardrails.palette.list-aria-label": "Diretrizes disponíveis",
+ "guardrails.palette.create-custom": "Proteção personalizada",
+ "guardrails.palette.create-custom-description": "Crie uma proteção com regras personalizadas",
+ "guardrails.palette.empty": "Nenhuma proteção disponível",
+ "guardrails.palette.loading": "Carregando...",
+ "guardrails.palette.load-error": "Falha ao carregar validadores integrados",
+ "guardrails.palette.uipath-group": "Proteções da UiPath",
+ "guardrails.palette.preview": "Visualizar",
+ "guardrails.palette.status-unauthorized": "Não autorizado"
}
diff --git a/packages/apollo-react/src/canvas/locales/pt.json b/packages/apollo-react/src/canvas/locales/pt.json
index ca50c60475..8b6ca7a47b 100644
--- a/packages/apollo-react/src/canvas/locales/pt.json
+++ b/packages/apollo-react/src/canvas/locales/pt.json
@@ -150,5 +150,148 @@
"canvas.node_mode_select.simulated_description": "Gerar uma resposta dinamicamente com um LLM",
"canvas.node_mode_select.simulated_label": "Simulado",
"canvas.node_mode_select.static_description": "Devolva sempre um valor que definir",
- "canvas.node_mode_select.static_label": "Fictício estático"
+ "canvas.node_mode_select.static_label": "Fictício estático",
+ "guardrails.form.more-information": "Mais informações",
+ "guardrails.form.enum-placeholder": "Selecionar...",
+ "guardrails.form.enum-list-placeholder": "Selecionar opções…",
+ "guardrails.form.add-item": "Adicionar",
+ "guardrails.form.remove-item": "Remover {label} {position}",
+ "guardrails.builder.edit-title": "Editar barreira de proteção {name}",
+ "guardrails.builder.add-title": "Adicionar barreira de proteção {name}",
+ "guardrails.builder.type-label": "Tipo de verificadores de integridade",
+ "guardrails.builder.name-label": "Nome da Barreira de proteção",
+ "guardrails.builder.name-placeholder": "Introduzir nome da barreira de proteção",
+ "guardrails.builder.description-label": "Descrição da barreira de proteção",
+ "guardrails.builder.description-placeholder": "Introduza a descrição da barreira de proteção",
+ "guardrails.builder.evals-label": "Ativar barreira de proteção para avaliações",
+ "guardrails.builder.evals-info-aria-label": "Mais informações",
+ "guardrails.builder.evals-tooltip": "Quando ativada, esta barreira de proteção será aplicada durante as execuções de avaliação.",
+ "guardrails.builder.save-as-new": "Guardar como novo",
+ "guardrails.builder.cancel": "Cancelar",
+ "guardrails.builder.save": "Guardar",
+ "guardrails.builder.byo-disabled-message": "A configuração desta barreira de proteção foi desativada e já não pode ser utilizada. Contacte o seu administrador para reativar a configuração ou substituir esta barreira de proteção antes de executar o agente.",
+ "guardrails.builder.unauthorized-message": "Não tem direito a utilizar barreiras de proteção. Pode aceder às definições de configuração, mas não é possível guardar as modificações.",
+ "guardrails.builder.feature-disabled-message": "Esta funcionalidade de barreira de proteção está desativada. Pode aceder às definições de configuração, mas não é possível guardar as modificações. É melhor remover este barreira de proteção desde que a funcionalidade esteja desativada.",
+ "guardrails.builder.scopes-label": "Âmbitos",
+ "guardrails.builder.tools-label": "Ferramentas",
+ "guardrails.builder.scope-agent-label": "Agente",
+ "guardrails.builder.scope-llm-label": "Chamadas LLM",
+ "guardrails.builder.scope-tool-label": "Ferramentas",
+ "guardrails.builder.action-type-label": "Tipo de Ação",
+ "guardrails.builder.action-log-label": "Registo",
+ "guardrails.builder.action-block-label": "Bloquear",
+ "guardrails.builder.action-filter-label": "Filtro",
+ "guardrails.builder.action-escalate-label": "Escalar",
+ "guardrails.builder.severity-label": "Nível de gravidade",
+ "guardrails.builder.severity-info-label": "Info",
+ "guardrails.builder.severity-warning-label": "Aviso",
+ "guardrails.builder.severity-error-label": "Erro",
+ "guardrails.builder.block-reason-label": "Motivo do bloqueio",
+ "guardrails.builder.block-reason-placeholder": "Introduza o motivo do bloqueio",
+ "guardrails.builder.assign-to-label": "Atribuir a",
+ "guardrails.builder.recipient-user-label": "Utilizador",
+ "guardrails.builder.recipient-group-label": "Grupo",
+ "guardrails.builder.recipient-email-label": "Endereço de e-mail",
+ "guardrails.builder.recipient-group-name-label": "Nome do grupo",
+ "guardrails.builder.recipient-fallback-label": "Destinatário",
+ "guardrails.builder.user-search-placeholder": "Pesquisar um utilizador…",
+ "guardrails.builder.group-search-placeholder": "Pesquisar um grupo…",
+ "guardrails.builder.email-placeholder": "Introduzir endereço de e-mail",
+ "guardrails.builder.group-name-placeholder": "Introduzir nome do grupo",
+ "guardrails.builder.action-app-label": "Aplicação Action",
+ "guardrails.builder.app-picker-unavailable": "Seletor de aplicações indisponível – requer anfitrião do Studio Web.",
+ "guardrails.builder.mixed-scopes-also-applied": "Esta barreira de proteção se aplica a:",
+ "guardrails.builder.mixed-scopes-save-as-new-hint": "Utilize \"Guardar como novo\" para criar uma cópia separada apenas para esta ferramenta.",
+ "guardrails.builder.name-required-error": "O nome da barreira de proteção é obrigatório",
+ "guardrails.builder.name-duplicate-error": "Já existe uma barreira de proteção com este nome",
+ "guardrails.builder.parameter-required-error": "Valor necessário",
+ "guardrails.builder.scopes-required-error": "É necessário, pelo menos, um âmbito",
+ "guardrails.builder.tools-required-error": "É necessária, pelo menos, uma ferramenta",
+ "guardrails.builder.block-reason-required-error": "O motivo do bloqueio é obrigatório",
+ "guardrails.builder.filter-fields-required-error": "A seleção de campos é obrigatória",
+ "guardrails.builder.recipient-required-error": "O destinatário é obrigatório",
+ "guardrails.builder.action-app-required-error": "A aplicação Action é obrigatória",
+ "guardrails.definitions.pii_detection.display-name": "Deteção de dados pessoais",
+ "guardrails.definitions.pii_detection.description": "Detete informações de identificação pessoal através dos Serviços Cognitivos do Azure.",
+ "guardrails.definitions.pii_detection.param.entities.label": "Entidades a detetar",
+ "guardrails.definitions.pii_detection.param.entityThresholds.label": "Limiares de deteção",
+ "guardrails.definitions.pii_detection.param.entityThresholds.tooltip": "Um valor entre 0 e 1. O nível de sensibilidade para deteção de dados pessoais. Limiares mais elevados detetam mais dados pessoais potenciais, mas podem resultar em mais falsos positivos.",
+ "guardrails.definitions.pii_detection.option.entities.Person": "Pessoa",
+ "guardrails.definitions.pii_detection.option.entities.Address": "Endereço",
+ "guardrails.definitions.pii_detection.option.entities.Date": "Data",
+ "guardrails.definitions.pii_detection.option.entities.PhoneNumber": "Número de Telefone",
+ "guardrails.definitions.pii_detection.option.entities.EugpsCoordinates": "Coordenadas GPS da UE",
+ "guardrails.definitions.pii_detection.option.entities.Email": "E-mail",
+ "guardrails.definitions.pii_detection.option.entities.CreditCardNumber": "Número de Cartão de Crédito",
+ "guardrails.definitions.pii_detection.option.entities.InternationalBankingAccountNumber": "Número de Conta Bancária Internacional (IBAN)",
+ "guardrails.definitions.pii_detection.option.entities.SwiftCode": "Código Swift",
+ "guardrails.definitions.pii_detection.option.entities.ABARoutingNumber": "Número de Roteamento ABA",
+ "guardrails.definitions.pii_detection.option.entities.USDriversLicenseNumber": "Número de Carta de Condução dos EUA",
+ "guardrails.definitions.pii_detection.option.entities.UKDriversLicenseNumber": "Número de Carta de Condução do Reino Unido",
+ "guardrails.definitions.pii_detection.option.entities.USIndividualTaxpayerIdentification": "Número de Identificação de Contribuinte Individual (ITIN) dos EUA",
+ "guardrails.definitions.pii_detection.option.entities.UKUniqueTaxpayerNumber": "Número Único de Contribuinte (UTR) do Reino Unido",
+ "guardrails.definitions.pii_detection.option.entities.USBankAccountNumber": "Número de Conta Bancária dos EUA",
+ "guardrails.definitions.pii_detection.option.entities.USSocialSecurityNumber": "Número de Segurança Social (SSN) dos EUA",
+ "guardrails.definitions.pii_detection.option.entities.UsukPassportNumber": "Número de Passaporte dos EUA/do Reino unido",
+ "guardrails.definitions.pii_detection.option.entities.NOIdentityNumber": "Número de Identidade da Noruega",
+ "guardrails.definitions.pii_detection.option.entities.FINationalID": "ID Nacional da Finlândia",
+ "guardrails.definitions.pii_detection.option.entities.SENationalID": "ID Nacional da Suécia",
+ "guardrails.definitions.pii_detection.option.entities.DKPersonalIdentificationNumber": "Número de Identificação Pessoal Dinamarquês",
+ "guardrails.definitions.pii_detection.option.entities.NLCitizensServiceNumber": "Número de Serviço do Cidadão dos Países Baixos",
+ "guardrails.definitions.pii_detection.option.entities.URL": "URL",
+ "guardrails.definitions.pii_detection.option.entities.IPAddress": "Endereço IP",
+ "guardrails.definitions.prompt_injection.display-name": "Injeção de pedidos",
+ "guardrails.definitions.prompt_injection.description": "Este validador é fornecido pela Noma Security e foi criado para detetar tentativas de ataques maliciosos (por exemplo, injeção de pedidos, jailbreak) em chamadas LLM.",
+ "guardrails.definitions.prompt_injection.param.threshold.label": "Limiar de deteção",
+ "guardrails.definitions.prompt_injection.param.threshold.tooltip": "Um valor entre 0 e 1. O nível de sensibilidade para deteção de Injeção de Pedido. Limiares mais elevados detetam mais Injeção de Pedido potencial, mas podem resultar em mais falsos positivos.",
+ "guardrails.definitions.harmful_content.display-name": "Conteúdo nocivo",
+ "guardrails.definitions.harmful_content.description": "Detetar conteúdo nocivo (por exemplo, ódio, violência) através da Segurança de Conteúdos do Azure AI.",
+ "guardrails.definitions.harmful_content.param.harmfulContentEntities.label": "Categorias de conteúdo",
+ "guardrails.definitions.harmful_content.param.harmfulContentEntityThresholds.label": "Limiares de gravidade",
+ "guardrails.definitions.harmful_content.param.harmfulContentEntityThresholds.tooltip": "Valor de número inteiro entre 0 e 6 (passo 2). O limiar de gravidade para a deteção de conteúdo nocivo. Valores mais elevados requerem conteúdo mais grave antes de serem acionados.",
+ "guardrails.definitions.harmful_content.option.harmfulContentEntities.Hate": "Ódio",
+ "guardrails.definitions.harmful_content.option.harmfulContentEntities.SelfHarm": "Autolesão",
+ "guardrails.definitions.harmful_content.option.harmfulContentEntities.Sexual": "Sexual",
+ "guardrails.definitions.harmful_content.option.harmfulContentEntities.Violence": "Violência",
+ "guardrails.definitions.user_prompt_attacks.display-name": "Ataques por prompt do utilizador",
+ "guardrails.definitions.user_prompt_attacks.description": "Detete ataques por prompt de utilizador que tentam contornar instruções do sistema através da Segurança de Conteúdos do Azure AI.",
+ "guardrails.definitions.intellectual_property.display-name": "Propriedade intelectual",
+ "guardrails.definitions.intellectual_property.description": "Detete potenciais violações de propriedade inteligente no texto e no código através da Segurança de Conteúdos do Azure AI.",
+ "guardrails.definitions.intellectual_property.param.ipEntities.label": "Tipos de conteúdo",
+ "guardrails.definitions.intellectual_property.option.ipEntities.Text": "Texto",
+ "guardrails.definitions.intellectual_property.option.ipEntities.Code": "Código",
+ "guardrails.definitions.llm_as_judge.display-name": "LLM como Judge",
+ "guardrails.definitions.llm_as_judge.description": "Detete violações de uma regra definida por si, utilizando um LLM como judge.",
+ "guardrails.definitions.llm_as_judge.usage-note": "As chamadas de modelo judge consomem Agent Units da mesma forma que as chamadas LLM do próprio agente. Aplique seletivamente para manter os custos previsíveis e escolha o modelo de judge com o custo em mente – as taxas variam consoante o modelo.",
+ "guardrails.definitions.llm_as_judge.param.guardrailText.label": "Pedido de regra",
+ "guardrails.definitions.llm_as_judge.param.model.label": "Modelo de judge",
+ "guardrails.definitions.llm_as_judge.param.positiveExamples.label": "Exemplos positivos",
+ "guardrails.definitions.llm_as_judge.param.negativeExamples.label": "Exemplos negativos",
+ "guardrails.definitions.llm_as_judge.param.threshold.label": "Exatidão",
+ "guardrails.definitions.llm_as_judge.param.guardrailText.tooltip": "Descreva a regra que o judge vai aplicar. Seja específico sobre o que deve passar e o que deve falhar.",
+ "guardrails.definitions.llm_as_judge.param.model.tooltip": "O modelo utilizado para avaliar a política em relação a cada payload.",
+ "guardrails.definitions.llm_as_judge.param.positiveExamples.tooltip": "Payloads opcionais que devem fazer passar a política. Utilizado pelo judge como âncoras de calibração.",
+ "guardrails.definitions.llm_as_judge.param.negativeExamples.tooltip": "Payloads opcionais que devem fazer falhar a política. Utilizado pelo judge como âncoras de calibração.",
+ "guardrails.definitions.llm_as_judge.param.threshold.tooltip": "Exatidão numa escala de 0 a 6. Os valores mais baixos são mais exatos – o judge sinaliza qualquer ação que insinue uma violação. Os valores mais elevados são mais flexíveis – apenas são sinalizadas violações claras e não ambíguas.",
+ "guardrails.list.title": "Barreiras de proteção",
+ "guardrails.list.add": "Adicionar",
+ "guardrails.list.empty": "Nenhuma barreira de proteção configurada",
+ "guardrails.list.reorder-item": "Reordenar barreira de proteção {name}",
+ "guardrails.list.edit-item": "Editar verificador de integridade",
+ "guardrails.list.remove-item": "Remover verificador de integridade",
+ "guardrails.list.preview": "Pré-visualizar",
+ "guardrails.list.provider": "Fornecedor",
+ "guardrails.list.action-unknown": "Desconhecido",
+ "guardrails.list.byo-disabled-notice": "A configuração desta barreira de proteção foi desativada e já não pode ser utilizada. Contacte o seu administrador para reativar a configuração ou substituir esta barreira de proteção antes de executar o agente.",
+ "guardrails.list.byo-unavailable-notice": "A configuração desta barreira de proteção já não está disponível. Substitua-a antes de executar o agente.",
+ "guardrails.list.status-unauthorized": "Não autorizado",
+ "guardrails.palette.list-aria-label": "Verificadores de integridade disponíveis",
+ "guardrails.palette.create-custom": "Barra de proteção personalizada",
+ "guardrails.palette.create-custom-description": "Criar uma barreira de proteção com regras personalizadas",
+ "guardrails.palette.empty": "Nenhuma barreira de proteção disponível",
+ "guardrails.palette.loading": "A carregar...",
+ "guardrails.palette.load-error": "Falha ao carregar validadores incorporados",
+ "guardrails.palette.uipath-group": "Barreiras de proteção UiPath",
+ "guardrails.palette.preview": "Pré-visualizar",
+ "guardrails.palette.status-unauthorized": "Não autorizado"
}
diff --git a/packages/apollo-react/src/canvas/locales/ro.json b/packages/apollo-react/src/canvas/locales/ro.json
index 9ba95f9af7..f40c715e24 100644
--- a/packages/apollo-react/src/canvas/locales/ro.json
+++ b/packages/apollo-react/src/canvas/locales/ro.json
@@ -150,5 +150,148 @@
"canvas.node_mode_select.simulated_description": "Generați dinamic un răspuns folosind un LLM",
"canvas.node_mode_select.simulated_label": "Simulat",
"canvas.node_mode_select.static_description": "Returnați întotdeauna o valoare pe care o definiți",
- "canvas.node_mode_select.static_label": "Simulare statică"
+ "canvas.node_mode_select.static_label": "Simulare statică",
+ "guardrails.form.more-information": "Mai multe informații",
+ "guardrails.form.enum-placeholder": "Selectează...",
+ "guardrails.form.enum-list-placeholder": "Selectați opțiuni...",
+ "guardrails.form.add-item": "Adaugă",
+ "guardrails.form.remove-item": "Eliminați {label} {position}",
+ "guardrails.builder.edit-title": "Editați guardrail-ul {name}",
+ "guardrails.builder.add-title": "Adăugați limita de siguranță {name}",
+ "guardrails.builder.type-label": "Tip mecanism de control",
+ "guardrails.builder.name-label": "Nume mecanism de control",
+ "guardrails.builder.name-placeholder": "Introduceți numele guardrail",
+ "guardrails.builder.description-label": "Descriere mecanism de control",
+ "guardrails.builder.description-placeholder": "Introduceți descrierea guardrail",
+ "guardrails.builder.evals-label": "Activează mecanismul de control pentru evaluări",
+ "guardrails.builder.evals-info-aria-label": "Mai multe informații",
+ "guardrails.builder.evals-tooltip": "Când este activată, această limită de siguranță va fi aplicată în timpul rulărilor de evaluare.",
+ "guardrails.builder.save-as-new": "Salvează ca nou",
+ "guardrails.builder.cancel": "Anulați",
+ "guardrails.builder.save": "Salvați",
+ "guardrails.builder.byo-disabled-message": "Această configurație a mecanismului de control a fost dezactivată și nu mai poate fi utilizată. Contactați administratorul pentru a reactiva configurația sau pentru a înlocui acest mecanism de control înainte de a executa agentul.",
+ "guardrails.builder.unauthorized-message": "Nu aveți dreptul să utilizați guardrails. Puteți accesa setările de configurare, dar modificările nu pot fi salvate.",
+ "guardrails.builder.feature-disabled-message": "Această funcție de protecție este dezactivată. Puteți accesa setările de configurare, dar modificările nu pot fi salvate. Este recomandat să eliminați această protecție cât timp funcția este dezactivată.",
+ "guardrails.builder.scopes-label": "Domenii",
+ "guardrails.builder.tools-label": "Unelte",
+ "guardrails.builder.scope-agent-label": "Agent",
+ "guardrails.builder.scope-llm-label": "Apeluri LLM",
+ "guardrails.builder.scope-tool-label": "Unelte",
+ "guardrails.builder.action-type-label": "Tip de acțiune",
+ "guardrails.builder.action-log-label": "Jurnal",
+ "guardrails.builder.action-block-label": "Block",
+ "guardrails.builder.action-filter-label": "Filtru",
+ "guardrails.builder.action-escalate-label": "Escalează",
+ "guardrails.builder.severity-label": "Nivelul de severitate",
+ "guardrails.builder.severity-info-label": "Informații",
+ "guardrails.builder.severity-warning-label": "Atenție",
+ "guardrails.builder.severity-error-label": "Eroare",
+ "guardrails.builder.block-reason-label": "Motivul blocării",
+ "guardrails.builder.block-reason-placeholder": "Introduceți motivul blocării",
+ "guardrails.builder.assign-to-label": "Atribuiți la",
+ "guardrails.builder.recipient-user-label": "Utilizator",
+ "guardrails.builder.recipient-group-label": "Grup",
+ "guardrails.builder.recipient-email-label": "Adresă de e-mail",
+ "guardrails.builder.recipient-group-name-label": "Nume grup",
+ "guardrails.builder.recipient-fallback-label": "Destinatar",
+ "guardrails.builder.user-search-placeholder": "Căutați un utilizator...",
+ "guardrails.builder.group-search-placeholder": "Căutați un grup...",
+ "guardrails.builder.email-placeholder": "Introduceți adresa de e-mail",
+ "guardrails.builder.group-name-placeholder": "Introduceți numele grupului",
+ "guardrails.builder.action-app-label": "Aplicația Action",
+ "guardrails.builder.app-picker-unavailable": "Selectorul de aplicații nu este disponibil — necesită o gazdă Studio Web.",
+ "guardrails.builder.mixed-scopes-also-applied": "Această măsură de protecție se aplică și la:",
+ "guardrails.builder.mixed-scopes-save-as-new-hint": "Utilizați „Salvați ca nou” pentru a crea o copie separată doar pentru acest instrument.",
+ "guardrails.builder.name-required-error": "Numele mecanismului de control este obligatoriu",
+ "guardrails.builder.name-duplicate-error": "Există deja un mecanism de control cu acest nume",
+ "guardrails.builder.parameter-required-error": "Valoare necesară",
+ "guardrails.builder.scopes-required-error": "Este necesar cel puțin un domeniu",
+ "guardrails.builder.tools-required-error": "Este necesar cel puțin un instrument",
+ "guardrails.builder.block-reason-required-error": "Este necesar un motiv pentru blocare",
+ "guardrails.builder.filter-fields-required-error": "Este necesară selectarea câmpurilor",
+ "guardrails.builder.recipient-required-error": "Este necesar un destinatar",
+ "guardrails.builder.action-app-required-error": "Aplicația Action este necesară",
+ "guardrails.definitions.pii_detection.display-name": "Detectarea PII",
+ "guardrails.definitions.pii_detection.description": "Detectați informațiile de identificare personală utilizând Azure Cognitive Services.",
+ "guardrails.definitions.pii_detection.param.entities.label": "Entități de detectat",
+ "guardrails.definitions.pii_detection.param.entityThresholds.label": "Praguri de detectare",
+ "guardrails.definitions.pii_detection.param.entityThresholds.tooltip": "Valoare între 0 și 1. Nivelul de sensibilitate pentru detectarea PII. Pragurile mai ridicate detectează mai multe PII potențiale, dar pot duce la mai multe rezultate fals pozitive.",
+ "guardrails.definitions.pii_detection.option.entities.Person": "Persoană",
+ "guardrails.definitions.pii_detection.option.entities.Address": "Adresă",
+ "guardrails.definitions.pii_detection.option.entities.Date": "Data",
+ "guardrails.definitions.pii_detection.option.entities.PhoneNumber": "Număr de telefon",
+ "guardrails.definitions.pii_detection.option.entities.EugpsCoordinates": "Coordonate GPS UE",
+ "guardrails.definitions.pii_detection.option.entities.Email": "E-mail",
+ "guardrails.definitions.pii_detection.option.entities.CreditCardNumber": "Număr card de credit",
+ "guardrails.definitions.pii_detection.option.entities.InternationalBankingAccountNumber": "Numărul contului bancar internațional (IBAN)",
+ "guardrails.definitions.pii_detection.option.entities.SwiftCode": "Cod SWIFT",
+ "guardrails.definitions.pii_detection.option.entities.ABARoutingNumber": "Număr de rutare ABA",
+ "guardrails.definitions.pii_detection.option.entities.USDriversLicenseNumber": "Număr permis de conducere SUA",
+ "guardrails.definitions.pii_detection.option.entities.UKDriversLicenseNumber": "Numărul permisului de conducere din Regatul Unit",
+ "guardrails.definitions.pii_detection.option.entities.USIndividualTaxpayerIdentification": "Numărul de identificare al contribuabilului individual din SUA (ITIN)",
+ "guardrails.definitions.pii_detection.option.entities.UKUniqueTaxpayerNumber": "Număr unic de contribuabil din Marea Britanie (UTR)",
+ "guardrails.definitions.pii_detection.option.entities.USBankAccountNumber": "Număr cont bancar SUA",
+ "guardrails.definitions.pii_detection.option.entities.USSocialSecurityNumber": "Număr de Securitate Socială din SUA (SSN)",
+ "guardrails.definitions.pii_detection.option.entities.UsukPassportNumber": "Număr de pașaport SUA/Marea Britanie",
+ "guardrails.definitions.pii_detection.option.entities.NOIdentityNumber": "Număr de identitate din Norvegia",
+ "guardrails.definitions.pii_detection.option.entities.FINationalID": "ID național finlandez",
+ "guardrails.definitions.pii_detection.option.entities.SENationalID": "ID național suedez",
+ "guardrails.definitions.pii_detection.option.entities.DKPersonalIdentificationNumber": "Număr personal de identificare danez",
+ "guardrails.definitions.pii_detection.option.entities.NLCitizensServiceNumber": "Număr de serviciu pentru cetățenii din Țările de Jos",
+ "guardrails.definitions.pii_detection.option.entities.URL": "URL",
+ "guardrails.definitions.pii_detection.option.entities.IPAddress": "Adresa IP",
+ "guardrails.definitions.prompt_injection.display-name": "Injectare prompt",
+ "guardrails.definitions.prompt_injection.description": "Acest validator este furnizat de Noma Security și este construit pentru a detecta încercări de atac malițios (de exemplu, injecție de prompt, jailbreak) în apelurile LLM.",
+ "guardrails.definitions.prompt_injection.param.threshold.label": "Prag de detecție",
+ "guardrails.definitions.prompt_injection.param.threshold.tooltip": "Valoare între 0 și 1. Nivelul de sensibilitate pentru detectarea Injecției Prompt. Pragurile mai ridicate detectează mai multe potențiale Injecții Prompt, dar pot duce la mai multe rezultate fals pozitive.",
+ "guardrails.definitions.harmful_content.display-name": "Conținut dăunător",
+ "guardrails.definitions.harmful_content.description": "Detectați conținutul dăunător (de exemplu, ură, violență) folosind Azure AI Content Safety.",
+ "guardrails.definitions.harmful_content.param.harmfulContentEntities.label": "Categorii de conținut",
+ "guardrails.definitions.harmful_content.param.harmfulContentEntityThresholds.label": "Praguri de severitate",
+ "guardrails.definitions.harmful_content.param.harmfulContentEntityThresholds.tooltip": "Valoare întreagă între 0 și 6 (pas 2). Pragul de severitate pentru detectarea conținutului dăunător. Valorile mai mari necesită ca un conținut mai sever să fie prezent pentru a declanșa detectarea.",
+ "guardrails.definitions.harmful_content.option.harmfulContentEntities.Hate": "Ură",
+ "guardrails.definitions.harmful_content.option.harmfulContentEntities.SelfHarm": "Autovătămare",
+ "guardrails.definitions.harmful_content.option.harmfulContentEntities.Sexual": "Sexual",
+ "guardrails.definitions.harmful_content.option.harmfulContentEntities.Violence": "Violență",
+ "guardrails.definitions.user_prompt_attacks.display-name": "Atacuri prin prompturi ale utilizatorului",
+ "guardrails.definitions.user_prompt_attacks.description": "Detectați atacurile asupra prompturilor utilizatorilor care încearcă să ocolească instrucțiunile de sistem folosind Azure AI Content Safety.",
+ "guardrails.definitions.intellectual_property.display-name": "Proprietate intelectuală",
+ "guardrails.definitions.intellectual_property.description": "Detectați potențiale încălcări ale proprietății intelectuale în text și cod utilizând Azure AI Content Safety.",
+ "guardrails.definitions.intellectual_property.param.ipEntities.label": "Tipuri de conținut",
+ "guardrails.definitions.intellectual_property.option.ipEntities.Text": "Text",
+ "guardrails.definitions.intellectual_property.option.ipEntities.Code": "Cod",
+ "guardrails.definitions.llm_as_judge.display-name": "LLM ca evaluator",
+ "guardrails.definitions.llm_as_judge.description": "Detectați încălcările unei reguli pe care o definiți, folosind un LLM ca evaluator.",
+ "guardrails.definitions.llm_as_judge.usage-note": "Apelurile modelului de evaluare consumă unități Agent în același mod ca apelurile LLM ale agentului. Aplicați selectiv pentru a menține costul previzibil și alegeți modelul de evaluare având în vedere costul — tarifele variază în funcție de model.",
+ "guardrails.definitions.llm_as_judge.param.guardrailText.label": "Solicitare de regulă",
+ "guardrails.definitions.llm_as_judge.param.model.label": "Evaluați modelul",
+ "guardrails.definitions.llm_as_judge.param.positiveExamples.label": "Exemple pozitive",
+ "guardrails.definitions.llm_as_judge.param.negativeExamples.label": "Exemple negative",
+ "guardrails.definitions.llm_as_judge.param.threshold.label": "Rigoare",
+ "guardrails.definitions.llm_as_judge.param.guardrailText.tooltip": "Descrieți regula pe care o va aplica evaluatorul. Specificați clar ce ar trebui să fie acceptat și ce ar trebui să fie respins.",
+ "guardrails.definitions.llm_as_judge.param.model.tooltip": "Modelul utilizat pentru a evalua politica pentru fiecare payload.",
+ "guardrails.definitions.llm_as_judge.param.positiveExamples.tooltip": "Sarcini utile opționale care ar trebui să respecte politica. Folosite de evaluator ca puncte de referință pentru calibrare.",
+ "guardrails.definitions.llm_as_judge.param.negativeExamples.tooltip": "Sarcini utile opționale care ar trebui să nu respecte politica. Folosite de evaluator ca puncte de calibrare.",
+ "guardrails.definitions.llm_as_judge.param.threshold.tooltip": "Rigoare pe o scară de la 0 la 6. Valorile mai mici sunt mai stricte — evaluatorul semnalează orice element care sugerează o încălcare. Valorile mai mari sunt mai permisive — sunt semnalate doar încălcările clare, fără ambiguitate.",
+ "guardrails.list.title": "Mecanisme de Control",
+ "guardrails.list.add": "Adaugă",
+ "guardrails.list.empty": "Nu sunt configurate măsuri de protecție",
+ "guardrails.list.reorder-item": "Reordonați limita de protecție {name}",
+ "guardrails.list.edit-item": "Editați limita de protecție",
+ "guardrails.list.remove-item": "Eliminați mecanismul de control",
+ "guardrails.list.preview": "Previzualizare",
+ "guardrails.list.provider": "Furnizor",
+ "guardrails.list.action-unknown": "Necunoscut",
+ "guardrails.list.byo-disabled-notice": "Această configurație a mecanismului de control a fost dezactivată și nu mai poate fi utilizată. Contactați administratorul pentru a reactiva configurația sau pentru a înlocui acest mecanism de control înainte de a executa agentul.",
+ "guardrails.list.byo-unavailable-notice": "Configurația acestui mecanism de control nu mai este disponibilă. Înlocuiți-l înainte de a executa agentul.",
+ "guardrails.list.status-unauthorized": "Neautorizat",
+ "guardrails.palette.list-aria-label": "Mecanisme de control disponibile",
+ "guardrails.palette.create-custom": "Limită de protecție personalizată",
+ "guardrails.palette.create-custom-description": "Creați un guardrail cu reguli personalizate",
+ "guardrails.palette.empty": "Nu există mecanisme de control disponibile",
+ "guardrails.palette.loading": "Se încarcă...",
+ "guardrails.palette.load-error": "Nu s-au putut încărca validatorii încorporați",
+ "guardrails.palette.uipath-group": "UiPath mecanisme de control",
+ "guardrails.palette.preview": "Previzualizare",
+ "guardrails.palette.status-unauthorized": "Neautorizat"
}
diff --git a/packages/apollo-react/src/canvas/locales/tr.json b/packages/apollo-react/src/canvas/locales/tr.json
index 04a0b5bad4..e1488862b3 100644
--- a/packages/apollo-react/src/canvas/locales/tr.json
+++ b/packages/apollo-react/src/canvas/locales/tr.json
@@ -150,5 +150,148 @@
"canvas.node_mode_select.simulated_description": "LLM kullanarak dinamik olarak yanıt oluşturun",
"canvas.node_mode_select.simulated_label": "Benzetim yapıldı",
"canvas.node_mode_select.static_description": "Her zaman tanımladığınız bir değeri döndür",
- "canvas.node_mode_select.static_label": "Statik sahte"
+ "canvas.node_mode_select.static_label": "Statik sahte",
+ "guardrails.form.more-information": "Daha fazla bilgi",
+ "guardrails.form.enum-placeholder": "Seç...",
+ "guardrails.form.enum-list-placeholder": "Seçenek belirle...",
+ "guardrails.form.add-item": "Ekle",
+ "guardrails.form.remove-item": "{label} {position} öğesini kaldır",
+ "guardrails.builder.edit-title": "{name} tasarım ve uygulama kuralını düzenle",
+ "guardrails.builder.add-title": "{name} tasarım ve uygulama kuralı ekle",
+ "guardrails.builder.type-label": "Tasarım ve uygulama kuralı türü",
+ "guardrails.builder.name-label": "Tasarım ve uygulama kuralı adı",
+ "guardrails.builder.name-placeholder": "Tasarım ve uygulama kuralı adını girin",
+ "guardrails.builder.description-label": "Tasarım ve uygulama kuralı açıklaması",
+ "guardrails.builder.description-placeholder": "Tasarım ve uygulama kuralı açıklamasını girin",
+ "guardrails.builder.evals-label": "Değerlendirmeler için tasarım ve uygulama kuralını etkinleştir",
+ "guardrails.builder.evals-info-aria-label": "Daha fazla bilgi",
+ "guardrails.builder.evals-tooltip": "Etkinleştirildiğinde bu tasarım ve uygulama kuralı değerlendirme çalıştırmaları sırasında uygulanır",
+ "guardrails.builder.save-as-new": "Yeni olarak kaydet",
+ "guardrails.builder.cancel": "İptal",
+ "guardrails.builder.save": "Kaydet",
+ "guardrails.builder.byo-disabled-message": "Bu guardrail'in yapılandırması devre dışı bırakıldı ve artık kullanılamaz. Aracıyı çalıştırmadan önce yapılandırmayı yeniden etkinleştirmek veya bu guardrail'i değiştirmek için yöneticinizle iletişime geçin.",
+ "guardrails.builder.unauthorized-message": "Tasarım ve uygulama kurallarını kullanma yetkiniz yok. Yapılandırma ayarlarına erişebilirsiniz, ancak değişiklikler kaydedilemez.",
+ "guardrails.builder.feature-disabled-message": "Bu tasarım ve uygulama kuralı özelliği devre dışı bırakıldı. Yapılandırma ayarlarına erişebilirsiniz, ancak değişiklikler kaydedilemez. Bu özellik devre dışı bırakıldığı sürece bu tasarım ve uygulama kuralını kaldırmak en iyisidir.",
+ "guardrails.builder.scopes-label": "Kapsamlar",
+ "guardrails.builder.tools-label": "Araçlar",
+ "guardrails.builder.scope-agent-label": "Agent",
+ "guardrails.builder.scope-llm-label": "LLM çağrıları",
+ "guardrails.builder.scope-tool-label": "Araçlar",
+ "guardrails.builder.action-type-label": "Eylem türü",
+ "guardrails.builder.action-log-label": "Günlük",
+ "guardrails.builder.action-block-label": "Engelle",
+ "guardrails.builder.action-filter-label": "Filtre",
+ "guardrails.builder.action-escalate-label": "İlerlet",
+ "guardrails.builder.severity-label": "Önem düzeyi",
+ "guardrails.builder.severity-info-label": "Bilgi",
+ "guardrails.builder.severity-warning-label": "Uyarı",
+ "guardrails.builder.severity-error-label": "Hata",
+ "guardrails.builder.block-reason-label": "Engelleme nedeni",
+ "guardrails.builder.block-reason-placeholder": "Engelleme nedenini girin",
+ "guardrails.builder.assign-to-label": "Atandığı Hedef",
+ "guardrails.builder.recipient-user-label": "Kullanıcı",
+ "guardrails.builder.recipient-group-label": "Grup",
+ "guardrails.builder.recipient-email-label": "E-posta adresi",
+ "guardrails.builder.recipient-group-name-label": "Grup adı",
+ "guardrails.builder.recipient-fallback-label": "Alıcı",
+ "guardrails.builder.user-search-placeholder": "Bir kullanıcı ara..",
+ "guardrails.builder.group-search-placeholder": "Grupları ara...",
+ "guardrails.builder.email-placeholder": "E-posta adresini girin",
+ "guardrails.builder.group-name-placeholder": "Grup adını girin",
+ "guardrails.builder.action-app-label": "Eylem Uygulaması",
+ "guardrails.builder.app-picker-unavailable": "Uygulama seçici kullanılamıyor — Studio Web ana bilgisayarını gerektirir.",
+ "guardrails.builder.mixed-scopes-also-applied": "Bu tasarım ve uygulama kuralı şunlar için de geçerlidir:",
+ "guardrails.builder.mixed-scopes-save-as-new-hint": "Yalnızca bu araç için ayrı bir kopya oluşturmak üzere \"Yeni olarak kaydet\"i kullanın.",
+ "guardrails.builder.name-required-error": "Tasarım ve uygulama kuralı adı gereklidir",
+ "guardrails.builder.name-duplicate-error": "Bu ada sahip bir tasarım ve uygulama kuralı zaten var.",
+ "guardrails.builder.parameter-required-error": "Değer gereklidir",
+ "guardrails.builder.scopes-required-error": "En az bir kapsam gerekiyor",
+ "guardrails.builder.tools-required-error": "En az bir araç gerekiyor.",
+ "guardrails.builder.block-reason-required-error": "Engelleme nedeni gereklidir",
+ "guardrails.builder.filter-fields-required-error": "Alan seçimi gereklidir",
+ "guardrails.builder.recipient-required-error": "Alıcı gereklidir",
+ "guardrails.builder.action-app-required-error": "Eylem uygulaması gereklidir",
+ "guardrails.definitions.pii_detection.display-name": "PII algılama",
+ "guardrails.definitions.pii_detection.description": "Azure Bilişsel Hizmetler'i kullanarak kimlik bilgilerini algılayın.",
+ "guardrails.definitions.pii_detection.param.entities.label": "Algılanacak varlıklar",
+ "guardrails.definitions.pii_detection.param.entityThresholds.label": "Algılama eşikleri",
+ "guardrails.definitions.pii_detection.param.entityThresholds.tooltip": "0 ile 1 arasında bir değer ör. PII algılama için hassasiyet düzeyi. Daha yüksek eşikler daha fazla olası PII algılar, ancak daha fazla yanlış pozitife neden olabilir.",
+ "guardrails.definitions.pii_detection.option.entities.Person": "Kişi",
+ "guardrails.definitions.pii_detection.option.entities.Address": "Adres",
+ "guardrails.definitions.pii_detection.option.entities.Date": "Tarih",
+ "guardrails.definitions.pii_detection.option.entities.PhoneNumber": "Telefon Numarası",
+ "guardrails.definitions.pii_detection.option.entities.EugpsCoordinates": "AB GPS Koordinatları",
+ "guardrails.definitions.pii_detection.option.entities.Email": "E-posta",
+ "guardrails.definitions.pii_detection.option.entities.CreditCardNumber": "Kredi Kartı Numarası",
+ "guardrails.definitions.pii_detection.option.entities.InternationalBankingAccountNumber": "Uluslararası Bankacılık Hesap Numarası",
+ "guardrails.definitions.pii_detection.option.entities.SwiftCode": "Swift Kodu",
+ "guardrails.definitions.pii_detection.option.entities.ABARoutingNumber": "ABA Routing Numarası",
+ "guardrails.definitions.pii_detection.option.entities.USDriversLicenseNumber": "ABD Sürücü Belgesi Numarası",
+ "guardrails.definitions.pii_detection.option.entities.UKDriversLicenseNumber": "Birleşik Krallık Sürücü Belgesi Numarası",
+ "guardrails.definitions.pii_detection.option.entities.USIndividualTaxpayerIdentification": "ABD Bireysel Vergi Kimlik Numarası (ITIN)",
+ "guardrails.definitions.pii_detection.option.entities.UKUniqueTaxpayerNumber": "Birleşik Krallık Vergi Mükellefi Numarası",
+ "guardrails.definitions.pii_detection.option.entities.USBankAccountNumber": "ABD Banka Hesap Numarası",
+ "guardrails.definitions.pii_detection.option.entities.USSocialSecurityNumber": "ABD Sosyal Güvenlik Numarası (SSN)",
+ "guardrails.definitions.pii_detection.option.entities.UsukPassportNumber": "ABD/Birleşik Krallık Pasaport Numarası",
+ "guardrails.definitions.pii_detection.option.entities.NOIdentityNumber": "Norveç Kimlik Numarası",
+ "guardrails.definitions.pii_detection.option.entities.FINationalID": "Finlandiya Vatandaşlık Kimliği",
+ "guardrails.definitions.pii_detection.option.entities.SENationalID": "İsveç Vatandaşlık Kimliği",
+ "guardrails.definitions.pii_detection.option.entities.DKPersonalIdentificationNumber": "Danca Kişisel Kimlik Numarası",
+ "guardrails.definitions.pii_detection.option.entities.NLCitizensServiceNumber": "Hollanda Vatandaşlık Hizmet Numarası",
+ "guardrails.definitions.pii_detection.option.entities.URL": "URL",
+ "guardrails.definitions.pii_detection.option.entities.IPAddress": "IP Adresi",
+ "guardrails.definitions.prompt_injection.display-name": "İstem ekleme",
+ "guardrails.definitions.prompt_injection.description": "Bu doğrulayıcı, Noma Güvenlik tarafından sağlanmaktadır ve kötü amaçlı saldırı girişimlerini (ör. istem ekleme, jailbreak) tespit etmek için oluşturulmuştur.",
+ "guardrails.definitions.prompt_injection.param.threshold.label": "Algılama eşiği",
+ "guardrails.definitions.prompt_injection.param.threshold.tooltip": "0 ile 1 arasında bir değer ör. İstem Ekleme algılama için hassasiyet düzeyi. Daha yüksek eşikler daha fazla olası İstem Ekleme algılar, ancak daha fazla yanlış pozitife neden olabilir.",
+ "guardrails.definitions.harmful_content.display-name": "Zararlı içerik",
+ "guardrails.definitions.harmful_content.description": "Zararlı içeriği (örn. nefret, şiddet) Azure AI İçerik Güvenliği kullanarak tespit edin.",
+ "guardrails.definitions.harmful_content.param.harmfulContentEntities.label": "İçerik kategorileri",
+ "guardrails.definitions.harmful_content.param.harmfulContentEntityThresholds.label": "Önem düzeyi eşikleri",
+ "guardrails.definitions.harmful_content.param.harmfulContentEntityThresholds.tooltip": "0 ile 6 arasında bir tamsayı değeri (2. adım). Zararlı içerik algılama için önem eşiği. Daha yüksek değerler tetiklenmeden önce daha ciddi içerik gerektirir.",
+ "guardrails.definitions.harmful_content.option.harmfulContentEntities.Hate": "Nefret",
+ "guardrails.definitions.harmful_content.option.harmfulContentEntities.SelfHarm": "Kendine zarar verme",
+ "guardrails.definitions.harmful_content.option.harmfulContentEntities.Sexual": "Cinsel",
+ "guardrails.definitions.harmful_content.option.harmfulContentEntities.Violence": "Şiddet",
+ "guardrails.definitions.user_prompt_attacks.display-name": "Kullanıcı istemi saldırı sayısı",
+ "guardrails.definitions.user_prompt_attacks.description": "Azure AI İçerik Güvenliği kullanarak sistem talimatlarını atlamaya çalışan kullanıcı istemi saldırılarını algılayın.",
+ "guardrails.definitions.intellectual_property.display-name": "Fikri mülkiyet",
+ "guardrails.definitions.intellectual_property.description": "Azure AI İçerik Güvenliği kullanarak metin ve koddaki olası fikri mülkiyet ihlallerini algılayın.",
+ "guardrails.definitions.intellectual_property.param.ipEntities.label": "İçerik türleri",
+ "guardrails.definitions.intellectual_property.option.ipEntities.Text": "Metin",
+ "guardrails.definitions.intellectual_property.option.ipEntities.Code": "Kod",
+ "guardrails.definitions.llm_as_judge.display-name": "Karar yetkilisi olarak LLM",
+ "guardrails.definitions.llm_as_judge.description": "Karar yetkilisi olarak bir LLM'yi kullanarak, tanımladığınız bir kuralın ihlallerini tespit edin.",
+ "guardrails.definitions.llm_as_judge.usage-note": "Karar yetkilisi model çağrıları Agent Units'i agent'ın kendi LLM çağrılarının yaptığı gibi tüketir. Maliyeti öngörülebilir tutmak için seçici bir şekilde uygulayın ve maliyeti göz önünde bulundurarak karar yetkilisi modeli seçin. Oranlar modele göre değişir.",
+ "guardrails.definitions.llm_as_judge.param.guardrailText.label": "Kural istemi",
+ "guardrails.definitions.llm_as_judge.param.model.label": "Karar yetkilisi modeli",
+ "guardrails.definitions.llm_as_judge.param.positiveExamples.label": "Pozitif örnekler",
+ "guardrails.definitions.llm_as_judge.param.negativeExamples.label": "Negatif örnekler",
+ "guardrails.definitions.llm_as_judge.param.threshold.label": "Katılık",
+ "guardrails.definitions.llm_as_judge.param.guardrailText.tooltip": "Karar yetkilisinin uygulayacağı kuralı açıklayın. Nelerin başarılı, nelerin başarısız olması gerektiği konusunda net olun.",
+ "guardrails.definitions.llm_as_judge.param.model.tooltip": "Her yüke karşı politikayı değerlendirmek için kullanılan model.",
+ "guardrails.definitions.llm_as_judge.param.positiveExamples.tooltip": "Politikayı geçmesi gereken isteğe bağlı yükler. Karar yetkilisi tarafından ayar bağlayıcıları olarak kullanılır.",
+ "guardrails.definitions.llm_as_judge.param.negativeExamples.tooltip": "Politikada başarısız olması gereken isteğe bağlı yükler. Karar yetkilisi tarafından ayar bağlayıcıları olarak kullanılır.",
+ "guardrails.definitions.llm_as_judge.param.threshold.tooltip": "0-6 ölçeğinde katılık. Daha düşük değerler daha katıdır. Karar yetkilisi ihlali ima eden her şeyi işaretler. Daha yüksek değerler daha esnektir. Yalnızca net ihlaller işaretlenir.",
+ "guardrails.list.title": "Tasarım ve uygulama kuralları",
+ "guardrails.list.add": "Ekle",
+ "guardrails.list.empty": "Tasarım ve uygulama kuralı yapılandırılmamış",
+ "guardrails.list.reorder-item": "{name} tasarım ve uygulama kuralını yeniden sırala",
+ "guardrails.list.edit-item": "Tasarım ve uygulama kuralını düzenle",
+ "guardrails.list.remove-item": "Uygulama ve tasarım kuralını kaldır",
+ "guardrails.list.preview": "Önizleme",
+ "guardrails.list.provider": "Sağlayıcı",
+ "guardrails.list.action-unknown": "Bilinmiyor",
+ "guardrails.list.byo-disabled-notice": "Bu guardrail'in yapılandırması devre dışı bırakıldı ve artık kullanılamaz. Aracıyı çalıştırmadan önce yapılandırmayı yeniden etkinleştirmek veya bu guardrail'i değiştirmek için yöneticinizle iletişime geçin.",
+ "guardrails.list.byo-unavailable-notice": "Bu guardrail yapılandırması artık kullanılamıyor. Aracıyı çalıştırmadan önce değiştirin.",
+ "guardrails.list.status-unauthorized": "Yetkilendirilmemiş",
+ "guardrails.palette.list-aria-label": "Kullanılabilir tasarım ve uygulama kuralları",
+ "guardrails.palette.create-custom": "Özel tasarım ve uygulama kuralı",
+ "guardrails.palette.create-custom-description": "Özel kurallarla bir tasarım ve uygulama kuralı oluştur",
+ "guardrails.palette.empty": "Kullanılabilir guardrail yok",
+ "guardrails.palette.loading": "Yükleniyor...",
+ "guardrails.palette.load-error": "Yerleşik doğrulayıcılar yüklenemedi",
+ "guardrails.palette.uipath-group": "UiPath korumaları",
+ "guardrails.palette.preview": "Önizleme",
+ "guardrails.palette.status-unauthorized": "Yetkilendirilmemiş"
}
diff --git a/packages/apollo-react/src/canvas/locales/zh-CN.json b/packages/apollo-react/src/canvas/locales/zh-CN.json
index 2388216415..fe8290f167 100644
--- a/packages/apollo-react/src/canvas/locales/zh-CN.json
+++ b/packages/apollo-react/src/canvas/locales/zh-CN.json
@@ -150,5 +150,148 @@
"canvas.node_mode_select.simulated_description": "使用 LLM 动态生成响应",
"canvas.node_mode_select.simulated_label": "已模拟",
"canvas.node_mode_select.static_description": "始终返回您定义的值",
- "canvas.node_mode_select.static_label": "静态模拟"
+ "canvas.node_mode_select.static_label": "静态模拟",
+ "guardrails.form.more-information": "更多信息",
+ "guardrails.form.enum-placeholder": "选择...",
+ "guardrails.form.enum-list-placeholder": "选择选项...",
+ "guardrails.form.add-item": "添加",
+ "guardrails.form.remove-item": "移除 {label} {position}",
+ "guardrails.builder.edit-title": "编辑 {name} 防护机制",
+ "guardrails.builder.add-title": "添加 {name} 防护机制",
+ "guardrails.builder.type-label": "防护机制类型",
+ "guardrails.builder.name-label": "防护机制名称",
+ "guardrails.builder.name-placeholder": "输入防护机制名称",
+ "guardrails.builder.description-label": "防护机制说明",
+ "guardrails.builder.description-placeholder": "输入防护机制描述",
+ "guardrails.builder.evals-label": "为评估启用防护机制",
+ "guardrails.builder.evals-info-aria-label": "更多信息",
+ "guardrails.builder.evals-tooltip": "启用后,此防护机制将在评估运行期间生效。",
+ "guardrails.builder.save-as-new": "另存为新内容",
+ "guardrails.builder.cancel": "取消",
+ "guardrails.builder.save": "保存",
+ "guardrails.builder.byo-disabled-message": "此防护机制配置已禁用,无法再使用。请在运行智能体之前联系您的管理员重新启用配置或替换此防护机制。",
+ "guardrails.builder.unauthorized-message": "您无权使用防护机制。您可以访问配置设置,但无法保存修改。",
+ "guardrails.builder.feature-disabled-message": "此防护机制功能已禁用。您可以访问配置设置,但无法保存修改。只要该功能处于禁用状态,最好移除此防护机制。",
+ "guardrails.builder.scopes-label": "作用域",
+ "guardrails.builder.tools-label": "工具",
+ "guardrails.builder.scope-agent-label": "代理",
+ "guardrails.builder.scope-llm-label": "LLM 调用",
+ "guardrails.builder.scope-tool-label": "工具",
+ "guardrails.builder.action-type-label": "操作类型",
+ "guardrails.builder.action-log-label": "日志",
+ "guardrails.builder.action-block-label": "屏蔽",
+ "guardrails.builder.action-filter-label": "筛选",
+ "guardrails.builder.action-escalate-label": "升级",
+ "guardrails.builder.severity-label": "严重性级别",
+ "guardrails.builder.severity-info-label": "Info",
+ "guardrails.builder.severity-warning-label": "警告",
+ "guardrails.builder.severity-error-label": "错误",
+ "guardrails.builder.block-reason-label": "阻止原因",
+ "guardrails.builder.block-reason-placeholder": "输入阻止原因",
+ "guardrails.builder.assign-to-label": "分配给",
+ "guardrails.builder.recipient-user-label": "用户",
+ "guardrails.builder.recipient-group-label": "组",
+ "guardrails.builder.recipient-email-label": "电子邮件地址",
+ "guardrails.builder.recipient-group-name-label": "组名称",
+ "guardrails.builder.recipient-fallback-label": "收件人",
+ "guardrails.builder.user-search-placeholder": "搜索用户...",
+ "guardrails.builder.group-search-placeholder": "搜索组...",
+ "guardrails.builder.email-placeholder": "输入电子邮件地址",
+ "guardrails.builder.group-name-placeholder": "输入组名称",
+ "guardrails.builder.action-app-label": "操作应用程序",
+ "guardrails.builder.app-picker-unavailable": "应用程序选取器不可用 — 需要 Studio Web 主机。",
+ "guardrails.builder.mixed-scopes-also-applied": "此防护机制也应用于:",
+ "guardrails.builder.mixed-scopes-save-as-new-hint": "请使用“另存为新”为此工具单独创建一份副本。",
+ "guardrails.builder.name-required-error": "防护机制名称为必填项",
+ "guardrails.builder.name-duplicate-error": "已存在同名防护机制",
+ "guardrails.builder.parameter-required-error": "值为必填字段",
+ "guardrails.builder.scopes-required-error": "至少需要一个作用域",
+ "guardrails.builder.tools-required-error": "至少需要一个工具",
+ "guardrails.builder.block-reason-required-error": "阻止原因为必填项",
+ "guardrails.builder.filter-fields-required-error": "“字段选择”为必填项",
+ "guardrails.builder.recipient-required-error": "收件人为必填项",
+ "guardrails.builder.action-app-required-error": "操作应用程序为必填项",
+ "guardrails.definitions.pii_detection.display-name": "PII 检测",
+ "guardrails.definitions.pii_detection.description": "使用 Azure 认知服务检测个人身份信息。",
+ "guardrails.definitions.pii_detection.param.entities.label": "要检测的实体",
+ "guardrails.definitions.pii_detection.param.entityThresholds.label": "检测阈值",
+ "guardrails.definitions.pii_detection.param.entityThresholds.tooltip": "介于 0 和 1 之间的值。PII 检测的敏感度级别。阈值越高,可检测到的潜在 PII 就越多,但可能会导致更多的误报。",
+ "guardrails.definitions.pii_detection.option.entities.Person": "人员",
+ "guardrails.definitions.pii_detection.option.entities.Address": "地址",
+ "guardrails.definitions.pii_detection.option.entities.Date": "日期",
+ "guardrails.definitions.pii_detection.option.entities.PhoneNumber": "电话号码",
+ "guardrails.definitions.pii_detection.option.entities.EugpsCoordinates": "欧盟 GPS 坐标",
+ "guardrails.definitions.pii_detection.option.entities.Email": "电子邮件",
+ "guardrails.definitions.pii_detection.option.entities.CreditCardNumber": "信用卡号",
+ "guardrails.definitions.pii_detection.option.entities.InternationalBankingAccountNumber": "国际银行账号 (IBAN)",
+ "guardrails.definitions.pii_detection.option.entities.SwiftCode": "SWIFT 代码",
+ "guardrails.definitions.pii_detection.option.entities.ABARoutingNumber": "ABA 路由号码",
+ "guardrails.definitions.pii_detection.option.entities.USDriversLicenseNumber": "美国驾驶证号",
+ "guardrails.definitions.pii_detection.option.entities.UKDriversLicenseNumber": "英国驾驶证号",
+ "guardrails.definitions.pii_detection.option.entities.USIndividualTaxpayerIdentification": "美国个人纳税识别号 (ITIN)",
+ "guardrails.definitions.pii_detection.option.entities.UKUniqueTaxpayerNumber": "英国唯一纳税人编号 (UTR)",
+ "guardrails.definitions.pii_detection.option.entities.USBankAccountNumber": "美国银行账号",
+ "guardrails.definitions.pii_detection.option.entities.USSocialSecurityNumber": "美国社会保障号码 (SSN)",
+ "guardrails.definitions.pii_detection.option.entities.UsukPassportNumber": "美国/英国护照号码",
+ "guardrails.definitions.pii_detection.option.entities.NOIdentityNumber": "挪威身份号码",
+ "guardrails.definitions.pii_detection.option.entities.FINationalID": "芬兰国民身份证号码",
+ "guardrails.definitions.pii_detection.option.entities.SENationalID": "瑞典国民身份证号码",
+ "guardrails.definitions.pii_detection.option.entities.DKPersonalIdentificationNumber": "丹麦个人身份证号码",
+ "guardrails.definitions.pii_detection.option.entities.NLCitizensServiceNumber": "荷兰公民服务号码",
+ "guardrails.definitions.pii_detection.option.entities.URL": "URL",
+ "guardrails.definitions.pii_detection.option.entities.IPAddress": "IP 地址",
+ "guardrails.definitions.prompt_injection.display-name": "提示词注入",
+ "guardrails.definitions.prompt_injection.description": "此验证程序由 Noma Security 提供,旨在检测 LLM 调用中的恶意攻击企图(例如提示词注入、越狱攻击)。",
+ "guardrails.definitions.prompt_injection.param.threshold.label": "检测阈值",
+ "guardrails.definitions.prompt_injection.param.threshold.tooltip": "介于 0 和 1 之间的值。提示词注入检测的敏感度级别。阈值越高,可检测到的潜在提示词注入就越多,但可能会导致更多的误报。",
+ "guardrails.definitions.harmful_content.display-name": "有害内容",
+ "guardrails.definitions.harmful_content.description": "使用 Azure AI 内容安全检测有害内容(例如仇恨、暴力)。",
+ "guardrails.definitions.harmful_content.param.harmfulContentEntities.label": "内容类别",
+ "guardrails.definitions.harmful_content.param.harmfulContentEntityThresholds.label": "严重性阈值",
+ "guardrails.definitions.harmful_content.param.harmfulContentEntityThresholds.tooltip": "介于 0 到 6 之间的整数值(第 2 步)。有害内容检测的严重性阈值。数值越高,触发所需的内容严重程度就越高。",
+ "guardrails.definitions.harmful_content.option.harmfulContentEntities.Hate": "仇恨",
+ "guardrails.definitions.harmful_content.option.harmfulContentEntities.SelfHarm": "自残",
+ "guardrails.definitions.harmful_content.option.harmfulContentEntities.Sexual": "色情",
+ "guardrails.definitions.harmful_content.option.harmfulContentEntities.Violence": "暴力",
+ "guardrails.definitions.user_prompt_attacks.display-name": "用户提示攻击",
+ "guardrails.definitions.user_prompt_attacks.description": "使用 Azure AI 内容安全检测试图绕过系统指令的用户提示攻击。",
+ "guardrails.definitions.intellectual_property.display-name": "知识产权",
+ "guardrails.definitions.intellectual_property.description": "使用 Azure AI 内容安全检测文本和代码中潜在的知识产权侵犯行为。",
+ "guardrails.definitions.intellectual_property.param.ipEntities.label": "内容类型",
+ "guardrails.definitions.intellectual_property.option.ipEntities.Text": "文本",
+ "guardrails.definitions.intellectual_property.option.ipEntities.Code": "代码",
+ "guardrails.definitions.llm_as_judge.display-name": "LLM 作为评判器",
+ "guardrails.definitions.llm_as_judge.description": "使用 LLM 作为评判器检测您定义的规则违规行为。",
+ "guardrails.definitions.llm_as_judge.usage-note": "评判模型调用与智能体自身的 LLM 调用一样,会消耗 Agent Unit。请有选择地使用以保持成本可预测,并在选择评判模型时考虑成本 — 不同模型的费率不同。",
+ "guardrails.definitions.llm_as_judge.param.guardrailText.label": "规则提示词",
+ "guardrails.definitions.llm_as_judge.param.model.label": "评判模型",
+ "guardrails.definitions.llm_as_judge.param.positiveExamples.label": "正面示例",
+ "guardrails.definitions.llm_as_judge.param.negativeExamples.label": "负面示例",
+ "guardrails.definitions.llm_as_judge.param.threshold.label": "严格程度",
+ "guardrails.definitions.llm_as_judge.param.guardrailText.tooltip": "描述评判器将执行的规则。请具体说明哪些应通过、哪些应失败。",
+ "guardrails.definitions.llm_as_judge.param.model.tooltip": "用于根据每个负载评估策略的模型。",
+ "guardrails.definitions.llm_as_judge.param.positiveExamples.tooltip": "应该通过策略的可选负载。评判器将其用作校准锚点。",
+ "guardrails.definitions.llm_as_judge.param.negativeExamples.tooltip": "应该违反策略的可选负载。评判器将其用作校准锚点。",
+ "guardrails.definitions.llm_as_judge.param.threshold.tooltip": "严格程度分为 0–6 级。数值越低越严格 — 评判器会标记任何暗示违规的内容。数值越高越宽松 — 仅标记明确、无歧义的违规行为。",
+ "guardrails.list.title": "防护机制",
+ "guardrails.list.add": "添加",
+ "guardrails.list.empty": "未配置防护机制",
+ "guardrails.list.reorder-item": "重新排序防护机制 {name}",
+ "guardrails.list.edit-item": "编辑防护机制",
+ "guardrails.list.remove-item": "删除防护机制",
+ "guardrails.list.preview": "预览",
+ "guardrails.list.provider": "提供程序",
+ "guardrails.list.action-unknown": "未知",
+ "guardrails.list.byo-disabled-notice": "此防护机制配置已禁用,无法再使用。请在运行智能体之前联系您的管理员重新启用配置或替换此防护机制。",
+ "guardrails.list.byo-unavailable-notice": "此防护机制的配置不再可用。在运行智能体之前进行替换。",
+ "guardrails.list.status-unauthorized": "未经授权",
+ "guardrails.palette.list-aria-label": "可用的防护机制",
+ "guardrails.palette.create-custom": "自定义防护机制",
+ "guardrails.palette.create-custom-description": "创建带有自定义规则的防护机制",
+ "guardrails.palette.empty": "没有可用的防护机制",
+ "guardrails.palette.loading": "正在加载…",
+ "guardrails.palette.load-error": "内置验证器加载失败",
+ "guardrails.palette.uipath-group": "UiPath 防护机制",
+ "guardrails.palette.preview": "预览",
+ "guardrails.palette.status-unauthorized": "未经授权"
}
diff --git a/packages/apollo-react/src/canvas/locales/zh-TW.json b/packages/apollo-react/src/canvas/locales/zh-TW.json
index 824ba4c8b4..e66383f072 100644
--- a/packages/apollo-react/src/canvas/locales/zh-TW.json
+++ b/packages/apollo-react/src/canvas/locales/zh-TW.json
@@ -150,5 +150,148 @@
"canvas.node_mode_select.simulated_description": "使用 LLM 動態產生回應",
"canvas.node_mode_select.simulated_label": "已模擬",
"canvas.node_mode_select.static_description": "一律傳回您定義的值",
- "canvas.node_mode_select.static_label": "靜態模擬"
+ "canvas.node_mode_select.static_label": "靜態模擬",
+ "guardrails.form.more-information": "更多資訊",
+ "guardrails.form.enum-placeholder": "選擇...",
+ "guardrails.form.enum-list-placeholder": "選取選項...",
+ "guardrails.form.add-item": "新增",
+ "guardrails.form.remove-item": "移除 {label} {position}",
+ "guardrails.builder.edit-title": "編輯 {name} 護欄",
+ "guardrails.builder.add-title": "新增 {name} 護欄",
+ "guardrails.builder.type-label": "護欄類型",
+ "guardrails.builder.name-label": "護欄名稱",
+ "guardrails.builder.name-placeholder": "輸入護欄名稱",
+ "guardrails.builder.description-label": "護欄說明",
+ "guardrails.builder.description-placeholder": "輸入護欄說明",
+ "guardrails.builder.evals-label": "為評估啟用護欄",
+ "guardrails.builder.evals-info-aria-label": "更多資訊",
+ "guardrails.builder.evals-tooltip": "啟用後,將在評估執行期間套用此護欄。",
+ "guardrails.builder.save-as-new": "另存為新版本",
+ "guardrails.builder.cancel": "取消",
+ "guardrails.builder.save": "儲存",
+ "guardrails.builder.byo-disabled-message": "此護欄的組態已停用,無法再使用。請聯絡管理員以重新啟用組態或取代此護欄,然後再執行代理。",
+ "guardrails.builder.unauthorized-message": "您沒有使用護欄的權限。您可以存取組態設定,但無法儲存修改。",
+ "guardrails.builder.feature-disabled-message": "此護欄功能已停用。您可以存取組態設定,但無法儲存修改。只要此功能處於停用狀態,即建議您將其移除。",
+ "guardrails.builder.scopes-label": "作用域",
+ "guardrails.builder.tools-label": "工具",
+ "guardrails.builder.scope-agent-label": "代理",
+ "guardrails.builder.scope-llm-label": "LLM 呼叫",
+ "guardrails.builder.scope-tool-label": "工具",
+ "guardrails.builder.action-type-label": "動作類型",
+ "guardrails.builder.action-log-label": "日誌",
+ "guardrails.builder.action-block-label": "遮罩",
+ "guardrails.builder.action-filter-label": "篩選",
+ "guardrails.builder.action-escalate-label": "升級",
+ "guardrails.builder.severity-label": "嚴重性層級",
+ "guardrails.builder.severity-info-label": "信息",
+ "guardrails.builder.severity-warning-label": "Warning警告",
+ "guardrails.builder.severity-error-label": "錯誤",
+ "guardrails.builder.block-reason-label": "封鎖原因",
+ "guardrails.builder.block-reason-placeholder": "輸入封鎖原因",
+ "guardrails.builder.assign-to-label": "分配給",
+ "guardrails.builder.recipient-user-label": "使用者",
+ "guardrails.builder.recipient-group-label": "組",
+ "guardrails.builder.recipient-email-label": "電子郵寄地址",
+ "guardrails.builder.recipient-group-name-label": "組名稱",
+ "guardrails.builder.recipient-fallback-label": "收件者",
+ "guardrails.builder.user-search-placeholder": "搜尋使用者...",
+ "guardrails.builder.group-search-placeholder": "搜尋群組...",
+ "guardrails.builder.email-placeholder": "輸入電子郵件地址",
+ "guardrails.builder.group-name-placeholder": "輸入群組名稱",
+ "guardrails.builder.action-app-label": "動作應用程式",
+ "guardrails.builder.app-picker-unavailable": "應用程式選擇器無法使用 — 需要 Studio Web 主機。",
+ "guardrails.builder.mixed-scopes-also-applied": "此護欄也適用於:",
+ "guardrails.builder.mixed-scopes-save-as-new-hint": "使用「另存為新檔案」建立一個僅供此工具使用的單獨副本。",
+ "guardrails.builder.name-required-error": "護欄名稱是必填欄位",
+ "guardrails.builder.name-duplicate-error": "已存在具有此名稱的護欄",
+ "guardrails.builder.parameter-required-error": "值為必填欄位",
+ "guardrails.builder.scopes-required-error": "至少需要一個範圍",
+ "guardrails.builder.tools-required-error": "至少需要一個工具",
+ "guardrails.builder.block-reason-required-error": "封鎖原因為必填欄位",
+ "guardrails.builder.filter-fields-required-error": "必須選取欄位",
+ "guardrails.builder.recipient-required-error": "收件者為必填欄位",
+ "guardrails.builder.action-app-required-error": "動作應用程式為必填欄位",
+ "guardrails.definitions.pii_detection.display-name": "PII 偵測",
+ "guardrails.definitions.pii_detection.description": "使用 Azure 認知服務偵測個人可識別資訊。",
+ "guardrails.definitions.pii_detection.param.entities.label": "要偵測的實體",
+ "guardrails.definitions.pii_detection.param.entityThresholds.label": "偵測閾值",
+ "guardrails.definitions.pii_detection.param.entityThresholds.tooltip": "介於 0 到 1 之間的值。PII 偵測的敏感度層級。較高的閾值可偵測到更多潛在的 PII,但可能會導致更多的誤報。",
+ "guardrails.definitions.pii_detection.option.entities.Person": "人員",
+ "guardrails.definitions.pii_detection.option.entities.Address": "地址",
+ "guardrails.definitions.pii_detection.option.entities.Date": "日期",
+ "guardrails.definitions.pii_detection.option.entities.PhoneNumber": "電話號碼",
+ "guardrails.definitions.pii_detection.option.entities.EugpsCoordinates": "歐盟 GPS 座標",
+ "guardrails.definitions.pii_detection.option.entities.Email": "電子郵件",
+ "guardrails.definitions.pii_detection.option.entities.CreditCardNumber": "信用卡號碼",
+ "guardrails.definitions.pii_detection.option.entities.InternationalBankingAccountNumber": "國際銀行業務帳號 (IBAN)",
+ "guardrails.definitions.pii_detection.option.entities.SwiftCode": "SWIFT 代碼",
+ "guardrails.definitions.pii_detection.option.entities.ABARoutingNumber": "ABA 傳送號碼",
+ "guardrails.definitions.pii_detection.option.entities.USDriversLicenseNumber": "美國駕照號碼",
+ "guardrails.definitions.pii_detection.option.entities.UKDriversLicenseNumber": "英國駕照號碼",
+ "guardrails.definitions.pii_detection.option.entities.USIndividualTaxpayerIdentification": "美國個人納稅人識別碼 (ITIN)",
+ "guardrails.definitions.pii_detection.option.entities.UKUniqueTaxpayerNumber": "英國唯一納稅人號碼 (UTR)",
+ "guardrails.definitions.pii_detection.option.entities.USBankAccountNumber": "美國銀行帳號",
+ "guardrails.definitions.pii_detection.option.entities.USSocialSecurityNumber": "美國社會保險號碼 (SSN)",
+ "guardrails.definitions.pii_detection.option.entities.UsukPassportNumber": "美國/英國護照號碼",
+ "guardrails.definitions.pii_detection.option.entities.NOIdentityNumber": "挪威身分證號碼",
+ "guardrails.definitions.pii_detection.option.entities.FINationalID": "芬蘭國民身分證",
+ "guardrails.definitions.pii_detection.option.entities.SENationalID": "瑞典國民身分證",
+ "guardrails.definitions.pii_detection.option.entities.DKPersonalIdentificationNumber": "丹麥個人識別號碼",
+ "guardrails.definitions.pii_detection.option.entities.NLCitizensServiceNumber": "荷蘭公民服務號碼",
+ "guardrails.definitions.pii_detection.option.entities.URL": "URL",
+ "guardrails.definitions.pii_detection.option.entities.IPAddress": "IP 地址",
+ "guardrails.definitions.prompt_injection.display-name": "提示插入",
+ "guardrails.definitions.prompt_injection.description": "此驗證工具由 NomaSecurity 提供,用於偵測 LLM 呼叫中的惡意攻擊嘗試 (例如,提示插入、越獄)。",
+ "guardrails.definitions.prompt_injection.param.threshold.label": "偵測閾值",
+ "guardrails.definitions.prompt_injection.param.threshold.tooltip": "介於 0 到 1 之間的值。提示插入偵測的敏感度等級。較高的閾值可偵測到更多潛在的提示插入,但可能會導致更多的誤報。",
+ "guardrails.definitions.harmful_content.display-name": "有害內容",
+ "guardrails.definitions.harmful_content.description": "使用 Azure AI 內容安全來偵測有害內容 (例如惡意、暴力)。",
+ "guardrails.definitions.harmful_content.param.harmfulContentEntities.label": "內容類別",
+ "guardrails.definitions.harmful_content.param.harmfulContentEntityThresholds.label": "嚴重性閾值",
+ "guardrails.definitions.harmful_content.param.harmfulContentEntityThresholds.tooltip": "0 至 6 之間的 Integer 值 (步驟 2)。有害內容偵測的嚴重性閾值。若值越高,則需要越嚴重的內容才能觸發。",
+ "guardrails.definitions.harmful_content.option.harmfulContentEntities.Hate": "仇恨",
+ "guardrails.definitions.harmful_content.option.harmfulContentEntities.SelfHarm": "自我損害",
+ "guardrails.definitions.harmful_content.option.harmfulContentEntities.Sexual": "性",
+ "guardrails.definitions.harmful_content.option.harmfulContentEntities.Violence": "暴力",
+ "guardrails.definitions.user_prompt_attacks.display-name": "使用者提示攻擊",
+ "guardrails.definitions.user_prompt_attacks.description": "使用 Azure AI 內容安全偵測嘗試繞過系統指令的使用者提示詞攻擊。",
+ "guardrails.definitions.intellectual_property.display-name": "智慧財產權",
+ "guardrails.definitions.intellectual_property.description": "使用 Azure AI 內容安全功能偵測文字和程式碼中潛在的違反智慧財產權行為。",
+ "guardrails.definitions.intellectual_property.param.ipEntities.label": "內容類型",
+ "guardrails.definitions.intellectual_property.option.ipEntities.Text": "文本",
+ "guardrails.definitions.intellectual_property.option.ipEntities.Code": "代碼",
+ "guardrails.definitions.llm_as_judge.display-name": "LLM 作為評判",
+ "guardrails.definitions.llm_as_judge.description": "使用 LLM 作為評判,偵測所定義規則的違規情況。",
+ "guardrails.definitions.llm_as_judge.usage-note": "評判模型呼叫耗用 Agent Unit 的方式與代理自身的 LLM 呼叫耗用方式相同。請選擇性地應用以保持成本可預測,並在選擇評判模型時納入成本考量 — 費率因模型而異。",
+ "guardrails.definitions.llm_as_judge.param.guardrailText.label": "規則提示",
+ "guardrails.definitions.llm_as_judge.param.model.label": "評判模型",
+ "guardrails.definitions.llm_as_judge.param.positiveExamples.label": "正面範例",
+ "guardrails.definitions.llm_as_judge.param.negativeExamples.label": "負面範例",
+ "guardrails.definitions.llm_as_judge.param.threshold.label": "嚴格性",
+ "guardrails.definitions.llm_as_judge.param.guardrailText.tooltip": "描述評判將強制執行的規則。請具體說明哪些情況應判定為通過,哪些情況應判定為不通過。",
+ "guardrails.definitions.llm_as_judge.param.model.tooltip": "用於針對每個裝載評估原則的模型。",
+ "guardrails.definitions.llm_as_judge.param.positiveExamples.tooltip": "應判定原則通過的選用裝載。由評判當作校準錨點使用。",
+ "guardrails.definitions.llm_as_judge.param.negativeExamples.tooltip": "應判定為原則不通過的選用裝載。由評判當作校準錨點使用。",
+ "guardrails.definitions.llm_as_judge.param.threshold.tooltip": "嚴格性範圍為 0–6。值越低越嚴格 — 評判會標記任何有違規跡象的行為。值越高越寬鬆 — 僅標記明確、無可爭議的違規行為。",
+ "guardrails.list.title": "護欄",
+ "guardrails.list.add": "新增",
+ "guardrails.list.empty": "未配置護欄",
+ "guardrails.list.reorder-item": "重新排序護欄 {name}",
+ "guardrails.list.edit-item": "編輯護欄",
+ "guardrails.list.remove-item": "移除護欄",
+ "guardrails.list.preview": "預覽",
+ "guardrails.list.provider": "提供程式",
+ "guardrails.list.action-unknown": "未知",
+ "guardrails.list.byo-disabled-notice": "此護欄的組態已停用,無法再使用。請聯絡管理員以重新啟用組態或取代此護欄,然後再執行代理。",
+ "guardrails.list.byo-unavailable-notice": "此護欄的組態不再可用。在執行代理程式之前取代它。",
+ "guardrails.list.status-unauthorized": "未經授權",
+ "guardrails.palette.list-aria-label": "可用的護欄",
+ "guardrails.palette.create-custom": "自訂護欄",
+ "guardrails.palette.create-custom-description": "使用自訂規則建立護欄",
+ "guardrails.palette.empty": "沒有可用的護欄",
+ "guardrails.palette.loading": "正在載入...",
+ "guardrails.palette.load-error": "無法載入內建驗證器",
+ "guardrails.palette.uipath-group": "UiPath 護欄",
+ "guardrails.palette.preview": "預覽",
+ "guardrails.palette.status-unauthorized": "未經授權"
}
diff --git a/packages/apollo-react/src/i18n/index.ts b/packages/apollo-react/src/i18n/index.ts
index e7875ec4e4..2dcf885b7a 100644
--- a/packages/apollo-react/src/i18n/index.ts
+++ b/packages/apollo-react/src/i18n/index.ts
@@ -1,3 +1,7 @@
export type { ApI18nProviderProps, SupportedLocale } from './ApI18nProvider';
export { ApI18nProvider, SUPPORTED_LOCALES, useApI18n } from './ApI18nProvider';
+// Sync accessor over the pre-imported catalogs — for hosts that merge component
+// messages into their own lingui singleton (the loadModelPickerMessages pattern)
+// instead of mounting ApI18nProvider.
+export { getPreImportedMessages } from './locale-registry';
export { useSafeLingui } from './useSafeLingui';
diff --git a/packages/apollo-react/src/test/setup.ts b/packages/apollo-react/src/test/setup.ts
index 02413a07f6..905654e755 100644
--- a/packages/apollo-react/src/test/setup.ts
+++ b/packages/apollo-react/src/test/setup.ts
@@ -1,7 +1,11 @@
import '@testing-library/jest-dom';
import { cleanup } from '@testing-library/react';
-import { afterEach } from 'vitest';
+import { toHaveNoViolations } from 'jest-axe';
+import { afterEach, expect } from 'vitest';
+
+// Extend Vitest's expect with jest-axe matchers (a11y assertions in component suites)
+expect.extend(toHaveNoViolations);
// Cleanup after each test
afterEach(() => {
diff --git a/packages/apollo-wind/src/components/forms/field-renderer.tsx b/packages/apollo-wind/src/components/forms/field-renderer.tsx
index 7ee8f008d6..21e4f5422c 100644
--- a/packages/apollo-wind/src/components/forms/field-renderer.tsx
+++ b/packages/apollo-wind/src/components/forms/field-renderer.tsx
@@ -10,8 +10,9 @@ import {
FormFieldError,
FormFieldLabel,
} from '@/components/ui/form-field';
+import { InfoTooltip } from '@/components/ui/info-tooltip';
import { Input } from '@/components/ui/input';
-import { Label } from '@/components/ui/label';
+import { Label, RequiredIndicator } from '@/components/ui/label';
import { MultiSelect } from '@/components/ui/multi-select';
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group';
import {
@@ -35,6 +36,7 @@ import type {
} from './form-schema';
import { hasOptions, isCustomField } from './form-schema';
import { RulesEngine } from './rules-engine';
+import { StringListField } from './string-list-field';
/**
* Field Renderer - Connects metadata to actual UI components
@@ -277,7 +279,10 @@ export function FormFieldRenderer({
name={field.name}
control={control}
defaultValue={field.defaultValue}
- render={({ field: formField, fieldState: { error } }) => (
+ // `ref` is destructured away: custom components are plain function components
+ // (CustomFieldComponentProps declares no ref), and spreading the Controller's ref
+ // onto one triggers React's function-component ref warning.
+ render={({ field: { ref: _ref, ...formField }, fieldState: { error } }) => (
+ {field.label}
+ {required && }
+ {field.tooltip && (
+
+ )}
+
+ );
+}
+
function FieldByType({ field, formField, error, disabled, required, options }: FieldByTypeProps) {
switch (field.type) {
case 'text':
case 'email':
return (
- {field.label}
+
formField.onChange(e.target.value)}
onBlur={formField.onBlur}
@@ -358,8 +392,9 @@ function FieldByType({ field, formField, error, disabled, required, options }: F
case 'number':
return (
- {field.label}
+
- {field.label}
+