Skip to content

fix: guard vendor_id/invoice_id=None in chat assistant tool callables (#418) - #579

Open
Deez-Automations wants to merge 1 commit into
GenAI-Security-Project:mainfrom
Deez-Automations:fix/chat-vendor-id-none-guard-418
Open

fix: guard vendor_id/invoice_id=None in chat assistant tool callables (#418)#579
Deez-Automations wants to merge 1 commit into
GenAI-Security-Project:mainfrom
Deez-Automations:fix/chat-vendor-id-none-guard-418

Conversation

@Deez-Automations

Copy link
Copy Markdown

Fixes #418.

The bug

VendorChatAssistant and CoPilotAssistant's native tool callables forward vendor_id/invoice_id straight to the underlying data functions with no None check. If the LLM calls a tool without supplying the ID argument, it reaches the data layer as None.

Corrections made against the linked issue

  • Scope is 12 call sites, not 5. The issue lists 5 methods on one class. In practice the same 5 methods are duplicated byte-for-byte across both VendorChatAssistant (vendor-portal-facing, POST /chat) and CoPilotAssistant (admin-portal-facing, POST /copilot/chat) — 10 sites total. Auditing the rest of _build_native_callables() for the same pattern turned up two more CoPilotAssistant-only methods with the identical gap: _call_get_vendor_compliance_docs and _call_get_vendor_activity_report. 12 sites fixed total.
  • Exception type correction. The issue says a TypeError (or a raw SQLAlchemy error) propagates. Traced the actual behavior instead of trusting that: SQLAlchemy translates Column == None into a valid IS NULL clause, so e.g. VendorRepository.get_vendor(None) just returns no matching row (primary keys are never NULL) rather than raising anything at the DB layer. It's the application code's own if not X: raise ValueError(...) checks that actually fire. Every affected path raises ValueError, never TypeError.
  • Severity correction. Every one of these methods is called exclusively through _execute_tool, which already wraps the call in a broad try/except Exception and converts any exception into a clean JSON error string before it reaches the LLM. So a None ID here was never an uncaught crash — the real (still worth fixing) bug is a misleading error message (e.g. "Vendor not found" when the actual problem is "you forgot to pass vendor_id"), which risks the LLM reasoning incorrectly, like concluding a real vendor doesn't exist.

Fix

Added an explicit is None guard at the top of all 12 methods, returning {"error": "<field> is required"} before the call reaches the data layer, and widened the type hints to int | None to reflect reality.

Tests

13 new tests in tests/unit/agents/test_chat_assistant_none_guards.py: one per guarded method (5 on VendorChatAssistant, 7 on CoPilotAssistant), plus one integration test through _execute_tool documenting the severity-correction point above. Confirmed RED before the fix, GREEN after.

Test plan

…GenAI-Security-Project#418)

VendorChatAssistant and CoPilotAssistant's native tool callables
(_call_get_vendor_details, _call_get_invoice_details,
_call_get_vendor_invoices, _call_get_vendor_payment_summary,
_call_get_vendor_contact_info) forwarded a None vendor_id/invoice_id
straight to the underlying data functions with no guard.

Corrections made against the linked issue while fixing:
- Scope is 12 call sites, not 5: the same 5 methods are duplicated
  byte-for-byte across both VendorChatAssistant and CoPilotAssistant
  (10 sites), plus two CoPilotAssistant-only methods with the
  identical unguarded pattern found while auditing the rest of
  _build_native_callables() (_call_get_vendor_compliance_docs,
  _call_get_vendor_activity_report).
- The issue claims a TypeError/SQLAlchemy error propagates. Traced
  the real behavior instead: SQLAlchemy translates `Column == None`
  to a valid `IS NULL` clause, so VendorRepository.get_vendor(None)
  just returns no row, and the application code's own `if not X:
  raise ValueError(...)` fires -- always ValueError, never TypeError.
- Severity correction: every one of these methods is called
  exclusively through _execute_tool, which already wraps the call in
  a broad try/except and converts any exception into a clean JSON
  error string before it reaches the LLM. So this was never an
  uncaught crash -- the real bug is a misleading error message
  ("vendor not found" instead of "you forgot to supply vendor_id"),
  which risks the LLM reasoning incorrectly (e.g. concluding a real
  vendor doesn't exist).

13 new tests, one per guarded method plus one integration test
through _execute_tool documenting the severity correction above.
Copilot AI lite review requested due to automatic review settings August 20, 2026 17:45

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bug_197_EVALUATE: CHAT-BOUNDARY-014 — vendor_id=None in _call_get_vendor_details propagates to DB layer with no clean error response

2 participants