Skip to content

feat(apollo-react): guardrail list section [AL-575] - #1140

Draft
andreizdrali-uipath wants to merge 5 commits into
mainfrom
feat/apollo-react-guardrail-list
Draft

feat(apollo-react): guardrail list section [AL-575]#1140
andreizdrali-uipath wants to merge 5 commits into
mainfrom
feat/apollo-react-guardrail-list

Conversation

@andreizdrali-uipath

@andreizdrali-uipath andreizdrali-uipath commented Sep 9, 2026

Copy link
Copy Markdown

Implements AL-575 (epic AL-526): the guardrails list
section, built fresh in the apollo-react guardrails family.

Draft, stacked on #1139 (AL-574), which sits on #1138. Opened against main so pr-checks and the dev-packages preview run (the #1138 pattern); the commits below the last one are #1107, #1138 and #1139. Review only the last commit, feat(apollo-react): guardrail list section [AL-575]. Rebased onto main once the PRs beneath it merge.

The list takes no dependency on the definitions layer (see the definitions bullet below); it is stacked there so that one preview version carries both, which is what a host needs to try the list against real, parsed definitions.

What it replaces

GuardrailsListingSection.tsx in Agents (968 LOC), and GuardrailsEditor.tsx (734) +
SortableGuardrailItem.tsx (126) in flow-workbench. Prop shape follows Flow's, per the design doc
recipe. Both hosts' components are orchestrators (palette, builder, remove dialog, save-as-new,
telemetry) wrapped around the rendering; this replaces the rendering, the reorder and the section
chrome, and the orchestration stays where it is.

The contract

The host filters, the component renders. GuardrailList never drops a row.

  • No feature flag crosses the boundary. Agents' six enableGuardrail* booleans and Flow's
    canvas.guardrails entitlement plus scope filter stay in the adapters; hosts pass the survivors
    as guardrails and definitions.
  • 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 / onEdit.
  • No telemetry in the package. Confluence §7.5 suggests an onTrack prop; I did not add one.
    feat(apollo-react): guardrails component family under canvas #1138 has no telemetry seam, the two products' event taxonomies do not overlap
    (guardrails.reordered vs GUARDRAILS_REORDER_BUTTON_CLICKED), and wrapping the intents is what
    both adapters do anyway. Easy to add later if you disagree.
  • onReorder reports the reordered 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 it back
    without the package knowing a fuller list exists.
  • definitions is read for three things only: the provider line, the two BYO notices and the
    status chip. Matching is the rule both products already ship, converged here: a BYO row matches on
    validator name alone, so an admin rebinding a configuration to another connection still
    resolves. The list's definition type is the minimum it reads
    (validator, status, byoValidatorName?, byoConnectorName?), which GuardrailDefinition and
    AL-574's EnrichedGuardrailDefinition both satisfy. So useGuardrailDefinitions' output feeds
    the list unchanged, and the list still compiles and tests green without the definitions layer
    present: the two stacks are independent and can merge in either order.

Divergences, resolved as one shape plus a seam

Concern Agents Flow Here
Row identity name id id ?? name, override with getItemId
Read-only isReadOnly disabled disabled
Reorder while read-only off on follows disabled; Flow passes reorderDisabled={false}
Row actions overflow menu inline buttons inline buttons; renderItemActions swaps in the menu and receives defaultActions
Clickable row yes no no, rowActivatesEdit opts in
Scope/action copy localized raw raw, formatScopes / formatAction localize host-side
Provider + description in a tooltip inline lines inline lines, renderRowTooltip adds the tooltip

Behaviour parity first

Every addition is opt-in, so an adopting host gets a swap that renders what it renders today and
turns the new UI on deliberately.

Prop Default Why
statusChips false Neither product chips status or administration today. The two BYO notices are not gated: both already show them, 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 Flow's read-only list stays reorderable and Agents' does not, so one flag could not express both.
unstyled false Agents renders inside a SectionAccordion, where the card border is extra chrome.
hideHeader false Agents owns the section title and its add affordance.
footer none Agents' add affordance sits below the rows and swaps for an entitlement line, which the header-only addSlot cannot express.
emptyState the default line Agents shows nothing when empty. Checked with === undefined, so an explicit null is honoured.
rowActivatesEdit false Agents opens the editor by clicking the row body.
renderRowTooltip none Agents hovers a combined description, provider and scopes tooltip.
formatScopes / formatAction raw values Presence of the prop decides, never its value, so () => null hides the line.

So each host passes:

  • Flow: previewChip, reorderDisabled={false}. Everything else already matches.
  • Agents: previewChip, unstyled, hideHeader, emptyState={null}, footer,
    rowActivatesEdit, formatAction={() => null}, formatScopes, renderItemActions,
    renderRowTooltip.

One deliberate exception I did not preserve: Agents' drag handle is an aria-hidden icon with the
listeners on it, so it is unreachable by keyboard. Mine is a real button carrying
setActivatorNodeRef, so focus also returns to it after a drop. That is a fix, not a regression.

Reuse, and one deliberate non-reuse

GuardrailStatusBanner and MixedScopesBanner are promoted from internal pieces to exports, as the
ticket asks. Both additions are additive; nothing existing changed shape.

The status chip is not GuardrailChip. That wraps a Radix Toggle, so it is a focusable button
carrying pressed state; these chips are read-only labels and would otherwise put fake buttons in the
tab order and misreport to screen readers. GuardrailStatusChip is Badge-based and shares the
chip family's pill geometry through a new GUARDRAIL_CHIP_GEOMETRY constant (feeding
guardrailChipVariants() into a Badge drags the Toggle appearance's bg-* and data-[state]
classes along, and they win over the badge variant). Reviewers who read the ticket line as "reuse
the chip UX" get what they expected; reviewers who meant the component literally, please push back.

Chips and notices

Available plus local administration renders nothing, so the common row stays quiet.
FeatureDisabled and Unauthorised chip as warnings, Disabled and the synthetic Unavailable
(a BYO row whose definition stopped resolving) as errors. The mapping is the pure, exported
getGuardrailListChips, so AL-578's centralized section can reuse it.

The local-vs-governance axis is called administration, never "origin": Agents'
GuardrailOriginChip and Flow's CentralizedGuardrailOriginBadge both already mean BYO versus
UiPath-managed, which is a different axis and travels on byoValidatorName. Governance rows still
belong to each product's separate centralized section (AL-578); this ships the chip and the seam so
either layout stays possible.

i18n

17 guardrails.list.* ids, resolved through useGuardrailListLabels on useSafeLingui. One
builder function holds every _({ id, message }) call, with the English record and the flat
id-to-message record derived from it, so the three cannot drift (the definitions-copy.ts idiom).
12 of the 17 are harvested from Flow's canvas catalog into all 12 translated locales, at 100%
coverage, so this adds no translation work: the harvest asserted our English equals Flow's for every
mapped id before writing, and rewrote i18next {{name}} to ICU {name}. The 5 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); hosts can supply them through
labels today. Flagged in the README.

remove-item takes Flow's remove-dialog translation ("Remove guardrail") rather than its row
button ("Delete guardrail"), so the row action, the onRemove intent and AL-577's dialog all say
the same thing.

i18n.test.ts asserts catalog-to-source parity both ways, which is what lingui extract would do
if src/canvas used macros.

Tests

84 new tests (the Guardrails suite is 288 passing), jest-axe on all three rendering suites.

The keyboard reorder runs the real dnd-kit sensor rather than a mock: happy-dom does no layout,
so every rect is zero and collision detection cannot tell two rows apart. The suite lays the rows on
a synthetic vertical grid and awaits a macrotask between keystrokes, because dnd-kit measures inside
requestAnimationFrame and the canvas test setup implements that as setTimeout(cb, 0), which
act() does not flush. Both halves are in __fixtures__/dnd-geometry.ts with the reason written
down.

Two things worth a look outside the component

  • @dnd-kit/modifiers@^9.0.0 added to apollo-react for restrictToVerticalAxis /
    restrictToParentElement, which both hosts already use. apollo-wind pins the same version, so
    there is no new install weight; the lockfile diff is the 3-line importer entry.
  • !./src/**/__fixtures__/** added to rslib.config.ts's entry list. The geometry helper
    imports @testing-library/react, and without the exclusion it was emitted into dist.

Local gate

tsc, biome and rslib build clean for @uipath/apollo-react; the full package suite is
2922 passing with 9 failures, all pre-existing and unrelated: 8 are localStorage being undefined
under happy-dom (canvas/utils/Storage.test.ts, canvas/hooks/useStorageState.test.ts, they
reproduce on the base branch), and one is a 5s test timeout in ap-model-picker that only appears
under full-suite load and passes in isolation.

build:icons was deliberately not run: on Windows it rewrites OS-native path separators into every
generated icon header, which is 1322 unrelated files. Fix queued separately.

Open review questions

  1. The mixed-scopes banner is a slot, not a typed prop. AL-575 names it, and it is promoted to
    an export, but the list composes banners through one statusBanner: ReactNode (a definitions
    load failure is the real case). Its second line is a "Save as new" hint, which is builder copy,
    and both products render it inside a builder rather than over a list. Say the word and it becomes
    a typed mixedScopes={{ scopes, tools }} prop again.
  2. No onTrack prop, against Confluence §7.5. Reasoning above.
  3. Agents' scoped-removal bug: GuardrailsListingSection swaps affectedScopes and
    affectedToolNames when building the remove-dialog payload. That code stays host-side either
    way, so this PR neither fixes nor preserves it, but the adapter has to decide.

Next

Agents and flow-workbench adoption behind EnableApolloGuardrailBuilder /
canvas.guardrails.apollo-components, with the existing components kept verbatim as *Legacy
twins. Those PRs need a published version (or the #1138-style preview pin), so they follow the
release.

apetraru-uipath and others added 4 commits September 9, 2026 11:58
… field

Generic forms-engine enablers extracted from the guardrails work (apollo-ui#1107
review): controlled values/onValuesChange/errors/disableValidation/container
props on MetadataForm (MetadataFormProps now exported), a string-list field type
with tooltip/textarea/multiselect metadata additions, label association and
custom-component ref handling in the field renderer, and a useWatch re-export so
cross-package custom fields share the RHF context.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ling

InfoTooltip moves out of the guardrails prototype into components/ui with its
a11y test; select and textarea get aria-invalid error styling; the root barrel
exposes the new forms/ui surface. The guardrails domain family itself moves to
apollo-react (canvas) per the #1107 review decision.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Moves the guardrails family out of apollo-wind's custom/ prototype shelf into
apollo-react per the #1107 review decision: canvas-adjacent (MUI-free, built on
wind primitives and the forms/ MetadataForm engine), exported from the canvas
components barrel plus a narrow ./canvas/guardrails subpath.

Strings move to lingui: useSafeLingui labels-hooks with explicit guardrails.*
ids replace the wind-local catalogs and loader; the 60 keys ship translated in
the shared canvas catalog for 13 locales (ru falls back to English per key).
Localized templates that cross into plain-string APIs are ICU messages
formatted with sentinel values, preserving the {{token}} convention.

The family's Tailwind classes ride the existing tailwind.canvas.css scan; adds
class-variance-authority (dep) and jest-axe (dev, matcher registered in the
shared test setup). Also silences Radix's aria-describedby warning on the
description-less builder dialog.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ilDefinitions

Adds the definitions layer the guardrails family was missing: the seam
between the `/api/execution/guardrails/definitions` payload and the
`GuardrailDefinition`s `GuardrailBuilder` renders. Flow and Agents each
carry their own copy of this today, and the two have drifted.

- `definitions-wire.ts` mirrors the payload as hand-written types, reusing
  `GuardrailScope` and `GuardrailDefinitionStatus` from `builder-types` so
  the wire and display layers cannot diverge. It admits both products'
  nullability variants.
- `definitions-parse.ts` validates unknown input and never throws: a
  non-array payload sets `inputError`, a bad definition is dropped whole
  and reported in `invalid`, unknown keys are stripped. zod is private to
  this module, pinned to the public mirror by a bidirectional assignability
  check on the hot path, a runtime key-set assertion and a source-level
  guard, so the folder's emitted declarations carry no schema types.
- `definitions-copy.ts` holds the canonical copy for the six built-in
  validators as 63 lingui messages in the shared canvas catalog, keyed by
  raw wire values. Translations harvested from both products, 62 of 63 in
  each of the 12 locales.
- `definitions-enrich.ts` resolves that copy onto the wire shape. Pure and
  React-free; `EnrichedGuardrailDefinition extends GuardrailDefinition`, so
  its output feeds the builder with no mapping.
- `useGuardrailDefinitions` composes the three over `useState` + `fetch` +
  `AbortController`, following `useDiscoveryModels`. `options.definitions`
  skips the request entirely, which is how each product keeps its own
  transport. Unlike `useDiscoveryModels` the context is compared by content,
  not identity: keying the effect off identity made an inline context object
  refetch on every render without terminating.

Where the products' English differed, all 17 choices are declared with a
reason in `definitions-parity.test.ts` and asserted against both products'
transcribed copy, so the shared table cannot quietly drift from the tables
it replaces.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

Dependency License Review

  • 1937 package(s) scanned
  • ✅ No license issues found
  • ⚠️ 2 package(s) excluded (see details below)
License distribution
License Packages
MIT 1708
ISC 88
Apache-2.0 55
BSD-3-Clause 27
BSD-2-Clause 23
BlueOak-1.0.0 8
MPL-2.0 4
MIT-0 3
CC0-1.0 3
MIT OR Apache-2.0 2
(MIT OR Apache-2.0) 2
Unlicense 2
LGPL-3.0-or-later 1
Python-2.0 1
CC-BY-4.0 1
(MPL-2.0 OR Apache-2.0) 1
Unknown 1
Artistic-2.0 1
(WTFPL OR MIT) 1
(BSD-2-Clause OR MIT OR Apache-2.0) 1
CC-BY-3.0 1
0BSD 1
(MIT OR CC0-1.0) 1
MIT AND ISC 1
Excluded packages
Package Version License Reason
@img/sharp-libvips-linux-x64 1.3.2 LGPL-3.0-or-later LGPL pre-built binary, not linked
khroma 2.1.0 Unknown MIT per GitHub repo, missing license field in package.json

Adds `GuardrailList` to the canvas guardrails family: the applied-guardrails
section both products own today (Agents `GuardrailsListingSection`, Flow
`GuardrailsEditor` + `SortableGuardrailItem`), extracted once.

The host filters and the list renders. Rows and definitions arrive already
filtered by feature flags, entitlements and scope, every callback is an intent
(dialogs, persistence and telemetry stay with the product), and `onReorder`
reports the visible array plus `{ from, to, id }` so a host rendering a filtered
view can splice the result back. Every addition beyond what both products show
today is opt-in, so adopting the list behind a flag renders what the host
renders now.

Reorder is real dnd-kit with pointer and keyboard sensors; the drag handle is a
real button, where Agents' handle today is an `aria-hidden` icon carrying the
listeners. With reorder off, no drag machinery mounts at all.

`GuardrailStatusChip` is `Badge`-based rather than the Radix-Toggle
`GuardrailChip`: these are read-only labels. `GuardrailStatusBanner` and
`MixedScopesBanner` are promoted to exports additively.

Chrome strings resolve through `useGuardrailListLabels` on `useSafeLingui` with
`guardrails.list.*` ids; 12 of the 17 are harvested from Flow's canvas catalog
into all 12 translated locales, the other 5 fall back to English per key.

Also here: `@dnd-kit/modifiers` added to apollo-react (wind already pinned it,
so no new install weight), the three label resolvers share one merge helper, and
`__fixtures__` no longer reaches the published bundle.
@andreizdrali-uipath
andreizdrali-uipath force-pushed the feat/apollo-react-guardrail-list branch from eed9161 to 4ac1aa1 Compare September 9, 2026 13:35
@andreizdrali-uipath
andreizdrali-uipath changed the base branch from feat/apollo-react-guardrails-family to feat/apollo-react-guardrail-definitions-layer September 9, 2026 13:36
@andreizdrali-uipath andreizdrali-uipath added the dev-packages Adds dev package publishing on pushes to this PR label Sep 9, 2026
@andreizdrali-uipath
andreizdrali-uipath changed the base branch from feat/apollo-react-guardrail-definitions-layer to main September 9, 2026 13:58
@andreizdrali-uipath andreizdrali-uipath added dev-packages Adds dev package publishing on pushes to this PR and removed dev-packages Adds dev package publishing on pushes to this PR labels Sep 9, 2026
@github-actions

github-actions Bot commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

📦 Dev Packages

Package Status Updated (PT)
@uipath/apollo-react@6.42.0-pr1140.ef13150 🟢 Published Sep 09, 2026, 07:07:31 AM
@uipath/apollo-wind@2.47.2-pr1140.ef13150 🟢 Published Sep 09, 2026, 07:05:55 AM

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dev-packages Adds dev package publishing on pushes to this PR pkg:apollo-react

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants