Skip to content

feat(apollo-wind): guardrail list section with reorder and status chips [AL-575] - #1133

Closed
andreizdrali-uipath wants to merge 1 commit into
feat/apollo-wind-guardrail-definitions-layerfrom
feat/apollo-wind-guardrail-list
Closed

feat(apollo-wind): guardrail list section with reorder and status chips [AL-575]#1133
andreizdrali-uipath wants to merge 1 commit into
feat/apollo-wind-guardrail-definitions-layerfrom
feat/apollo-wind-guardrail-list

Conversation

@andreizdrali-uipath

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

Copy link
Copy Markdown

Implements AL-575 (epic AL-526): the guardrails
list section, extracted once into @uipath/apollo-wind.

Draft, and stacked. Base is #1132, which is
itself stacked on #1107. Both are unmerged. A PR
based off those branches runs almost no CI, since pr-checks, preview-deploy (including the
Storybook visual diff), dev-publish, pr-size, security-scan and codeql all filter their
pull_request trigger to main and support/**. This stays draft until #1107 merges and the
chain rebases onto main.

What it replaces

GuardrailsListingSection.tsx in Agents (968 LOC), and GuardrailsEditor.tsx (743) +
SortableGuardrailItem.tsx (126) + MixedScopesBanner.tsx (38) in flow-workbench. Prop shape
follows Flow's, per the extraction recipe.

The contract

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

  • No feature flag crosses the boundary. Agents' enableOutOfTheBoxGuardrails,
    enableGuardrailPII, enableGuardrailPromptInjection, enableGuardrailHarmfulContent,
    enableGuardrailIntellectualProperty and enableGuardrailUserPromptAttacks stay in the
    Agents adapter, as does Flow's canvas.guardrails entitlement and its allowed-scope filter.
    Hosts pass the survivors as guardrails and definitions.
  • Callbacks are intents. onRemove fires the request; the confirmation dialog 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. Hosts wrap the callbacks.
  • onReorder reports the reordered visible array plus the move, so a host rendering a
    filtered view (Flow's tool view over an agent-level list) can splice back without the package
    knowing the full list exists.

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
Row actions overflow menu inline buttons inline buttons; renderItemActions swaps in the menu
Clickable row yes no no, rowActivatesEdit opts in
Scope/action copy localized raw raw, formatScopes / formatAction localize host-side
Provider + description in a MUI tooltip inline lines inline lines

getItemOrigin tags governance-managed rows: both products fetch them from a different
endpoint, so nothing on the record identifies them.

Behaviour parity first

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

Prop Default Why
statusChips false Neither product renders status or origin chips today. The two BYO row notices are not gated, because both already show those.
previewChip false Product lifecycle, not a package concern. Both hosts pass it today and drop it at GA without a release.
reorderDisabled disabled Flow's read-only list stays reorderable and Agents' does not, so one flag could not express both. Flow passes false.
unstyled false Agents renders inside a SectionAccordion, where the card border is extra chrome.
footer none Agents' add affordance sits below the rows and swaps for an entitlement line, which addSlot (header-only) cannot express.
emptyState={null} shows the default line Agents shows nothing when empty. An undefined check rather than ??, so an explicit null is honoured.
renderRowTooltip none Agents hovers a combined description, provider and scopes tooltip on top of the inline lines.

So each host passes:

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

One deliberate exception I did not restore: Agents' drag handle is an aria-hidden icon with
the listeners on it, so it is unreachable by keyboard. Mine is a real button. That is a bug fix,
not a regression.

Reuse, and one deliberate non-reuse

GuardrailStatusBanner and MixedScopesBanner are promoted from internal pieces to exports and
integrated, as the ticket asks. Both additions are additive; nothing existing changed shape, so
there is no breaking-change footer.

The status and origin chips are 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 reuses guardrailChipVariants' geometry tokens, so the two chip families
still read as one system. Reviewers who read the ticket line as "reuse the chip UX" should get
what they expected; reviewers who meant the component literally, please push back.

Chips and notices

Available + origin local renders nothing, so the common row stays quiet. FeatureDisabled
and Unauthorised chip as warnings, Disabled and Unavailable as errors. The two inline
notices (BYO configuration disabled, BYO configuration gone) are the ones both products already
render, and they keep the definitions.length > 0 guard: without it every BYO row flashes "no
longer available" while the catalog is still in flight.

i18n

Ten of the list's strings are harvested from Flow's canvas catalog into all 13 non-English
catalogs, so this adds no translation work, matching how #1107 seeded the family. The four
status/origin chip labels and the row aria-label have no Flow equivalent and are English-only
for now; they fall back per key and hosts can supply them through labels today. Flagged in the
README.

The three label resolvers now share one merge helper instead of three copies of the same loop.

Tests

84 new tests. jest-axe on all three rendering suites. The keyboard reorder is exercised through
the real dnd-kit sensor rather than mocked: jsdom reports a zero rect for everything, so the test
lays the rows out on a synthetic vertical grid, which is the minimum geometry collision detection
needs to tell two rows apart.

Coverage floors ratchet 60/63/54/58 to 69/72/63/67 (measured 69.44 / 72.62 / 64.25 / 67.31,
up from 69.03 / 72.11 / 63.49 / 66.87 on the base branch). The five new modules are at 100%
lines and functions, 91 to 100% branches.

Local gate

typecheck, lint, test and build pass for @uipath/apollo-wind. Two notes for anyone
reproducing on Windows:

  • packages/apollo-core's build:fonts and packages/apollo-ui-icons' build both fail here for
    pre-existing, unrelated reasons (a single-quoted postcss glob, and a path-join bug in the icon
    rename step), which blocks turbo's dependency step. Neither package is touched by this branch.
  • forms/form-plugins.test.tsx (26) and ui/calendar.test.tsx (1) fail identically on the base
    branch, verified by stashing. localStorage is undefined in that environment.

Open question for review

Both products render governance-managed guardrails in a separate CentralizedGuardrailsSection
rather than as chip-tagged rows in the main list. The ticket asks for an origin chip, which
implies one mixed list. Two things make that more than a chip:

  1. "Origin chip" is already taken, and means something else. Agents' GuardrailOriginChip
    and Flow's CentralizedGuardrailOriginBadge both render BYO vs UiPath managed, not
    local vs governance. Flow's comment says why: "A connector may expose a validator id a
    built-in also uses, so origin is otherwise invisible."
    So getItemOrigin here redefines a
    word the products already use. Renaming it (getItemAdministration?) is cheap now and
    breaking later.
  2. Governance rows are a different type. CentralizedGuardrail has no id, a nullish
    name, scopes at the top level rather than under selector, and an ActionType enum
    instead of { $actionType }, plus executionStage and the appliesTo* flags. They are also
    read-only, unorderable, and click through to a details panel rather than the builder. A
    merged list would need per-row capabilities and host-side mapping, which is a ticket rather
    than a chip.

This PR ships the chip (behind statusChips) and the seam so either layout stays possible, and
leaves the separate sections alone. Worth deciding here whether to rename the prop before it is
public.

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 real published version, not a preview pin, so they follow the release.

@github-actions

github-actions Bot commented Sep 8, 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

@andreizdrali-uipath andreizdrali-uipath changed the title feat(apollo-wind): guardrail list section with reorder and status chips feat(apollo-wind): guardrail list section with reorder and status chips [AL-575] Sep 9, 2026
Extracts the guardrails list section that exists twice today: Agents'
GuardrailsListingSection (968 LOC) and Flow's GuardrailsEditor + SortableGuardrailItem
+ MixedScopesBanner. Adopts Flow's prop shape, as the extraction recipe calls for.

The contract is that the host filters and the component renders. None of Agents' six
enableGuardrail* booleans crosses the boundary, and neither does Flow's canvas.guardrails
entitlement: hosts pass the surviving rows and definitions. Telemetry, the remove
confirmation dialog and the add/edit flows also stay host-side, so the callbacks are
intents rather than mutations.

Where the two products diverge, one shape plus a seam:

- row identity is `id` falling back to `name`, overridable with getItemId
- row actions default to Flow's inline buttons; renderItemActions swaps in Agents' menu
- rowActivatesEdit brings back Agents' clickable row, off by default
- formatScopes / formatAction keep localized scope and action copy host-side
- getItemOrigin tags governance-managed rows, which no record identifies on its own

onReorder reports the reordered visible array plus the move, so a host rendering a
filtered view can splice the change back into its full list without the package
knowing about the full list.

Every addition is opt-in, so an adopting host gets a swap that renders exactly what it
renders today. statusChips and previewChip are off by default; reorderDisabled splits the
drag handle from the action buttons, because Flow's read-only list stays reorderable and
Agents' does not; unstyled, footer, emptyState={null} and renderRowTooltip let Agents keep
its accordion chrome, its add button below the rows and its combined hover.

Status and origin chips are Badge-based rather than GuardrailChip: that wraps a Radix
Toggle, so it is a focusable button with pressed state, and these are read-only labels
that would otherwise land in the tab order. The geometry matches so the two chip
families still read as one system. GuardrailStatusBanner and MixedScopesBanner are
promoted from internal pieces to exports, additively.

Ten of the list's strings are harvested from Flow's canvas catalog into all 13
non-English catalogs, so the extraction introduces no new translation work. The three
label resolvers now share one merge helper instead of three copies of the same loop.

Coverage floors ratchet 60/63/54/58 to 69/72/63/67.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant