12 KiB
Phase 2 — Security-First Tool Discovery
وضعیت: در کد پیاده شده است.
تاریخ: ۱۹ اوت ۲۰۲۶
Source of Truth کد: hesabixAPI/app/services/ai/
مرتبط: tool-discovery-architecture.md · dynamic-tool-discovery.md
اصل معماری
Security is a hard boundary, not a ranking signal.
A tool must be authorized before it can become a discovery candidate.
Discovery reduces context; it must never weaken authorization.
1. Current Security Flow
پس از Phase 1، هویت Tool در Manifest/Index است. مسیر زندهٔ چت:
All tools (registry)
│
▼
Tenant / session business_id ← function_registry.get_function_definitions
│ (business_context_required)
▼
Role filter ← allowed_roles
│
▼
User permission ← required_permissions + ai_permission_map
│
▼
Security / side-effect filter ← ai_tool_security.filter_security_candidates
│ (execution_mode + query mutation)
▼
Intent / domain filter ← select_tool_names / select_catalog_tool_names
│
▼
Ranking / Top-K ← rank_and_cap_tool_names (۴۸ / ۱۲۸)
│
▼
LLM
│
▼
Tool call
│
▼
Execution guard ← ai_write_guard + ai_execution_policy
│ (analyzer block, approval, high-risk)
▼
Handler (session business_id injected)
| لایه | کجا | چه زمانی |
|---|---|---|
| Tool identity | ai_tool_manifest / AIFunction |
ثبت Tool |
| Tool security class | side_effect + intent_write + always_confirm |
قبل از Ranking |
| User permission | required_permissions + ai_permission_map |
قبل از Security filter |
| UI permission alias | ai_permission_map.PERMISSION_ALIASES |
فقط ترجمهٔ رشته به سکشن UI |
| Tenant / company | business_id نشست + business_context_required |
Registry؛ تکرار نمیشود |
| Execution mode | ai_execution_policy |
Discovery (analyzer=read-only) و Execution |
| Approval | ai_write_guard / should_require_write_approval |
بعد از tool_call، قبل از handler |
| MCP list | get_function_definitions |
Permission/tenant فقط |
| MCP call | is_write_function + approve_writes |
Guard اجرا؛ listing مجوز اجرا نیست |
Intent selection (select_tool_names) دیگر روی کل کاتالوگ permissionخورده کار نمیکند؛ ورودیاش مجموعهٔ امنیتیشده است. force_tool_names (تأیید در جریان) از فیلتر mutation معاف است تا پیام «تأیید» Tool نوشتنی را حذف نکند.
2. Problems
Phase 0/1 این شکافها را باقی گذاشته بود:
- Permission قبل از Intent بود، اما side-effect بعد از Intent نبود.
delete_invoiceدر دامنهٔfinancialاست؛ گزارش وضعیت مالی آن را وارد ۴۸/۱۲۸ میکرد. - کاتالوگ Autonomous همهٔ writeها را بعد از Ranking دوباره union میکرد. سقف ۱۲۸ امنیت را override میکرد.
filter_function_definitionsروی مجموعهٔ خالی fail-open بود و همهٔ تعاریف را برمیگرداند.- Intent نوشتن با Destructive یکی بود (
حذفداخل_WRITE_KEYWORDS). - Export شبیه Read بهنظر میرسید درحالیکه
side_effect=exportوalways_confirmاست. - امتیاز keyword میتوانست Tool غیرمجاز را در Top-K نگه دارد چون فیلتر سخت قبل از rank نبود.
3. Security Model
طبقهبندی Tool از Manifest است، نه از prefix نام. Prefix فقط داخل infer_side_effect بهعنوان signal ذخیره میشود.
| کلاس Discovery | شرط Manifest | مثال |
|---|---|---|
READ_ONLY |
side_effect=none |
search_invoices |
WRITE |
side_effect=write یا intent_write (و delete/export/execute نباشد) |
create_invoice |
DESTRUCTIVE |
side_effect=delete |
delete_invoice |
HIGH_RISK |
side_effect=export یا execute |
export_business_data, execute_workflow |
UNKNOWN |
خارج از Manifest یا enabled=false |
— |
always_confirm و risk_level سیاست اجرا/تأیید هستند، نه امتیاز Discovery. اگر always_confirm را در Discovery معادل HIGH_RISK میگرفتیم، create_workflow از intent نوشتن اتوماسیون حذف میشد. تأیید همچنان در Execution Guard است.
کلاس mutation کوئری (جدا از دامنهٔ مالی/انبار):
| Query | Mutation |
|---|---|
| فاکتور علی را نشان بده | READ |
| فاکتور جدید ایجاد کن | WRITE |
| این فاکتور را حذف کن | DESTRUCTIVE |
| خروجی اکسل / export | EXPORT |
| اجرای workflow | EXECUTE |
| خالی / نامشخص | UNKNOWN → فقط read |
اولویت: Destructive > Execute > Export > Write > Read.
ماتریس مجاز (supervised/autonomous):
| Mutation \ Tool | READ_ONLY | WRITE | DESTRUCTIVE | HIGH_RISK |
|---|---|---|---|---|
| READ / UNKNOWN | بله | خیر | خیر | خیر |
| WRITE | بله | بله | خیر | خیر |
| DESTRUCTIVE | بله | خیر | بله | خیر |
| EXPORT | بله | خیر | خیر | فقط export |
| EXECUTE | بله | خیر | خیر | فقط execute |
Analyzer: فقط READ_ONLY، حتی اگر کوئری نوشتنی باشد.
4. Candidate Filtering
قرارداد Discovery:
Input:
permissioned tool names (بعد از tenant/role/permission)
user context / tenant (در Registry مصرف شده؛ اینجا تکرار نمیشود)
execution_mode
intent / query / history
forced_names (تأیید در جریان، plan/subagent در صورت expose)
Output:
authorized candidates only
API:
filter_security_candidates(
names,
user_query,
execution_mode=mode,
forced_names=...,
history_messages=...,
unknown_policy="deny", # production
)
سپس select_tool_names / select_catalog_tool_names فقط روی همین مجموعه rank میکنند. prefer_names و امتیاز semantic/keyword نمیتوانند نام حذفشده را برگردانند.
unknown_policy="allow" فقط برای fillerهای تست ranking است (zzz_filler_*) که در Manifest نیستند.
5. Permission Architecture
لایهها جدا میمانند. Registry محل سیاست User نیست.
Tool Identity Manifest name + AIFunction.name
Tool Security side_effect / intent_write / always_confirm / risk_level
User Permission AIFunction.required_permissions (رشتههای محصول)
UI Permission ai_permission_map aliases → people.view / invoices.add / …
Tenant Policy session business_id + can_access_business در handler
ai_permission_map همچنان alias UI/Product است. Tool Identity محسوب نمیشود.
6. Tenant Isolation
Toolها رکورد per-tenant نیستند؛ کاتالوگ سراسری است. جداسازی داده:
get_function_definitions: بدونbusiness_idهیچ Tool باbusiness_context_requiredcandidate نمیشود.- Handler:
business_idاز نشست inject میشود؛business_idجعلی در arguments اگر به کسبوکار دیگر اشاره کند رد میشود. - Discovery فیلتر tenant جداگانه تکرار نمیکند تا cache/logic دوتایی ساخته نشود.
«Tool متعلق به tenant دیگر» در این مدل یعنی نام در ورودی permissioned/session نیست → candidate نمیشود.
7. Risk Classification
Export عمداً READ_ONLY نیست. فیلد جدید (SENSITIVE_READ) اضافه نشد؛ side_effect=export + always_confirm همان مفهوم exfiltration را میپوشاند و در Discovery کلاس HIGH_RISK است.
Destructive از side_effect=delete میآید (نه فقط delete_* در Intent). Write از metadata است. Execute از side_effect=execute.
8. Approval Flow
Discovery فیلتر میکند؛ جای Guard اجرا را نمیگیرد.
User Request → Intent → Security Filter → Candidate → LLM
→ Tool Call → Approval / Risk Guard → Execution
- analyzer: write در candidate نیست و
should_block_write_in_analyzerهمچنان فعال است. - supervised: write فقط با mutation نوشتن candidate میشود؛ اجرا بدون
approve_writesنمیشود. - autonomous: high-risk (
always_confirm) همچنان تأیید میخواهد. force_tool_namesبرای ادامهٔ نوبت تأیید لازم است تا Tool از لیست حذف نشود.
9. MCP Security
MCP سرور خروجی است (POST /api/v1/ai/mcp)، نه موتور Discovery داخلی.
| مسیر | فیلتر |
|---|---|
tools/list |
همان permission/tenant Registry — کلاینت MCP انتخاب خودش را دارد |
tools/call |
prepare_mcp_tool_arguments در پشتی _approve_writes را حذف میکند؛ write فقط با params.approve_writes is True |
Listing یک write tool اجازهٔ اجرا نیست. Agent داخلی و MCP هر دو به Execution Guard ختم میشوند.
10. Failure Modes
| حالت | رفتار |
|---|---|
| UNKNOWN classification | NOT CANDIDATE (unknown_policy=deny) |
| Permission نامشخص / رد | نام به فیلتر نمیرسد |
| بدون business_id | Toolهای کسبوکار حذف میشوند |
| کوئری عمومی مالی | delete/export/execute/write نیستند |
| مجموعهٔ allowed خالی | هیچ Schemaای به مدل نمیرود (fail-closed) |
| Rank بالا روی Tool غیرمجاز | بیاثر؛ نام در ورودی rank نیست |
| LLM tool_call خارج از لیست | Registry + write guard |
| MCP call بدون approve | build_approval_required_result |
11. Tests
tests/test_ai_tool_security.py بهعلاوهٔ رگرسیون:
- Unauthorized / authorized read / write روی read query
- Generic financial query → نه delete/export/execute
- Explicit delete →
delete_invoiceممکن است candidate شود؛is_write_functionهمچنان True (approval) - Unknown → fail-closed
- Ranking
prefer_namesامنیت را override نمیکند - MCP write/export/execute/delete همچنان gated
- Tenant = عدم ورود نام خارج از ورودی نشست
- Gold recall، catalog، execution policy، write guard، channel، MCP write، subagent، memory
12. Performance
فیلتر روی حدود ۱۸۰ نام در حافظه است (Index از قبل ساخته شده). در این Phase cache جدید اضافه نشد:
- Permission همان مسیر Registry است (بدون query اضافه per tool فراتر از قبل)
- Metadata از
ToolIndex/ Manifest - Authorized set هر نوبت از روی نامهای permissionخورده محاسبه میشود
Cache مجموعهٔ مجاز وقتی کاتالوگ به ۱۲۰۰+ برسد و هزینهٔ permission غالب شود در Phaseهای بعد بررسی شود.
13. Migration Notes
- تعداد Tool: ۱۸۰ → ۱۸۰
- سقف ۴۸/۱۲۸ عوض نشده
- Vector / hybrid /
search_tools/ progressive schema پیاده نشده - تست
test_autonomous_catalog_keeps_907_write_toolsبهtest_autonomous_catalog_keeps_mutation_allowed_writesتغییر کرد: محافظت از همهٔ deleteها روی کوئری ایجاد شخص همان باگ Phase 0 بود filter_function_definitions([])دیگر همه را برنمیگرداند
14. Phase 3 Recommendation
موتور واحد discover_tools(ctx, query, mode, channel) -> ToolOffer برای چت، MCP، CRM، subagent — هنوز بدون vector search. ورودی همان Authorized set این Phase است. سقف و Schema کامل را یکی کند تا get_available_functions شاخهٔ تکراری نداشته باشد.