diff --git a/packages/apollo-react/src/canvas/components/Guardrails/README.md b/packages/apollo-react/src/canvas/components/Guardrails/README.md
index e6fa069b9..5ae9a4957 100644
--- a/packages/apollo-react/src/canvas/components/Guardrails/README.md
+++ b/packages/apollo-react/src/canvas/components/Guardrails/README.md
@@ -8,9 +8,10 @@ re-exported from `./canvas`). Members: the definitions layer (wire types, parser
copy and `useGuardrailDefinitions`), `GuardrailList` (the applied-guardrails section),
`GuardrailPalette` (the add-guardrail picker), `GuardrailRemoveDialog` (the removal
confirmation), `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`.
+screen shell), `GuardrailValidatorForm` (the validator parameter section, also rendered
+inside the builder), and `CentralizedGuardrailsSection` + `CentralizedGuardrailDetails` (the
+read-only governance guardrails a policy enforces), plus the leaves the sections compose:
+`GuardrailStatusChip`, `GuardrailStatusBanner` and `MixedScopesBanner`.
## Hover and focus, family-wide
@@ -503,6 +504,99 @@ comes from Agents' `common.close`; its own dialog labels that button with a hard
`chore(l10n): sync from Localization`, and until it runs `useSafeLingui` renders the English
default, so nothing is missing on screen.
+## CentralizedGuardrailsSection
+
+The read-only list of guardrails an organization's AI Trust Layer governance policy enforces
+on an agent, and `CentralizedGuardrailDetails`, the content behind a row.
+
+```tsx
+import {
+ CentralizedGuardrailDetails,
+ CentralizedGuardrailsSection,
+ getApplicableCentralizedGuardrails,
+} from '@uipath/apollo-react/canvas/guardrails';
+
+;
+```
+
+Contract highlights:
+
+- **Governance guardrails are their own record.** `CentralizedGuardrail` mirrors both
+ products' policy schemas: no `id`, `scopes` at the top level rather than under a
+ `selector`, and `action` as a bare discriminator rather than an object. It is deliberately
+ not a variant of `GuardrailBuilderValue`. `executionStage` stays `string` because both
+ products parse it as one; `action` is the closed four-value union both close it to, and a
+ TypeScript string enum member assigns to its literal, so Agents' `ActionType` fits.
+- **Props, never contexts.** Both products hold the policy and the definitions in a context of
+ their own (`useGovernance`, `GuardrailDefinitionsContext`, `useAiTrustLayerGovernancePolicy`);
+ passing them in is what lets one component serve both.
+- **The host filters, the component renders.** `getApplicableCentralizedGuardrails` is the
+ predicate for the agent kind being edited, exported so no host rewrites it. An empty
+ `guardrails` renders nothing; `emptyState` overrides that, and an explicit `null` is
+ honoured.
+- **`definitions` is optional, and `undefined` means "not loaded yet".** That is what keeps a
+ row from claiming a configuration was deleted while the catalog is still in flight. An
+ empty array means it loaded and the configuration really is gone.
+- **Scopes, actions and execution stages default to the family's own labels**, with
+ `formatScope` / `formatAction` to override. Every one of those strings already existed in
+ the canvas catalog, so defaulting removes a prop an adapter can forget for a visible
+ regression (`Llm` instead of "LLM calls").
+- **A broken BYO configuration gets a chip and a sentence.** The chip is `GuardrailList`'s
+ own (`Unavailable` / `Disabled`, same ids) and makes the row findable in a long policy; the
+ sentence under it, which both products already show, says what to do about it.
+- **The row's accessible name is its own text.** Both products put an `aria-label` on it,
+ which overrides the content and hides the description, the provider and the
+ broken-configuration message from screen readers entirely.
+- **Layout knobs for both hosts**: `unstyled` drops the card border and padding, `hideHeader`
+ drops the heading, info popover and policy caption. Agents nests the section in its own
+ `SectionAccordion` and uses both.
+- **`docsHref` is opt-in.** Product documentation URLs never ship in this package.
+
+### CentralizedGuardrailDetails
+
+The details **content**, not a shell: Agents opens a dialog and Flow pushes a panel overlay,
+each with its own header, breadcrumb and dismissal, so the surrounding chrome stays host
+orchestration. The `Details in a dialog` and `Details in a panel overlay` stories show both.
+
+```tsx
+;
+```
+
+- **One configuration renderer for both origins.** A BYO guardrail states its configuration
+ as connector parameters and a built-in as `entities` / `entityThresholds`.
+ `resolveCentralizedGuardrailParameters` lifts the built-in fields onto the parameter shape
+ so one resolver covers both, and a threshold map absorbs its `keySource` list into its key
+ column.
+- **Labels and entity names come from the matching definition**, so a centralized guardrail
+ names its entities the way the guardrail editor names them ("US Social Security Number
+ (SSN)", not `USSocialSecurityNumber`) and each validator names its own configuration
+ ("Severity thresholds" for harmful content, "Detection thresholds" for PII). With no
+ definition matched it falls back to generic labels and raw values, which is what both
+ products render today.
+- **A read-only value is text, not a disabled input.** The family's parameter editors are the
+ MetadataForm stack and have no read-only mode, and these values arrive as untyped wire data
+ rather than `GuardrailValidatorParameter`s. A disabled input, which is how Flow renders this
+ today, is also worse than text: it cannot be focused, so its content is not selectable, not
+ copyable and skipped by a screen reader.
+
+Both components resolve a built-in validator's name and description from the canonical copy
+table (see *Definitions layer*), never from the definitions array: a policy can enforce a
+validator this tenant is not entitled to and therefore has no definition for. A BYO
+guardrail's description comes from its connector definition and never from the curated table,
+since a connector may expose a validator id a built-in also uses.
+
## GuardrailBuilder
The complete Add/Edit screen for an OOTB guardrail validator: status banners, usage note,
diff --git a/packages/apollo-react/src/canvas/components/Guardrails/centralized-guardrail-details.test.tsx b/packages/apollo-react/src/canvas/components/Guardrails/centralized-guardrail-details.test.tsx
new file mode 100644
index 000000000..51ef3c21f
--- /dev/null
+++ b/packages/apollo-react/src/canvas/components/Guardrails/centralized-guardrail-details.test.tsx
@@ -0,0 +1,209 @@
+import { render, screen, within } from '@testing-library/react';
+import { axe } from 'jest-axe';
+import { describe, expect, it } from 'vitest';
+import { ApI18nProvider } from '../../../i18n';
+import { CentralizedGuardrailDetails } from './centralized-guardrail-details';
+import type { CentralizedGuardrail, CentralizedGuardrailDefinition } from './centralized-types';
+
+const guardrail = (overrides: Partial = {}): CentralizedGuardrail => ({
+ validator: 'pii_detection',
+ executionStage: 'Both',
+ appliesToAutonomousAgents: true,
+ appliesToConversationalAgents: true,
+ scopes: ['Agent', 'Tool'],
+ action: 'escalate',
+ ...overrides,
+});
+
+const PII_DEFINITION: CentralizedGuardrailDefinition = {
+ validator: 'pii_detection',
+ parameters: [
+ {
+ id: 'entities',
+ type: 'enum-list',
+ label: 'Entities to detect',
+ optionLabels: { Email: 'Email address', USSocialSecurityNumber: 'US SSN' },
+ },
+ {
+ id: 'entityThresholds',
+ type: 'map-enum',
+ label: 'Detection thresholds',
+ keySource: 'entities',
+ },
+ ],
+};
+
+const BYO_DEFINITION: CentralizedGuardrailDefinition = {
+ validator: 'pii_detection',
+ byoValidatorName: 'Acme PII',
+ byoConnectorName: 'Acme Security',
+ description: 'Acme runs its own detector.',
+ parameters: [
+ { id: 'mode', type: 'enum', label: 'Detection mode', optionLabels: { fast: 'Fast' } },
+ { id: 'strict', type: 'boolean', label: 'Strict matching' },
+ ],
+};
+
+/** The value rendered under a `
`, as a screen reader would pair them. */
+const valueFor = (label: string) =>
+ screen.getByText(label).parentElement?.querySelector('dd')?.textContent;
+
+describe('CentralizedGuardrailDetails', () => {
+ it('explains that the configuration is not editable here', () => {
+ render();
+
+ expect(
+ screen.getByText(/governance policy manages this configuration\. You cannot edit it here\./)
+ ).toBeInTheDocument();
+ });
+
+ it('states the guardrail, the policy enforcing it, and what it does', () => {
+ render();
+
+ expect(valueFor('Guardrail type')).toContain('PII detection');
+ expect(valueFor('AI Trust Layer policy')).toBe('Acme policy');
+ expect(valueFor('Execution stage')).toBe('Pre & post-execution');
+ expect(valueFor('Scopes')).toBe('Agent, Tools');
+ expect(valueFor('Action')).toBe('Escalate');
+ });
+
+ it('offers a line of its own when nothing describes the guardrail', () => {
+ render(
+
+ );
+
+ expect(valueFor('Guardrail description')).toBe('No description available.');
+ });
+
+ it('shows the connector and its description for a BYO guardrail', () => {
+ render(
+
+ );
+
+ expect(valueFor('Guardrail type')).toContain('Acme PII');
+ expect(screen.getByText('BYO')).toBeInTheDocument();
+ expect(valueFor('Provider')).toBe('Acme Security');
+ expect(valueFor('Guardrail description')).toBe('Acme runs its own detector.');
+ });
+
+ it('banners a missing configuration and a disabled one', () => {
+ const { rerender } = render(
+
+ );
+ expect(screen.getByText(/could not be found/)).toBeInTheDocument();
+
+ rerender(
+
+ );
+ expect(screen.getByText(/has been disabled/)).toBeInTheDocument();
+ });
+
+ it('names the entities the way the guardrail editor names them', () => {
+ render(
+
+ );
+
+ const thresholds = screen.getByText('Detection thresholds').parentElement as HTMLElement;
+ const rows = within(thresholds).getAllByRole('listitem');
+ expect(rows.map((row) => row.textContent)).toEqual(['Email address0.8', 'US SSN—']);
+ });
+
+ it('renders a BYO connector configuration in the order the connector declares', () => {
+ render(
+
+ );
+
+ const configuration = screen.getByText('Configuration').parentElement as HTMLElement;
+ const terms = within(configuration).getAllByRole('term');
+ expect(terms.map((term) => term.textContent)).toEqual(['Detection mode', 'Strict matching']);
+ expect(valueFor('Detection mode')).toBe('Fast');
+ expect(valueFor('Strict matching')).toBe('Enabled');
+ });
+
+ it('drops the configuration block for a guardrail the policy left unconfigured', () => {
+ render(
+
+ );
+
+ expect(screen.queryByText('Configuration')).not.toBeInTheDocument();
+ });
+
+ it('lets a host replace the scope and action names', () => {
+ render(
+ `<${scope}>`}
+ formatAction={(action) => action.toUpperCase()}
+ />
+ );
+
+ expect(valueFor('Scopes')).toBe(', ');
+ expect(valueFor('Action')).toBe('ESCALATE');
+ });
+
+ it('translates through the ambient catalog', () => {
+ render(
+
+
+
+ );
+
+ // The type, description and scope labels reuse the builder's ids, so they are translated
+ // already; this view's own ids render English until the l10n sync reaches them.
+ expect(screen.getByText('ガードレールの種類')).toBeInTheDocument();
+ expect(screen.getByText('スコープ')).toBeInTheDocument();
+ });
+
+ it('has no axe violations', async () => {
+ const { container } = render(
+
+ );
+
+ expect(await axe(container)).toHaveNoViolations();
+ });
+});
diff --git a/packages/apollo-react/src/canvas/components/Guardrails/centralized-guardrail-details.tsx b/packages/apollo-react/src/canvas/components/Guardrails/centralized-guardrail-details.tsx
new file mode 100644
index 000000000..969a9a7e8
--- /dev/null
+++ b/packages/apollo-react/src/canvas/components/Guardrails/centralized-guardrail-details.tsx
@@ -0,0 +1,126 @@
+import { Alert, AlertDescription, cn } from '@uipath/apollo-wind';
+import { Info } from 'lucide-react';
+import type { GuardrailScope } from './builder-types';
+import {
+ findCentralizedBuiltInDefinition,
+ findCentralizedByoDefinition,
+ formatCentralizedAction,
+ formatCentralizedExecutionStage,
+ formatCentralizedScope,
+ getCentralizedGuardrailDisplay,
+ isCentralizedGuardrailConfigMissing,
+ resolveCentralizedGuardrailParameters,
+} from './centralized-guardrail-utils';
+import type {
+ CentralizedGuardrail,
+ CentralizedGuardrailActionType,
+ CentralizedGuardrailDefinition,
+} from './centralized-types';
+import { CentralizedDetailField } from './components/centralized-detail-field';
+import { CentralizedGuardrailOriginChip } from './components/centralized-guardrail-origin-chip';
+import { CentralizedGuardrailParameters } from './components/centralized-guardrail-parameters';
+import { GuardrailStatusBanner } from './components/guardrail-status-banner';
+import { useGuardrailDefinitionCopy } from './definitions-copy';
+import type { CentralizedGuardrailsLabels } from './i18n';
+import { useCentralizedGuardrailsLabels } from './i18n';
+
+export interface CentralizedGuardrailDetailsProps<
+ TDefinition extends CentralizedGuardrailDefinition = CentralizedGuardrailDefinition,
+> {
+ guardrail: CentralizedGuardrail;
+ /** Name of the AI Trust Layer policy enforcing it. */
+ policyName: string;
+ /** The same array the section receives; `undefined` while the catalog is loading. */
+ definitions?: TDefinition[];
+ /** Replace the localized scope names. Defaults to the family's own scope labels. */
+ formatScope?: (scope: GuardrailScope) => string;
+ /** Replace the localized action name. Defaults to the family's own action labels. */
+ formatAction?: (action: CentralizedGuardrailActionType) => string;
+ labels?: Partial;
+ className?: string;
+}
+
+/** Read-only details of one centralized guardrail: content only, the host owns the shell. */
+export function CentralizedGuardrailDetails<
+ TDefinition extends CentralizedGuardrailDefinition = CentralizedGuardrailDefinition,
+>({
+ guardrail,
+ policyName,
+ definitions,
+ formatScope,
+ formatAction,
+ labels: labelOverrides,
+ className,
+}: CentralizedGuardrailDetailsProps) {
+ const labels = useCentralizedGuardrailsLabels(labelOverrides);
+ const copy = useGuardrailDefinitionCopy();
+
+ const byoDefinition = findCentralizedByoDefinition(guardrail, definitions);
+ const definition = guardrail.isByo
+ ? byoDefinition
+ : findCentralizedBuiltInDefinition(guardrail, definitions);
+ const { name, description } = getCentralizedGuardrailDisplay(guardrail, {
+ definition: byoDefinition,
+ copy,
+ });
+ const isConfigMissing = isCentralizedGuardrailConfigMissing(guardrail, definitions);
+ const isConfigDisabled = byoDefinition?.status === 'Disabled';
+
+ const parameterRows = resolveCentralizedGuardrailParameters(guardrail, {
+ definition,
+ labels: {
+ enabled: labels.parameterEnabled,
+ disabled: labels.parameterDisabled,
+ entities: labels.entitiesFallback,
+ thresholds: labels.thresholdsFallback,
+ },
+ });
+
+ return (
+
+ {/* `note`, not `alert`: nothing just happened. `mt-0` as in `GuardrailStatusBanner`. */}
+
+
+ {labels.managedMessage}
+
+
+ {isConfigMissing && (
+
+ )}
+ {isConfigDisabled && (
+
+ )}
+
+
+ );
+}
diff --git a/packages/apollo-react/src/canvas/components/Guardrails/centralized-guardrails.stories.tsx b/packages/apollo-react/src/canvas/components/Guardrails/centralized-guardrails.stories.tsx
new file mode 100644
index 000000000..3a243f92e
--- /dev/null
+++ b/packages/apollo-react/src/canvas/components/Guardrails/centralized-guardrails.stories.tsx
@@ -0,0 +1,305 @@
+import type { Meta, StoryObj } from '@storybook/react-vite';
+import {
+ Button,
+ Dialog,
+ DialogContent,
+ DialogHeader,
+ DialogTitle,
+ TooltipProvider,
+} from '@uipath/apollo-wind';
+import { ArrowLeft } from 'lucide-react';
+import { useState } from 'react';
+import { CentralizedGuardrailDetails } from './centralized-guardrail-details';
+import { CentralizedGuardrailsSection } from './centralized-guardrails-section';
+import type { CentralizedGuardrail, CentralizedGuardrailDefinition } from './centralized-types';
+
+const meta = {
+ title: 'Components/UiPath/Centralized Guardrails',
+ component: CentralizedGuardrailsSection,
+ parameters: {
+ layout: 'padded',
+ docs: {
+ description: {
+ component: `
+The read-only list of guardrails an organization's AI Trust Layer governance policy enforces
+on an agent, plus the details content behind a row.
+
+Nobody edits these in the product, so the section shows what is enforced and reports which row
+the user picked. Opening the details is host orchestration: one product uses a dialog, the
+other a panel overlay, each with its own header and dismissal, so this package ships the
+content and the stories below show both shells.
+
+A centralized guardrail is its own record rather than a variant of a locally configured one:
+no id, scopes at the top level, and a bare action discriminator. Pass the policy in as props.
+ `,
+ },
+ },
+ },
+ tags: ['autodocs'],
+ decorators: [
+ (Story) => (
+
+
+
+
+
+ ),
+ ],
+} satisfies Meta;
+
+export default meta;
+type Story = StoryObj;
+
+const DOCS_HREF = 'https://docs.example.com/centralized-guardrails';
+
+const piiGuardrail: CentralizedGuardrail = {
+ validator: 'pii_detection',
+ executionStage: 'Pre',
+ appliesToAutonomousAgents: true,
+ appliesToConversationalAgents: true,
+ scopes: ['Agent', 'Llm'],
+ action: 'block',
+ entities: ['Email', 'USSocialSecurityNumber', 'CreditCardNumber'],
+ entityThresholds: { Email: 0.8, USSocialSecurityNumber: 0.95 },
+};
+
+const harmfulContentGuardrail: CentralizedGuardrail = {
+ validator: 'harmful_content',
+ executionStage: 'Both',
+ appliesToAutonomousAgents: true,
+ appliesToConversationalAgents: true,
+ scopes: ['Llm'],
+ action: 'escalate',
+ entityThresholds: { Hate: 2, Violence: 4 },
+};
+
+const byoGuardrail: CentralizedGuardrail = {
+ validator: 'pii_detection',
+ name: 'Acme strict PII',
+ isByo: true,
+ executionStage: 'Post',
+ appliesToAutonomousAgents: true,
+ appliesToConversationalAgents: false,
+ scopes: ['Tool'],
+ action: 'log',
+ parameters: [
+ { id: 'mode', parameterType: 'enum', value: 'thorough' },
+ { id: 'redact', parameterType: 'boolean', value: true },
+ { id: 'entityScores', parameterType: 'map-enum', value: { Email: 0.6, IBAN: 0.9 } },
+ ],
+};
+
+const byoDefinition: CentralizedGuardrailDefinition = {
+ validator: 'pii_detection',
+ byoValidatorName: 'Acme strict PII',
+ byoConnectorName: 'Acme Security',
+ description: 'Acme runs detection against its own corpus before anything leaves the tenant.',
+ parameters: [
+ {
+ id: 'mode',
+ type: 'enum',
+ label: 'Detection mode',
+ optionLabels: { thorough: 'Thorough', fast: 'Fast' },
+ },
+ { id: 'redact', type: 'boolean', label: 'Redact matches' },
+ { id: 'entityScores', type: 'map-enum', label: 'Confidence scores' },
+ ],
+};
+
+const definitions: CentralizedGuardrailDefinition[] = [
+ {
+ validator: 'pii_detection',
+ parameters: [
+ {
+ id: 'entities',
+ type: 'enum-list',
+ label: 'Entities to detect',
+ optionLabels: {
+ Email: 'Email',
+ USSocialSecurityNumber: 'US Social Security Number (SSN)',
+ CreditCardNumber: 'Credit Card Number',
+ },
+ },
+ {
+ id: 'entityThresholds',
+ type: 'map-enum',
+ label: 'Detection thresholds',
+ keySource: 'entities',
+ },
+ ],
+ },
+ {
+ validator: 'harmful_content',
+ parameters: [
+ {
+ id: 'harmfulContentEntities',
+ type: 'enum-list',
+ label: 'Content categories',
+ optionLabels: { Hate: 'Hate', Violence: 'Violence' },
+ },
+ {
+ id: 'harmfulContentEntityThresholds',
+ type: 'map-enum',
+ label: 'Severity thresholds',
+ keySource: 'harmfulContentEntities',
+ },
+ ],
+ },
+ byoDefinition,
+];
+
+const noop = () => {};
+
+export const Default: Story = {
+ args: {
+ guardrails: [piiGuardrail, harmfulContentGuardrail, byoGuardrail],
+ definitions,
+ policyName: 'Acme production policy',
+ docsHref: DOCS_HREF,
+ onSelect: noop,
+ },
+};
+
+export const WithoutDetails: Story = {
+ name: 'Nothing to open',
+ args: { ...Default.args, onSelect: undefined },
+ parameters: {
+ docs: {
+ description: {
+ story:
+ 'Without `onSelect` a row is plain text rather than a disabled button, so it keeps its place in the reading order and out of the tab order.',
+ },
+ },
+ },
+};
+
+export const ConfigurationProblems: Story = {
+ name: 'Broken configurations',
+ args: {
+ guardrails: [byoGuardrail, { ...byoGuardrail, name: 'Acme legacy PII' }],
+ // The second guardrail names a configuration this tenant no longer has at all.
+ definitions: [{ ...byoDefinition, status: 'Disabled' }],
+ policyName: 'Acme production policy',
+ docsHref: DOCS_HREF,
+ },
+ parameters: {
+ docs: {
+ description: {
+ story:
+ 'A chip makes a broken row findable in a long policy; the sentence under it says what to do. Both are shown, since an admin scanning for trouble and an admin fixing it need different things.',
+ },
+ },
+ },
+};
+
+export const StillLoading: Story = {
+ name: 'Catalog still loading',
+ args: {
+ guardrails: [byoGuardrail],
+ definitions: undefined,
+ policyName: 'Acme production policy',
+ },
+ parameters: {
+ docs: {
+ description: {
+ story:
+ 'With `definitions` left undefined the section says nothing about a missing configuration. Passing an empty array means the catalog loaded and the configuration really is gone.',
+ },
+ },
+ },
+};
+
+export const InsideHostChrome: Story = {
+ name: 'Inside a host section',
+ args: {
+ ...Default.args,
+ unstyled: true,
+ hideHeader: true,
+ },
+ decorators: [
+ (Story) => (
+
+
Guardrails (host accordion)
+
+
+
+
+ ),
+ ],
+};
+
+/** The details content in the dialog one product opens. */
+export const DetailsInADialog: Story = {
+ name: 'Details in a dialog',
+ args: Default.args,
+ render: (args) => {
+ const [selected, setSelected] = useState(null);
+ return (
+ <>
+
+
+ >
+ );
+ },
+ parameters: {
+ docs: {
+ description: {
+ story:
+ 'The dialog, its title and its dismissal are the host’s. Fifteen lines of shell is the reason this package ships no wrapper.',
+ },
+ },
+ },
+};
+
+/** The same content in the panel overlay the other product pushes. */
+export const DetailsInAPanel: Story = {
+ name: 'Details in a panel overlay',
+ args: Default.args,
+ render: (args) => {
+ const [selected, setSelected] = useState(null);
+ return (
+
+ {selected === null ? (
+
+
+
+ ) : (
+
+
+
+ Guardrail details
+
+
+
+
+
+ )}
+
+ );
+ },
+};
diff --git a/packages/apollo-react/src/canvas/components/Guardrails/centralized-parity.test.ts b/packages/apollo-react/src/canvas/components/Guardrails/centralized-parity.test.ts
new file mode 100644
index 000000000..478fd9d10
--- /dev/null
+++ b/packages/apollo-react/src/canvas/components/Guardrails/centralized-parity.test.ts
@@ -0,0 +1,196 @@
+import { describe, expect, it } from 'vitest';
+import { CENTRALIZED_GUARDRAILS_EN_LABELS, type CentralizedGuardrailsLabels } from './i18n';
+
+/**
+ * Pins the centralized section's English against each product, by the rules of
+ * `definitions-parity.test.ts`. Transcribed from Agents `origin/main`
+ * (`frontend-sw/src/components/definition/CentralizedGuardrailsSection/*`) and Flow
+ * `origin/develop` (`packages/canvas/src/components/properties-panel/guardrails/Centralized*`).
+ */
+
+type Host = 'agents' | 'flow';
+
+interface HostCopy {
+ agents?: string;
+ flow?: string;
+}
+
+/** What each product says today, keyed by our label. `undefined` means it has no equivalent. */
+const HOST_COPY: Partial> = {
+ title: { agents: 'Centralized guardrails', flow: 'Centralized guardrails' },
+ info: {
+ agents:
+ "These guardrails are enforced by your organization's AI Trust Layer governance policy and cannot be edited here.",
+ flow: "Your organization's AI Trust Layer governance policy enforces these guardrails. You cannot edit them here.",
+ },
+ docsLink: {
+ agents: 'Learn more about centralized guardrails',
+ flow: 'View centralized guardrails documentation',
+ },
+ policyCaption: {
+ agents: 'Enforced by AI Trust Layer policy: {{policyName}}',
+ flow: 'Enforced by AI Trust Layer policy: {{policyName}}',
+ },
+ viewDetails: { agents: 'View details for {{name}}', flow: 'View details for {{name}}' },
+ guardrailType: { flow: 'Guardrail type' },
+ policyField: { flow: 'AI Trust Layer policy' },
+ provider: { agents: 'Provider', flow: 'Provider' },
+ description: { agents: 'Description', flow: 'Guardrail description' },
+ noDescription: { agents: 'No description available.', flow: 'No description available.' },
+ executionStage: { agents: 'Execution stage', flow: 'Execution stage' },
+ scopes: { agents: 'Scopes', flow: 'Scopes' },
+ action: { agents: 'Action', flow: 'Action' },
+ configuration: { agents: 'Configuration' },
+ managedMessage: {
+ agents:
+ "This configuration is managed by your organization's AI Trust Layer governance policy and cannot be edited here.",
+ flow: "Your organization's AI Trust Layer governance policy manages this configuration. You cannot edit it here.",
+ },
+ originByo: { agents: 'BYO', flow: 'BYO' },
+ originUiPath: { agents: 'UiPath managed', flow: 'UiPath managed' },
+ missingConfigMessage: {
+ agents:
+ "This guardrail's configuration could not be found — it may have been deleted. Contact your administrator to fix the AI Trust Layer policy.",
+ flow: "This guardrail's configuration could not be found — it may have been deleted. Contact your administrator to fix the AI Trust Layer policy.",
+ },
+ disabledConfigMessage: {
+ agents:
+ "This guardrail's configuration has been disabled. Contact your administrator to re-enable it.",
+ flow: "This guardrail's configuration has been disabled. Contact your administrator to re-enable it.",
+ },
+ stagePre: { agents: 'Pre-execution', flow: 'Pre-execution' },
+ stagePost: { agents: 'Post-execution', flow: 'Post-execution' },
+ stageBoth: { agents: 'Pre & post-execution', flow: 'Pre & post-execution' },
+ scopeAgent: { agents: 'Agent', flow: 'Agent' },
+ scopeLlm: { agents: 'LLM calls', flow: 'LLM calls' },
+ scopeTool: { agents: 'Tools', flow: 'Tools' },
+ actionBlock: { agents: 'Block', flow: 'Block' },
+ actionEscalate: { agents: 'Escalate', flow: 'Escalate' },
+ actionFilter: { agents: 'Filter', flow: 'Filter' },
+ actionLog: { agents: 'Log', flow: 'Log' },
+ parameterEnabled: { agents: 'Enabled', flow: 'Enabled' },
+ parameterDisabled: { agents: 'Disabled', flow: 'Disabled' },
+ entitiesFallback: { agents: 'Entities to detect', flow: 'Entities to detect' },
+ thresholdsFallback: { agents: 'Detection threshold', flow: 'Detection thresholds' },
+};
+
+interface CopyDivergence {
+ label: keyof CentralizedGuardrailsLabels;
+ chosen: Host;
+ reason: string;
+}
+
+const EXPECTED_DIVERGENCES: CopyDivergence[] = [
+ {
+ label: 'info',
+ chosen: 'flow',
+ reason:
+ 'Active voice and two short sentences; Agents buries the subject in a relative clause. Also the only one of the pair that is translated anywhere',
+ },
+ {
+ label: 'docsLink',
+ chosen: 'flow',
+ reason:
+ 'Agents’ "Learn more about…" reads better, but it ships untranslated in all twelve locales while Flow’s is at 100%, and an untranslated link label is worse than a plainer one',
+ },
+ {
+ label: 'description',
+ chosen: 'flow',
+ reason:
+ 'Matches the builder’s own field label, whose id this reuses, so one string covers both screens',
+ },
+ {
+ label: 'managedMessage',
+ chosen: 'flow',
+ reason: 'Same voice as the section’s info text, which is also Flow’s',
+ },
+ {
+ label: 'thresholdsFallback',
+ chosen: 'flow',
+ reason:
+ 'Plural: this labels a list of per-entity thresholds, not one column header as in Agents’ table',
+ },
+];
+
+/** Labels only one product has; adopting it costs the other nothing. */
+const SINGLE_SOURCE: Array = [
+ 'guardrailType',
+ 'policyField',
+ 'configuration',
+];
+
+/** Neither product's centralized section has these; they are `GuardrailList`'s status chips. */
+const OWN_ADDITIONS: Array = [
+ 'statusUnavailable',
+ 'statusDisabled',
+];
+
+describe('centralized guardrails copy', () => {
+ const divergenceFor = (label: keyof CentralizedGuardrailsLabels) =>
+ EXPECTED_DIVERGENCES.find((entry) => entry.label === label);
+
+ it('says exactly what both products say wherever they already agree', () => {
+ const invented: string[] = [];
+ for (const [label, hosts] of Object.entries(HOST_COPY) as Array<
+ [keyof CentralizedGuardrailsLabels, HostCopy]
+ >) {
+ if (hosts.agents === undefined || hosts.flow === undefined) continue;
+ if (hosts.agents !== hosts.flow) continue;
+ const ours = CENTRALIZED_GUARDRAILS_EN_LABELS[label];
+ if (ours !== hosts.agents)
+ invented.push(`${label}\n both: ${hosts.agents}\n ours: ${ours}`);
+ }
+
+ expect(invented).toEqual([]);
+ });
+
+ it('matches the chosen product verbatim wherever they disagree', () => {
+ const wrong: string[] = [];
+ for (const divergence of EXPECTED_DIVERGENCES) {
+ const chosen = HOST_COPY[divergence.label]?.[divergence.chosen];
+ const ours = CENTRALIZED_GUARDRAILS_EN_LABELS[divergence.label];
+ if (chosen !== ours) {
+ wrong.push(`${divergence.label}\n ${divergence.chosen}: ${chosen}\n ours: ${ours}`);
+ }
+ }
+
+ expect(wrong).toEqual([]);
+ });
+
+ it('declares every disagreement, so a silent third wording cannot slip in', () => {
+ const undeclared: string[] = [];
+ for (const [label, hosts] of Object.entries(HOST_COPY) as Array<
+ [keyof CentralizedGuardrailsLabels, HostCopy]
+ >) {
+ const disagree =
+ hosts.agents !== undefined && hosts.flow !== undefined && hosts.agents !== hosts.flow;
+ if (disagree && divergenceFor(label) === undefined) undeclared.push(label);
+ if (!disagree && divergenceFor(label) !== undefined) {
+ undeclared.push(`${label} (declared, but the products agree)`);
+ }
+ }
+
+ expect(undeclared).toEqual([]);
+ });
+
+ it('accounts for every string this component owns', () => {
+ const unaccounted = (
+ Object.keys(CENTRALIZED_GUARDRAILS_EN_LABELS) as Array
+ ).filter(
+ (label) =>
+ HOST_COPY[label] === undefined &&
+ !SINGLE_SOURCE.includes(label) &&
+ !OWN_ADDITIONS.includes(label)
+ );
+
+ expect(unaccounted).toEqual([]);
+ });
+
+ it('adopts a single-source label verbatim from the product that has it', () => {
+ for (const label of SINGLE_SOURCE) {
+ const hosts = HOST_COPY[label];
+ const only = hosts?.agents ?? hosts?.flow;
+ expect(CENTRALIZED_GUARDRAILS_EN_LABELS[label]).toBe(only);
+ }
+ });
+});
diff --git a/packages/apollo-react/src/canvas/components/Guardrails/centralized-types.ts b/packages/apollo-react/src/canvas/components/Guardrails/centralized-types.ts
new file mode 100644
index 000000000..3894933a9
--- /dev/null
+++ b/packages/apollo-react/src/canvas/components/Guardrails/centralized-types.ts
@@ -0,0 +1,66 @@
+import type { GuardrailScope } from './builder-types';
+
+// Structural mirrors of both products' policy schemas, so a host passes its own zod-inferred
+// types unmapped.
+
+/** Agents' `ActionType` string enum assigns to this union as well as Flow's `z.enum`. */
+export type CentralizedGuardrailActionType = 'block' | 'escalate' | 'filter' | 'log';
+
+/** A BYO guardrail's configured parameter; `parameterType` and `value` are unvalidated. */
+export interface CentralizedGuardrailParameter {
+ id: string;
+ parameterType?: string | null;
+ value?: unknown;
+}
+
+/** One guardrail enforced by the organization's AI Trust Layer policy. */
+export interface CentralizedGuardrail {
+ validator: string;
+ /** Admin-given display name. Only set for BYO guardrails; it is also their identity. */
+ name?: string | null;
+ /** Tells a BYO entry apart from a built-in validator sharing the same `validator` id. */
+ isByo?: boolean | null;
+ /** `Pre` / `Post` / `Both`, typed open as in both products' schemas. */
+ executionStage: string;
+ appliesToAutonomousAgents: boolean;
+ appliesToConversationalAgents: boolean;
+ scopes: GuardrailScope[];
+ action: CentralizedGuardrailActionType;
+ /** Built-in validators only: the detected entities, and their per-entity thresholds. */
+ entities?: string[] | null;
+ entityThresholds?: Record | null;
+ /** BYO only: connector-specific configuration, passed through from the policy. */
+ parameters?: CentralizedGuardrailParameter[] | null;
+}
+
+/** The parameter definition fields this component reads, so any host's shape satisfies it. */
+export interface CentralizedGuardrailParameterDefinition {
+ id: string;
+ type: string;
+ /** Pre-resolved display label; falls back to the parameter id. */
+ label?: string;
+ /** Keyed by the raw wire value. */
+ optionLabels?: Record;
+ /** For `map-enum`: id of the sibling `enum-list` whose selection provides the keys. */
+ keySource?: string;
+}
+
+/** The definition fields this component reads; the same array the palette and list take. */
+export interface CentralizedGuardrailDefinition {
+ validator: string;
+ status?: string;
+ description?: string;
+ byoValidatorName?: string;
+ byoConnectorName?: string;
+ parameters?: CentralizedGuardrailParameterDefinition[];
+}
+
+/** One resolved row of a centralized guardrail's configuration, ready to render. */
+export type CentralizedGuardrailParameterRow =
+ | { id: string; label: string; kind: 'value'; value: string }
+ | {
+ id: string;
+ label: string;
+ kind: 'thresholds';
+ thresholds: Array<{ key: string; label: string; value: number | undefined }>;
+ };
diff --git a/packages/apollo-react/src/canvas/components/Guardrails/components/centralized-detail-field.tsx b/packages/apollo-react/src/canvas/components/Guardrails/components/centralized-detail-field.tsx
new file mode 100644
index 000000000..027fb8e5a
--- /dev/null
+++ b/packages/apollo-react/src/canvas/components/Guardrails/components/centralized-detail-field.tsx
@@ -0,0 +1,22 @@
+import { cn } from '@uipath/apollo-wind';
+import type * as React from 'react';
+
+export interface CentralizedDetailFieldProps {
+ label: string;
+ className?: string;
+ children: React.ReactNode;
+}
+
+/** One labelled field of a read-only centralized guardrail, as a `