#2074

ENG-662: Let the AI apply or create guardrails and masks per risk level

Draftracerxdlwants to merge intofrom

IMPORTANT

Every PR MUST have exactly one of these labels, the merge is blocked otherwise:

  • major / minor / patch publishes a new release on merge with the corresponding semver bump.
  • skip-release merges without publishing a release (use for docs, CI changes, refactors, etc.).

📝 Description

Per risk level, the AI agent can apply library guardrails and masks, or create a one-time rule, for one statement only. Guardrails deny only; a mask covers that statement's response only, and any path where the sidecar cannot enforce it is refused. Stacked on #2065 (ENG-661).

📣 User-facing impact

A risk level can let the AI agent pick which guardrails and data masking rules protect each statement, or write a one-time rule, with each choice recorded.

🔗 Related Issue

ENG-662

🚀 Type of Change

  • 🐛 Bug fix (non-breaking change which fixes an issue)
  • ✨ New feature (non-breaking change which adds functionality)
  • 💥 Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • 📚 Documentation update
  • 🎨 Style/UI update
  • ♻️ Code refactor
  • ⚡ Performance improvement
  • ✅ Test update
  • 🔧 Build configuration change
  • 🧹 Chore

📋 Changes Made

  • sidecar: level guardrails/mask (mode, rules), top-level rule_library, capability analyzer_ai_rules, Enterprise license gate. A rule turn with apply_*/create_* tools (joined into the hand-off turn), strict decoding of created rules, the rule library as escaped data in the prompt. Per-statement guardrails; a per-exchange mask plan in the gate that refuses every path a codec cannot guarantee. README docs.
  • gateway: saves validate the new fields (license, library names, not on block/defer); composition serves the selected library rules as rule_library with collision-free names; session ai_decisions carry ai_rules, bounded.
  • webapp_v2: the "AI policy settings" card (Guardrails / Data Masking, rules the AI can use, one-time rules), per the Figma design.
  • webapp: the session "AI decisions" section shows each applied and created rule with its reason.

🧪 Testing

Result: per risk level, an admin can let the AI agent apply library guardrails and masks, or create a one-time rule, for each statement. An applied guardrail denies the statement with its message. A created mask masks only that statement's result. When the sidecar cannot enforce a mask on a path, it refuses the statement. Tested on a2d3854b against its base eng-661 b33e3400.

1. Does the agent's rule apply, to that statement only, and fail closed?

The same stack on both sides: licensed gateway, managed sidecar, Postgres and a stub LLM (fake-llm sets its next answer). What to look at:

  • Step 2: the eng-661 plane refuses the config (HTTP 422, unknown field "guardrails"). No lane starts, so Steps 3–8 cannot connect. eng-662 saves it and starts both lanes.
  • Step 3: the applied guardrail denies the DELETE with deny-delete's message.
  • Step 4: one connection, the same SELECT twice: the first result is ****, the second is ana@example.test.
  • Steps 5–6: lane payments-hold files a review for a DELETE with no rule (reviews filed: 1). With deny-delete applied, the DELETE is denied and the count stays 1.
  • Step 7: a masked statement sent with the extended protocol (\bind) is refused, not run unmasked.
  • Last frame: the audit has each statement with the rule the agent applied or created, and its reason.

2. Does the session page show each rule and its reason?

Sessions from image 1, with experimental.sidecar_session_events on. Left: eng-661 with the rule fields removed: the same DELETE runs, and AI decisions shows only the risk and the action. Middle: eng-662 shows "Guardrail · Applied: deny-delete · Matched" with the reason, and the DELETE is Denied. Right: the created mask with its definition, then "None applied" for the next SELECT on that connection.

3. Does the AI policy settings card match the design?

Same seed (three library guardrails). Low risk → Hold for approval. eng-661 shows the Approver card only. eng-662 adds the AI policy settings card above it, as in Figma: Guardrails on, 1 of 3 rules, "Let the AI create one-time rules", Data Masking off, "Next", Approver collapsed. The Figma list repeats one placeholder rule; the form lists the real library.

4. Does -validate warn and refuse per the ticket's table, and is an old sidecar refused?

One config per table row. eng-661 refuses all eight. eng-662 loads each warning case with one note:, refuses an unknown mode, and refuses rule options with no license. DURING DEPLOY: a real eng-661 sidecar build, managed by the eng-662 plane, gets "cannot build this sidecar's config … does not report "analyzer_ai_rules"" and exits 1.

What it solves: Admins can let the AI agent choose, per statement, which guardrails and masks protect a risk level, or write a one-time rule, with each choice and reason in the audit and the session. Before, a level had one fixed action.

What QA caught on the way

  • Three review rounds on the sidecar and the plane, all fixed:
    • 6 P1 + 1 P2: a masked statement behind an outstanding one, a masked Postgres Parse, HTTP close-delimited and compressed bodies, lax decoding of created rules, served rule-name collisions; P2: rule text could open a prompt line. TestAMaskedStatementBehindAnOutstandingOneIsRefused, TestAMaskedPostgresParseIsRefused, TestAMaskedHTTPResponseTheCodecMightNotMaskIsRefused, TestACreatedRuleWithAForbiddenFieldIsDroppedWhole, TestFoldRuleLibraryNamesNeverCollide, TestRuleTextCannotInjectPromptLines.
    • 4 P1: a later request could replace a live HTTP mask, partial or LF-only heads, a flush with a stale plan, mssql masks not tied to one batch (now refused). TestACloseDelimitedMaskedResponseIsRefused, TestAMaskedHTTPHeadIsReadWholeOrRefused, TestAnUnmaskedHeldRowIsFlushedUnchanged, TestAnAIMaskOnMSSQLIsDenied.
    • 3 P1: opaque HTTP media types, chunked bodies split at the codec's 64 KiB unit, a raw tail at EOF. TestAMaskedResponseTheCodecPassesThroughIsRefused, TestATruncatedMaskedFrameIsDroppedAtTheEnd.
  • UI review: P1, after logout the form showed the previous organization's rule names (the library now resets on logout and only the newest fetch commits). P2, a selection could be saved as "every rule" when it was not exactly the loaded library. No automated test: webapp_v2 has no unit-test runner.
  • Live QA on dc4095c1 (licensed plane, managed sidecar): 17 of 18 passed. F2 (P1): the session page did not show the rules and reasons. Fixed in a2d3854b; QA's recheck on a2d3854b passed. Image 2 shows it.

Acceptance criteria

  • Per risk level, set the features, the rule source and the allowed rules (UI, licensed save and reload)
  • The allowed rules and definitions reach the sidecar (rule_library, live)
  • The session details show every AI-applied and AI-created rule with its reason (live, a2d3854b)
  • AI-created rules are not saved in the rule library (live)
  • The UI matches the design (image 3)
  • Only the tools the level allows; create tools only in existing_and_new; rule enum holds only allowed names (live)
  • An AI guardrail that matches denies with its message (live)
  • An AI mask rewrites the response of this statement only (live, image 1)
  • Invalid AI-created rules are dropped and recorded (live, TestACreatedRuleWithAForbiddenFieldIsDroppedWhole)
  • With require_approval, an AI guardrail deny ends the statement before a review is filed; an AI mask applies before the hold (live, image 1)
  • With ai-handoff, rules apply only when the agent does not block (live)
  • -validate warns and refuses per the ticket table (image 4)
  • Docs: sidecar/README.md

Re-run

(cd sidecar && go test ./...) && (cd sidecar/analyzer/vertex && CGO_ENABLED=0 go test ./...)
(cd gateway && go test -timeout 50m ./services/... ./api/sidecar/ ./api/ai/... ./models/ ./api/session/)
(cd webapp_v2 && npm run lint && npm run build)

Test Configuration:

  • Browser(s): Chromium
  • OS: macOS (arm64)

Tests performed:

  • Unit tests pass
  • Integration tests pass (CI pending)
  • Manual testing completed (licensed plane, managed sidecar, Postgres, stub LLM)

📸 Screenshots (if applicable)

See Testing.

✅ Checklist

  • My code follows the style guidelines of this project
  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • My changes generate no new warnings
  • New and existing unit tests pass locally with my changes
  • I have checked my code and corrected any misspellings

📄 Additional Notes

Risk: high (model output decides what gets masked).

  • Stacked on #2065 → #2053: merge those first.
  • Sidecar↔plane: the new fields need capability analyzer_ai_rules; older builds are refused, never downgraded. No gateway↔agent change. No migration.
  • Enterprise only: the sidecar and the plane refuse these fields without a license that grants guardrails or data masking.

Open items

  • Per-protocol mask scope. Postgres masks one statement (image 1). mysql, oracle and clickhouse: after an AI-masked statement, the connection denies every statement until reconnect. mssql and mongodb (also ssh, ica, grpc without descriptors) refuse AI masks. HTTP masks length-delimited JSON and text, and refuses chunked, compressed, opaque and close-delimited responses.
  • Each limit needs libhoop: every rewriter must report a value it forwards unmasked; mssql must report the end of each response, attention, and unsupported column types; http must report a body sent unmasked and the real end of a close-delimited body; postgres must tie Bind/Execute to their Parse.
  • The session "AI decisions" section needs experimental.sidecar_session_events. With it off, the audit still has the rules.
  • Listener (config) mask rules keep their limits; this ticket does not change them: http non-text bodies pass unmasked; chunked text masks in 64 KiB units, so a split value passes; bodies over 16 MiB pass; a partial postgres or mysql frame at EOF passes; mssql SQL_VARIANT, XML and UDT pass and stop rewriting.
  • Not in these recordings: rules with hand-off, an invalid created rule, a mask before a human hold, a failed rule turn, the HTTP boundaries. QA passed them live on dc4095c1. a2d3854b only changes the CLJS session view.
  • The LLM is a stub. The UI ran from QA's builds of b33e3400 and a2d3854b (sources identical to the worktrees) behind a local proxy, not the packaged image.
  • External docs are not published yet.

Activity

27
racerxdl4d ago

/agentic_review

github-actions[bot]4d ago

📋 API Changelog

API Changelog unknown vs. unknown

No changes to report, but the specs are different

qodo-code-review[bot]4d ago

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (1) 📎 Requirement gaps (1) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)


1. Raw responses evade AI masking ✓ Resolved
Description
aiMaskScope grants MaskScopeExchange to a raw gRPC listener because hasSchema() treats
grpc.raw as a schema. When an AI mask applies to an RPC whose raw response contains an undeclared
string field, libhoop skips that field without an error, so the response can reach the client
unmasked.
Code

sidecar/daemon/airules.go[R196-197]

+		if lc.GRPC.hasSchema() {
+			return gate.MaskScopeExchange
Relevance

●●● Strong

The PR explicitly promises fail-closed masking where enforcement is uncertain; raw responses create
that exact bypass.

PR-#1877 PR-#1857

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The new scope check accepts any gRPC configuration for which hasSchema() is true; that predicate
includes raw mode. Libhoop explicitly limits raw rewriting to declared paths, skips undeclared
fields, and forwards the resulting payload.

hoop -> libhoop sidecar/daemon/airules.go[193-200] sidecar/daemon/grpc.go[317-324] External repo: hoophq/libhoop, v2/codec/grpc/raw.go [44-56] External repo: hoophq/libhoop, v2/codec/grpc/raw.go [446-466] External repo: hoophq/libhoop, v2/codec/grpc/server.go [1024-1051]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Raw gRPC is classified as supporting per-exchange AI masks, but libhoop masks only declared raw response paths and forwards undeclared fields unchanged.

## Fix Focus Areas
- sidecar/daemon/airules.go[193-200]
- sidecar/daemon/grpc.go[320-324]

## Recommended Fix
Treat raw gRPC listeners as `MaskScopeNone` unless the codec can guarantee enforcement for AI-selected masks. Keep the existing gate refusal for an AI-masked statement, and add a test with an undeclared response string field.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



2. Conflicting AI rule choices still apply ✗ Dismissed
Description
ruleLevel.parse skips a no_rule call without checking whether the same answer also contains rule
calls. When a solo rule turn returns both no_rule and an apply or create call, the latter can
still deny the statement or mask its response despite the explicit choice to apply no rule.
Code

sidecar/analyzer/rules.go[R538-542]

+		if solo && c.Name == toolNoRule {
+			if _, ok := callReason(c.Args); !ok {
+				return nil, errRulesMalformed
+			}
+			continue
Relevance

●●● Strong

The explicit no-rule choice conflicts with later enforcement; analyzer correctness fixes are
consistently accepted.

PR-#1913 PR-#1976

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The solo tool declares that it applies no rule, but the parser continues after encountering it;
subsequent rule calls are appended to the decision and later enforced.

sidecar/analyzer/rules.go[479-487] sidecar/analyzer/rules.go[534-579] sidecar/analyzer/rules.go[882-920]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
A solo rule answer can contain `no_rule` alongside calls that apply rules, and both are accepted.
## Fix Focus Areas
- sidecar/analyzer/rules.go[534-549]
- sidecar/analyzer/rules_test.go[1-566]
## Recommended Fix
Reject a solo answer containing `no_rule` unless it is the only call. Validate its arguments strictly, and add a test showing that a mixed answer applies no rules.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Malformed rules can still be applied ✗ Dismissed
Description
decodeStrict checks dec.More() after decoding one JSON object, which does not reject a second
top-level JSON value. A created guardrail or mask with valid arguments followed by trailing JSON
therefore passes validation and reaches rule compilation and application.
Code

sidecar/analyzer/rules.go[R619-626]

+func decodeStrict(raw json.RawMessage, v any) error {
+	dec := json.NewDecoder(bytes.NewReader(raw))
+	dec.DisallowUnknownFields()
+	if err := dec.Decode(v); err != nil {
+		return err
+	}
+	if dec.More() {
+		return errors.New("trailing data after the arguments")
Relevance

●●● Strong

This is a deterministic strict-decoding bug directly contradicting the feature’s malformed-rule
rejection requirement.

PR-#1913

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The checklist requires every AI-created rule to undergo strict, config-equivalent validation and
rejects malformed rules. The new decoder claims to reject trailing data but uses Decoder.More(),
which only checks for another element within the current container and does not reject another
top-level JSON value; the same decoder is used by both created-rule builders.

Validate configuration and AI-created rules sidecar/analyzer/rules.go[617-628] sidecar/analyzer/rules.go[650-666] sidecar/analyzer/rules.go[708-721]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Strict rule-argument validation accepts a second top-level JSON value after the decoded object, allowing malformed created guardrails or masks to be applied.

## Fix Focus Areas
- sidecar/analyzer/rules.go[619-628]

## Recommended Fix
After decoding the expected object, perform a second decode and require `io.EOF`; reject any additional token or value. Keep unknown-field rejection and the existing validation error path unchanged.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


4. Later packets reuse an old AI rule 📎 Requirement gap ≡ Correctness
Description
ruleTurn stores the model's ruleDecision in the shared classification cache and returns it on a
later cache hit. When another packet has the same classification and memo key before the
configurable cache TTL expires, that packet reuses the earlier statement's rule decision instead of
receiving a packet-lifetime decision.
Code

sidecar/analyzer/rules.go[R827-830]

+	content := approvalContent(c.text, level, c.res.Title, e.cfg.Listener, stmt.Protocol, principal(ec))
+	memoKey := ruleMemoKey(rl, content)
+	if m, ok := e.cache.decision(c.key, memoKey); ok && m.rules != nil {
+		return ruleOutcome{decision: m.rules}
Relevance

●●● Strong

The finding directly violates the PR’s packet-lifetime caching requirement and concerns a
security-relevant decision reuse.

PR-#1786 PR-#1857

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The checklist requires the AI rule decision to remain consistent through the packet lifetime without
being reused beyond that packet. The changed code looks up and stores ruleDecision in the shared
cache, while that cache retains entries according to configurable size and TTL rather than packet
completion.

Cache AI rule decisions for packet lifetime sidecar/analyzer/rules.go[812-830] sidecar/analyzer/rules.go[841-850] sidecar/analyzer/cache.go[41-55] sidecar/analyzer/cache.go[99-140]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
AI rule decisions are stored in the shared classification cache, whose configurable TTL can outlive the packet that produced the decision, allowing later packets to reuse an earlier statement's rule choice.

## Fix Focus Areas
- sidecar/analyzer/rules.go[812-850]
- sidecar/analyzer/cache.go[41-55]
- sidecar/analyzer/cache.go[99-140]

## Recommended Fix
Store the AI rule decision on the packet or exchange context keyed by the statement shape, and reuse it only during that packet's lifetime. Do not persist `ruleDecision` in the lane-level TTL/LRU classification cache; preserve the existing cache for classification results and any decisions whose required lifetime is explicitly cache-scoped.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Dismiss ↗ | View ↗


View medium (5)
5. The AI toggle lacks a visible label ✗ Dismissed
Description
The feature Switch is named only through aria-label={label} while the visible feature text is
rendered separately in a Text element. Each rendered AI policy feature therefore has a focusable
toggle without a visible label association.
Code

webapp_v2/src/pages/Features/AiSessionAnalyzer/components/AIPolicyCard.jsx[R104-118]

+        <Group justify="space-between" wrap="nowrap" align="flex-start">
+          <Stack gap={4}>
+            <Text size="sm" fw={600}>
+              {label}
+            </Text>
+            <Text size="xs" c="dimmed">
+              {description}
+            </Text>
+          </Stack>
+          <Switch
+            aria-label={label}
+            checked={value.on}
+            disabled={locked && !value.on}
+            onChange={(e) => onChange({ on: e.currentTarget.checked })}
+          />
Relevance

●● Moderate

The visible-label concern is plausible, but no close historical precedent establishes acceptance for
this switch-label pattern.

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The changed switch is focusable and receives only an aria-label, while the visible feature name is
a separate text node rather than an associated label element.

Rule 1800943: Associate each form input with a visible <label> element webapp_v2/src/pages/Features/AiSessionAnalyzer/components/AIPolicyCard.jsx[104-118]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The AI policy switch has only an `aria-label`; its visible feature name is not rendered as an associated label for the control.

## Fix Focus Areas
- webapp_v2/src/pages/Features/AiSessionAnalyzer/components/AIPolicyCard.jsx[104-118]

## Recommended Fix
Give each switch a stable `id` and associate it with a visible `<label>` using the feature label, or use the switch component's visible label support. Preserve the current checked and disabled behavior.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


6. A policy renderer breaks component naming ✗ Dismissed
Description
The new policyCard function returns JSX but begins with a lowercase identifier instead of
PascalCase. Because it is used to render the AI policy card for risk levels, future JSX usage and
component tooling can treat it inconsistently with the other React components in this change.
Code

webapp_v2/src/pages/Features/AiSessionAnalyzer/Create/SidecarAiAnalyzerFields.jsx[R93-103]

+  const policyCard = (level) => {
+    const action = form[level]
+    if (action === '' || !appliesAIRules(action)) return null
+    let helper
+    if (isHoldAction(action)) helper = 'Rules apply before the approval.'
+    else if (isHandoffAction(action)) helper = 'Rules apply when the agent does not block.'
+    // A bare hold files under an imported approval rule; an object would name
+    // the AI agent instead, so this level cannot carry AI rules.
+    const imported = isHoldAction(action) && form.approval[level].approver === APPROVER_RULE
+    return (
+      <AIPolicyCard
Relevance

●● Moderate

The naming concern is subjective because policyCard is invoked as a helper, not rendered directly as
a JSX component.

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The changed policyCard declaration returns JSX and is invoked to render the policy card, but its
identifier starts with a lowercase letter.

Rule 1800940: Use PascalCase for React component names webapp_v2/src/pages/Features/AiSessionAnalyzer/Create/SidecarAiAnalyzerFields.jsx[93-103]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`policyCard` is a JSX-returning React renderer declared with a lowercase name, contrary to the required PascalCase component naming convention.

## Fix Focus Areas
- webapp_v2/src/pages/Features/AiSessionAnalyzer/Create/SidecarAiAnalyzerFields.jsx[93-103]

## Recommended Fix
Rename `policyCard` to a PascalCase name such as `PolicyCard` or `RenderPolicyCard`, and update every invocation in this file while preserving its current arguments and return behavior.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


7. New policy file uses PascalCase ✗ Dismissed
Description
The new React component files AIPolicyCard.jsx and CardConnector.jsx use PascalCase filenames
instead of lowercase kebab-case filenames. The naming exception for code identifiers does not apply
to file paths, so these additions make component discovery inconsistent for later maintainers and
tooling.
Code

webapp_v2/src/pages/Features/AiSessionAnalyzer/components/AIPolicyCard.jsx[R1-5]

+import { useState } from 'react'
+import { Group, Paper, ScrollArea, Stack, Text } from '@mantine/core'
+import { PackagePlus, Search } from 'lucide-react'
+import Badge from '@/components/Badge'
+import Button from '@/components/Button'
Relevance

●● Moderate

The filename rule supports the finding, but available history lacks a close precedent for React
component filename enforcement.

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The patch adds AIPolicyCard.jsx and CardConnector.jsx as JavaScript component files; both
filenames contain uppercase letters rather than lowercase words separated by hyphens.

Rule 1800939: Use kebab-case only for file and folder names, not for code identifiers webapp_v2/src/pages/Features/AiSessionAnalyzer/components/AIPolicyCard.jsx[1-5] webapp_v2/src/pages/Features/AiSessionAnalyzer/components/CardConnector.jsx[1-7]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The newly added React component files `AIPolicyCard.jsx` and `CardConnector.jsx` use PascalCase filenames instead of the required kebab-case format.

## Fix Focus Areas
- webapp_v2/src/pages/Features/AiSessionAnalyzer/components/AIPolicyCard.jsx[1-5]
- webapp_v2/src/pages/Features/AiSessionAnalyzer/components/CardConnector.jsx[1-7]

## Recommended Fix
Rename the files to lowercase kebab-case names such as `ai-policy-card.jsx` and `card-connector.jsx`, and update all import paths that reference them. Keep the exported component identifiers PascalCase.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


8. Webapp checks lack verifiable results 📘 Rule violation ✧ Quality
Description
The PR changes webapp_v2 files but its testing section reports only sidecar testing and does not
provide successful npm run lint, npm test, and npm run build results for webapp_v2. Because
all three required checks are undocumented for this changed project, lint, test, or build
regressions can reach merge without verification.
Code

webapp_v2/src/pages/Features/AiSessionAnalyzer/Create/SidecarAiAnalyzerFields.jsx[R15-16]

+import { REVIEW_MODE_HOLD, REVIEW_MODES, appliesAIRules, isHandoffAction, isHoldAction } from '@/pages/sidecarRuleVocabulary'
+import AIPolicyCard from '../components/AIPolicyCard'
Relevance

●● Moderate

The repository has active testing requirements, but no close historical precedent confirms
documentation-only testing findings are accepted.

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The PR modifies webapp_v2, as shown by the changed import and component usage. The PR testing
section reports sidecar testing only and contains no verifiable evidence for the three required
webapp_v2 commands, violating the checklist requirement.

Rule 2362431: PRs touching webapp_v2 must have passing lint, test, and build scripts webapp_v2/src/pages/Features/AiSessionAnalyzer/Create/SidecarAiAnalyzerFields.jsx[15-16]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
This PR modifies `webapp_v2`, but the PR description does not provide verifiable successful results for the required lint, test, and build commands.

## Fix Focus Areas
- webapp_v2/src/pages/Features/AiSessionAnalyzer/Create/SidecarAiAnalyzerFields.jsx[15-16]

## Recommended Fix
Run `npm run lint`, `npm test`, and `npm run build` from the `webapp_v2` directory, resolve any failures, and record the successful results in the PR description or ensure the corresponding CI checks are required and passing.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Dismiss ↗ | View ↗


9. The rule search lacks a visible label ✗ Dismissed
Description
The TextInput in RuleList supplies only an aria-label and placeholder, with no visible
<label> associated with the control. Users who rely on visible form labels cannot identify the
search field consistently, and the same issue reaches every rule list rendered by this card.
Code

webapp_v2/src/pages/Features/AiSessionAnalyzer/components/AIPolicyCard.jsx[R42-49]

+        <TextInput
+          size="sm"
+          placeholder="Search rules"
+          aria-label="Search rules"
+          leftSection={<Search size={14} aria-hidden="true" />}
+          value={query}
+          onChange={(e) => setQuery(e.currentTarget.value)}
+        />
Relevance

●● Moderate

The accessibility requirement is plausible, but no close historical precedent establishes acceptance
for visible-label findings here.

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The changed TextInput has a placeholder and aria-label but no visible label or explicit label
association, violating the requirement for visible labels on user-entry controls.

Rule 1800943: Associate each form input with a visible <label> element webapp_v2/src/pages/Features/AiSessionAnalyzer/components/AIPolicyCard.jsx[42-49]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The rule search input has an accessible name through `aria-label`, but it does not have a visible associated label as required for changed form controls.

## Fix Focus Areas
- webapp_v2/src/pages/Features/AiSessionAnalyzer/components/AIPolicyCard.jsx[42-49]

## Recommended Fix
Add a visible label associated with the search input, using the component's label support or an explicit `<label for>` and matching input `id`. Keep the existing search behavior and accessible name.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



10. Sidecar config saves skip AI rule name checks ✗ Dismissed
Description
ValidateAnalyzerAIRules says it runs on every save, but only the analyzer-rule binding API calls
it (sidecarbind.go:95). Sidecar configuration PUT/PATCH never calls it, yet
composeSidecar/foldRuleLibrary read AI options from the stored listener analyzer block too. So a
save through that path is accepted with rule names the library doesn't hold, or with
guardrails/mask on a block/defer level, and the sidecar then only warns and ignores them.
Code

gateway/services/sidecarairules.go[R389-392]

+// ValidateAnalyzerAIRules refuses an analyzer rule whose levels let the AI
+// apply guardrails or data masking rules when the sidecar could not run them
+// as the admin wrote them. It runs on every save, bound or not:
+//
Relevance

●●● Strong

Recent gateway validation precedents favor closing save-path gaps for composed analyzer
configurations.

PR-#1863

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The validator is called only from the analyzer-rule binding handler. Composition says a level's AI
options also come from the stored listener block, which sidecar config PUT/PATCH writes without this
check. On the sidecar, unknown names and block-level options are only warnings, so the save looks
successful while the selection is silently dropped.

gateway/api/sidecarbind/sidecarbind.go[90-97] gateway/services/sidecarconfig.go[147-155] gateway/services/sidecarairules.go[389-431] sidecar/daemon/airules.go[234-276]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`ValidateAnalyzerAIRules` (library rule names, not allowed on block/defer levels) runs only when an analyzer rule is saved. Sidecar configuration PUT/PATCH can store listener analyzer blocks with AI `guardrails`/`mask` options that skip these checks, even though composition reads them.

## Fix Focus Areas
- gateway/services/sidecarairules.go[389-431]
- gateway/api/sidecar/sidecar.go[455-570]

## Recommended Fix
In the shared sidecar configuration write path, call `services.ValidateAnalyzerAIRules` for every listener's `Analyzer` block (marshal the block to JSON and pass the listener name as the rule name), and return 422 on error. If that is out of scope, change the doc comment so it no longer says it runs on every save.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Context sources
✅ Compliance rules (platform): 180 rules
✅ Skills: build-ticket
✅ Cross-repo context — repo relationships
  Explored: repo: hoophq/documentation (sha: 52af6604) — View relationship
  Explored: repo: hoophq/terraform-provider-hoop (sha: 8ad463c5) — View relationship
  Explored: repo: hoophq/libhoop (sha: 55dc7bef) — View relationship
  Explored: repo: hoophq/internal-runtime-control (sha: f9e2bc6f) — View relationship
  Explored: repo: hoophq/hoop-benchmarks (sha: ec40a61e) — View relationship
  Explored: repo: hoophq/hoop-skills (sha: 372cfc31) — View relationship
  Explored: repo: hoophq/voight-kampff (sha: c6c71dc5) — View relationship
Review mode: Auto: 🧠 Deep: Broad security-sensitive logic spans sidecar, gateway, and multiple UI paths.
Tip of the day
💡 Did you know, you can add REVIEW.md to your repo root and Qodo follows it on every PR

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

qodo-code-review[bot]reviewed4d ago
sandromello4d ago

✅ Build Completed with Success, Version=2074.0.0-ga2d3854

racerxdlreviewed4d ago
racerxdlreviewed4d ago
racerxdlreviewed4d ago
racerxdlreviewed4d ago
qodo-code-review[bot]reviewed4d ago
qodo-code-review[bot]reviewed4d ago
qodo-code-review[bot]reviewed4d ago
sandromello4d ago

✅ Build Completed with Success, Version=2074.0.0-ga9d93b7

sandromello2d ago

✅ Build Completed with Success, Version=2074.0.0-g457f4c7

This pull request is a draft

Merging is unavailable until it is marked ready for review.