Merge pull request 'fix(agents): INV-AG3 — כלי שמורים לסוכן להריץ חייב להיות מוענק לו, ושער-CI שיאכוף' (#424) from worktree-agent-tool-grants into main
All checks were successful
INV-AG3 Agent Tool Grants / agent-tool-grants (push) Successful in 4s
Build & Deploy / build-and-deploy (push) Successful in 10s
G12 Leak-Guard / leak-guard (push) Successful in 5s
Lint — undefined names / undefined-names (push) Successful in 11s

This commit was merged in pull request #424.
This commit is contained in:
2026-08-04 09:09:03 +00:00
7 changed files with 288 additions and 5 deletions

View File

@@ -15,8 +15,12 @@ tools:
- mcp__legal-ai__document_get_text - mcp__legal-ai__document_get_text
- mcp__legal-ai__extract_claims - mcp__legal-ai__extract_claims
- mcp__legal-ai__extract_appraiser_facts - mcp__legal-ai__extract_appraiser_facts
- mcp__legal-ai__get_appraiser_facts
- mcp__legal-ai__get_claims - mcp__legal-ai__get_claims
- mcp__legal-ai__aggregate_claims_to_arguments - mcp__legal-ai__aggregate_claims_to_arguments
- mcp__legal-ai__get_legal_arguments
- mcp__legal-ai__analyze_protocol
- mcp__legal-ai__get_protocol_analysis
- mcp__legal-ai__search_case_documents - mcp__legal-ai__search_case_documents
- mcp__legal-ai__search_decisions - mcp__legal-ai__search_decisions
- mcp__legal-ai__search_precedent_library - mcp__legal-ai__search_precedent_library
@@ -123,9 +127,9 @@ tools:
4. חלץ טענות/תשובות/תגובות (`extract_claims` עם doc_type ו-party_hint מתאימים) 4. חלץ טענות/תשובות/תגובות (`extract_claims` עם doc_type ו-party_hint מתאימים)
- **מסמך גדול (>15,000 תווים):** מאז phase 1 של מערכת הניתוח, ה-chunking הסמנטי + מקבילות + retry מטופל אוטומטית. גם מסמך של 100K+ תווים ירוץ עד הסוף. אם בכל זאת נכשל — דווח ב-issue. - **מסמך גדול (>15,000 תווים):** מאז phase 1 של מערכת הניתוח, ה-chunking הסמנטי + מקבילות + retry מטופל אוטומטית. גם מסמך של 100K+ תווים ירוץ עד הסוף. אם בכל זאת נכשל — דווח ב-issue.
- **טיפול בכשל:** אם `extract_claims` החזיר `partial=true` או 0 טענות ממסמך לא ריק — נסה שוב פעם אחת. אם עדיין נכשל — סטטוס issue = `blocked`, פרסם comment עם הפירוט. - **טיפול בכשל:** אם `extract_claims` החזיר `partial=true` או 0 טענות ממסמך לא ריק — נסה שוב פעם אחת. אם עדיין נכשל — סטטוס issue = `blocked`, פרסם comment עם הפירוט.
5. **חלץ עובדות שמאי** — לכל מסמך `doc_type='appraisal'` בתיק, הרץ `extract_appraiser_facts(case_number)` (פעם אחת לתיק, מטפל בכל השומות). **חובה בכל ערר השבחה (8xxx) ופיצויים (9xxx) — בלי זה ה-writer לא יוכל לכתוב את בלוק ז עם מספרים מדויקים.** 5. **חלץ עובדות שמאי** — לכל מסמך `doc_type='appraisal'` בתיק, הרץ `extract_appraiser_facts(case_number)` (פעם אחת לתיק, מטפל בכל השומות). **חובה בכל ערר השבחה (8xxx) ופיצויים (9xxx) — בלי זה ה-writer לא יוכל לכתוב את בלוק ז עם מספרים מדויקים.** מיד אחריו הרץ `mcp__legal-ai__get_appraiser_facts(case_number)` כדי **לקרוא בחזרה** את מה שנשמר ולוודא שהחילוץ אכן נחת — אל תדווח על חילוץ שלא אימתת בקריאה.
6. וודא שכל פריט מסווג ל-claim_type הנכון 6. וודא שכל פריט מסווג ל-claim_type הנכון
7. **קבץ טענות לטיעונים משפטיים** — לאחר שכל הטענות חולצו וסוּוגו, הרץ `aggregate_claims_to_arguments(case_number)` שמקבץ את הפרופוזיציות הגולמיות לטיעונים משפטיים מובחנים (~6-12 לכל צד). זהו קלט מובנה לבלוק ז (טענות הצדדים) ולבלוק י (דיון) — הכותב נשען עליו. אם 0 טענות חולצו — דלג. הפלט עובר שער-אישור (ראה `get_legal_arguments`). 7. **קבץ טענות לטיעונים משפטיים** — לאחר שכל הטענות חולצו וסוּוגו, הרץ `aggregate_claims_to_arguments(case_number)` שמקבץ את הפרופוזיציות הגולמיות לטיעונים משפטיים מובחנים (~6-12 לכל צד). זהו קלט מובנה לבלוק ז (טענות הצדדים) ולבלוק י (דיון) — הכותב נשען עליו. אם 0 טענות חולצו — דלג. הפלט עובר שער-אישור — קרא אותו בחזרה עם `mcp__legal-ai__get_legal_arguments(case_number)` ואמת שמספר הטיעונים לכל צד סביר לפני שאתה ממשיך.
### שלב 2: ניתוח מעמיק ### שלב 2: ניתוח מעמיק
הצג במבנה הבא: הצג במבנה הבא:
@@ -270,6 +274,29 @@ search_precedent_library(
**מינימום:** מספר queries ב-Q1+Q2+Q3 לקורפוס הסמכותי = מספר טענות סף + מספר סוגיות מרכזיות. אם זיהית 5 סוגיות + 2 טענות סף → לפחות 7 queries. **מינימום:** מספר queries ב-Q1+Q2+Q3 לקורפוס הסמכותי = מספר טענות סף + מספר סוגיות מרכזיות. אם זיהית 5 סוגיות + 2 טענות סף → לפחות 7 queries.
## משימה על-פי-דרישה: ניתוח פרוטוקול-הדיון
**זו אינה חלק מהזרימה הרגילה** — היא מגיעה כ-issue ייעודי כשחיים לוחץ "נתח את פרוטוקול הדיון" בפאנל **"מה קרה בדיון"** (טאב טיעונים-ועמדות). ה-issue יאמר במפורש להריץ `analyze_protocol`.
```
mcp__legal-ai__analyze_protocol(case_number="<מספר-התיק>")
mcp__legal-ai__analyze_protocol(case_number="<מספר-התיק>", document_id="<uuid>") # כשיש כמה פרוטוקולים
```
הכלי משווה את **פרוטוקול דיון ועדת-הערר** מול **הטיעונים המאוגדים**, ומסווג כל שינוי ל-`strengthened` / `newly_raised` / `dropped`, עם `evidence_quote` מהפרוטוקול לכל שורה (INV-AH — אין שורה בלי ציטוט).
**שני תנאים מוקדמים — בדוק אותם לפני שאתה מריץ:**
1. **פרוטוקול של ועדת-הערר.** הכלי בוחר אוטומטית פרוטוקול שה-`protocol_scope` שלו אינו `lower`; פרוטוקול של הוועדה **המקומית** מזין רקע (בלוק ו) בלבד ולא מוביל את ההשוואה. אם כל הפרוטוקולים בתיק הם `lower` — אין דיון-ערר להשוות אליו.
2. **טיעונים מאוגדים.** הרץ `get_legal_arguments(case_number)`. אם ריק — הרץ קודם `aggregate_claims_to_arguments`.
אם תנאי חסר — **אל תעקוף**: כתוב comment בעברית שמפרט מה חסר, וסגור `blocked`.
`re-run` מחליף את הניתוח הקודם (idempotent). לקריאה בלבד, בלי ניתוח מחדש: `mcp__legal-ai__get_protocol_analysis(case_number, change_type="")`.
**דווח ב-comment בעברית**: כמה טענות התחזקו, כמה נטענו לראשונה, כמה ירדו — ומה החידוד המרכזי שעלה בדיון.
> ⚠️ **הרץ את הכלי בקדמה — לעולם לא ברקע.** שיגור ל-background וסיום התור מסיים את ה-run, וקבוצת-התהליכים נהרגת יחד איתו: הניתוח נקטע ולא נשמר דבר (נצפה ב-CMP-229, 2026-08-04). הכלי לוקח כמה דקות; זה תקין. חכה לו.
## שלב 6: בדיקת שלמות — לפני שמסיימים! ## שלב 6: בדיקת שלמות — לפני שמסיימים!
**לפני סיום, בצע את הבדיקות הבאות. אם בדיקה נכשלת — אל תסיים כ-"done".** **לפני סיום, בצע את הבדיקות הבאות. אם בדיקה נכשלת — אל תסיים כ-"done".**

View File

@@ -41,6 +41,7 @@ tools:
- mcp__legal-ai__halacha_corroboration - mcp__legal-ai__halacha_corroboration
- mcp__legal-ai__corroboration_rebuild - mcp__legal-ai__corroboration_rebuild
- mcp__legal-ai__extract_appraiser_facts - mcp__legal-ai__extract_appraiser_facts
- mcp__legal-ai__get_appraiser_facts
- mcp__legal-ai__extract_plans - mcp__legal-ai__extract_plans
- mcp__legal-ai__plan_get - mcp__legal-ai__plan_get
- mcp__legal-ai__plan_search - mcp__legal-ai__plan_search
@@ -704,6 +705,8 @@ ls data/cases/$CASE_NUMBER/documents/research/analysis-and-research.md
``` ```
⚠️ אם מחזיר `status="sides_missing"` → דווח לחיים שאין תיוג `appraiser_side` במסמכי השומה (`document_update` עם `appraiser_side` בערכים `committee`/`appellant`/`deciding`). עצור עד שיתוקן. ⚠️ אם מחזיר `status="sides_missing"` → דווח לחיים שאין תיוג `appraiser_side` במסמכי השומה (`document_update` עם `appraiser_side` בערכים `committee`/`appellant`/`deciding`). עצור עד שיתוקן.
אחרי החילוץ — הרץ `mcp__legal-ai__get_appraiser_facts(case_number="...")` כדי **לקרוא בחזרה** ולוודא שהעובדות אכן נשמרו. חילוץ שדיווח הצלחה אך לא נקרא בחזרה אינו ראיה שהנתונים שם.
אם הטבלה כבר מלאה — `write_interim_draft` ידלג על ההרצה אוטומטית, אז גם בלי הצעד הזה זה יעבוד. אם הטבלה כבר מלאה — `write_interim_draft` ידלג על ההרצה אוטומטית, אז גם בלי הצעד הזה זה יעבוד.
3. **כתיבת 5 הבלוקים:** 3. **כתיבת 5 הבלוקים:**

View File

@@ -17,6 +17,7 @@ tools:
- mcp__legal-ai__search_precedent_library - mcp__legal-ai__search_precedent_library
- mcp__legal-ai__search_internal_decisions - mcp__legal-ai__search_internal_decisions
- mcp__legal-ai__precedent_library_get - mcp__legal-ai__precedent_library_get
- mcp__legal-ai__precedent_library_list
- mcp__legal-ai__precedent_list - mcp__legal-ai__precedent_list
- mcp__legal-ai__halacha_review - mcp__legal-ai__halacha_review
--- ---

View File

@@ -0,0 +1,25 @@
name: INV-AG3 Agent Tool Grants
# Hard gate for INV-AG3 (docs/spec/X4-agents.md §2א): a subagent's `tools:`
# frontmatter is a CLOSED allow-list, so any MCP tool an agent is TOLD to run —
# by the backend delegation in web/, or by its own instructions — must appear
# there. Built after analyze_protocol shipped without a grant (2026-06-30) and
# the analyst was handed an issue instructing it to run a tool it could not
# call (CMP-229, 2026-08-04). Pure-stdlib check (no venv) — fast, runs on every
# PR and on push to main.
on:
pull_request:
branches: [main]
push:
branches: [main]
jobs:
agent-tool-grants:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: INV-AG3 — agent tool-grant guard
run: python3 scripts/agent_tool_grants_guard.py

View File

@@ -146,9 +146,29 @@ another company`, [X2 §2](X2-multi-company.md)).
**כלל:** ה-frontmatter `tools:` של כל סוכן מעניק **בדיוק** את הכלים שהוראותיו דורשות — כל כלי שההוראות **כלל:** ה-frontmatter `tools:` של כל סוכן מעניק **בדיוק** את הכלים שהוראותיו דורשות — כל כלי שההוראות
מצריכות מוענק, וכלי שמוענק-ולא-בשימוש נבחן. מופע של [G10](00-constitution.md#inv-g10-המערכת-מסייעת--שערים-אנושיים-הם-invariant) מצריכות מוענק, וכלי שמוענק-ולא-בשימוש נבחן. מופע של [G10](00-constitution.md#inv-g10-המערכת-מסייעת--שערים-אנושיים-הם-invariant)
(שערים מוגדרים) ו-[G2](00-constitution.md#inv-g2-מקור-אמת-יחיד--אין-מסלולים-מקבילים-מתפצלים); מקביל ל-[X9 INV-TOOL6](X9-mcp-tool-contract.md). (שערים מוגדרים) ו-[G2](00-constitution.md#inv-g2-מקור-אמת-יחיד--אין-מסלולים-מקבילים-מתפצלים); מקביל ל-[X9 INV-TOOL6](X9-mcp-tool-contract.md).
**מקור-סמכות:** frontmatter `tools:` מול ה-instructions בקבצי-[.claude/agents/](../../.claude/agents/). (פרויקטלי-תפעולי.)
**אכיפה:** בדיקת-עקביות tools↔instructions (FU-13 ✅ 2026-06-06). אכיפה אוטומטית עתידית — בתת-פרויקט 5 (spec-guardian). > **ה-frontmatter הוא allow-list סגורה.** כלי הרשום בשרת-ה-MCP אך חסר מהרשימה **אינו ניתן לקריאה**
**הפרה ידועה:** — (טופל ב-FU-13: legal-analyst קיבל `aggregate_claims_to_arguments`; researcher כבר היה תקין; `extract_references`/`extract_internal_citations` הם מטלת-researcher, לא analyst — ראה §2א). > ע"י הסוכן — גם כשהשרת מחובר לחלוטין. הסוכן חווה זאת כ"הכלים לא נחשפים לסשן", ובלי הבנת המנגנון
> הוא נוטה **לעקוף** (SQL ישיר, סקריפט מקומי) במקום לדווח — וזה מסלול מקביל, כלומר הפרת G2.
> **"מורים להריץ" כולל את ה-backend:** טקסט של issue שנוצר ב-`web/` ומכיל `mcp__legal-ai__X` הוא
> הוראה לכל דבר, ולכן מחייב הענקה.
**מקור-סמכות:** frontmatter `tools:` מול ה-instructions בקבצי-[.claude/agents/](../../.claude/agents/)
**ומול הוראות-ה-backend** ב-`web/`. (פרויקטלי-תפעולי.)
**אכיפה:****אוטומטית מ-2026-08-04** — [`scripts/agent_tool_grants_guard.py`](../../scripts/agent_tool_grants_guard.py),
שער-CI קשיח ([`.gitea/workflows/agent-tool-grants.yaml`](../../.gitea/workflows/agent-tool-grants.yaml)),
בדפוס [`leak_guard.py`](../../scripts/leak_guard.py) של G12. ארבעה כללים: (1) כל `mcp__legal-ai__X`
ב-`web/` מוענק לסוכן כלשהו · (2) כל `mcp__legal-ai__X` בגוף קובץ-סוכן מוענק **באותו** קובץ ·
(3) אין הענקה לכלי שאינו רשום בשרת · (4) שם-כלי בגרשיים-הפוכים ללא תחילית — מוענק, או מסווג
מפורשות ב-`CONTRASTIVE_OK` (אזכור ניגודי / מטלת-סוכן-אחר / שם-עמודה מתנגש). לא-סוכנים ולכן
מוחרגים: `hermes-curator.md` ו-`legal-analyst-gemini-critique.md` (בלי frontmatter בכוונה —
האדפטר שולח פרומפט גולמי) ו-`HEARTBEAT.md` (checklist משותף).
**הפרה ידועה:** — (היסטוריה: FU-13 ✅ 2026-06-06 — `aggregate_claims_to_arguments` ל-analyst;
`extract_references`/`extract_internal_citations` הם מטלת-researcher, ראה §2א. **הישנות 2026-08-04**
— האכיפה הידנית לא החזיקה: `analyze_protocol` (24e3e2f, 2026-06-30) נרשם בשרת בלי הענקה, ו-#226
הוסיף delegation שמורה למנתח להריץ אותו → CMP-229 נשרף בשתי הרצות ועקף ל-SQL ידני. נסגר יחד עם
`get_protocol_analysis`/`get_legal_arguments`/`get_appraiser_facts` ו-`precedent_library_list` ל-QA,
והאכיפה הועברה ל-CI כדי שלא תישען שוב על משמעת ידנית.)
### INV-AG4: שער שטן-מליץ — red-team לידים לא-סמכותיים תחת אישור-יו ### INV-AG4: שער שטן-מליץ — red-team לידים לא-סמכותיים תחת אישור-יו
**כלל:** אחרי שלב-הניתוח (`analysis-and-research.md` תקין) וב**לפני** הפעלת הכותב, ה-CEO מפעיל **כלל:** אחרי שלב-הניתוח (`analysis-and-research.md` תקין) וב**לפני** הפעלת הכותב, ה-CEO מפעיל

View File

@@ -98,6 +98,7 @@
|--------|------|---------|-----------| |--------|------|---------|-----------|
| `spec-guard.sh` | bash | **PreToolUse hook לאכיפת "פרוטוקול כתיבת-קוד"** (CLAUDE.md §פרוטוקול כתיבת-קוד) — בכל Edit/Write/MultiEdit על נתיב-קוד (`web/`, `mcp-server/`, `web-ui/src/`, `scripts/`, `adapters/`) מזריק תזכורת ל-Claude לקרוא את `docs/spec/00-constitution.md`+ספ-התחום ולוודא קיום G1G12 — לפני שכותבים. **+ leak-guard בזמן-אמת (G12):** על כתיבה ל-`mcp-server/src/*` בודק את התוכן-הנכתב (`new_string`/`content`) ומזהיר אם מוזרק מונח-Paperclip לשכבת-האינטליגנציה (לא-deduped). המקבילה האינטראקטיבית ל-INV-AG1. קלט JSON ב-stdin, פלט `hookSpecificOutput.additionalContext` (non-blocking, exit 0). Dedup פעם-בסשן לתזכורת-הספ. רשום ב-`.claude/settings.json`. | נקרא אוטומטית ע"י Claude Code (hook) | | `spec-guard.sh` | bash | **PreToolUse hook לאכיפת "פרוטוקול כתיבת-קוד"** (CLAUDE.md §פרוטוקול כתיבת-קוד) — בכל Edit/Write/MultiEdit על נתיב-קוד (`web/`, `mcp-server/`, `web-ui/src/`, `scripts/`, `adapters/`) מזריק תזכורת ל-Claude לקרוא את `docs/spec/00-constitution.md`+ספ-התחום ולוודא קיום G1G12 — לפני שכותבים. **+ leak-guard בזמן-אמת (G12):** על כתיבה ל-`mcp-server/src/*` בודק את התוכן-הנכתב (`new_string`/`content`) ומזהיר אם מוזרק מונח-Paperclip לשכבת-האינטליגנציה (לא-deduped). המקבילה האינטראקטיבית ל-INV-AG1. קלט JSON ב-stdin, פלט `hookSpecificOutput.additionalContext` (non-blocking, exit 0). Dedup פעם-בסשן לתזכורת-הספ. רשום ב-`.claude/settings.json`. | נקרא אוטומטית ע"י Claude Code (hook) |
| `leak_guard.py` | python | **המאכף הקנוני של INV-G12 (שער-הפלטפורמה / docs/spec/X15 §4 / R4).** שני כללים קשיחים: (1) `mcp-server/src` ללא סמלי-Paperclip (allowlist מנומק לפי substring); (2) רק `web/agent_platform_port.py` (+ קבצי-המעטפת) מייבאים את לקוח-Paperclip. stdlib-בלבד (אין venv). `leak_guard.py` = סריקת-repo (exit 1 על הפרה); `leak_guard.py <file>...` = קבצים נתונים (ל-hook). משותף ל-spec-guard.sh (hook), ל-CI (`.gitea/workflows/leak-guard.yaml`) ול-`mcp-server/tests/test_platform_port_leak_guard.py`. | CI + hook + pytest | | `leak_guard.py` | python | **המאכף הקנוני של INV-G12 (שער-הפלטפורמה / docs/spec/X15 §4 / R4).** שני כללים קשיחים: (1) `mcp-server/src` ללא סמלי-Paperclip (allowlist מנומק לפי substring); (2) רק `web/agent_platform_port.py` (+ קבצי-המעטפת) מייבאים את לקוח-Paperclip. stdlib-בלבד (אין venv). `leak_guard.py` = סריקת-repo (exit 1 על הפרה); `leak_guard.py <file>...` = קבצים נתונים (ל-hook). משותף ל-spec-guard.sh (hook), ל-CI (`.gitea/workflows/leak-guard.yaml`) ול-`mcp-server/tests/test_platform_port_leak_guard.py`. | CI + hook + pytest |
| `agent_tool_grants_guard.py` | python | **המאכף הקנוני של INV-AG3 (מפת-הרשאות הסוכנים / docs/spec/X4-agents.md §2א).** ה-frontmatter `tools:` של סוכן הוא **allow-list סגורה** — כלי הרשום בשרת-ה-MCP אך חסר ממנה אינו ניתן לקריאה, גם כשהשרת מחובר. ארבעה כללים קשיחים: (1) כל `mcp__legal-ai__X` המופיע ב-`web/` (delegation שיוצר issue לסוכן) מוענק לסוכן כלשהו; (2) כל `mcp__legal-ai__X` בגוף קובץ-סוכן מוענק ב-frontmatter של **אותו** קובץ; (3) אין הענקה לכלי שאינו רשום ב-`@mcp.tool`; (4) שם-כלי בגרשיים-הפוכים ללא תחילית — מוענק, או מסווג ב-`CONTRASTIVE_OK` עם נימוק (כולל בדיקת-התיישנות לסיווגים). מחריג קבצים שאינם סוכני-claude_local: `hermes-curator.md`, `legal-analyst-gemini-critique.md`, `HEARTBEAT.md`. stdlib-בלבד. נבנה אחרי CMP-229 (2026-08-04) — `analyze_protocol` נרשם בשרת ב-2026-06-30 בלי הענקה, ו-#226 הורה למנתח להריץ אותו. CI: `.gitea/workflows/agent-tool-grants.yaml`. | CI |
| `check_undefined_names.py` | python | **CI gate ל-undefined names (מחלקת ה-NameError).** מריץ pyflakes על `web`, `mcp-server/src`, `scripts` ומפיל build (exit 1) רק על "undefined name"/"may be undefined" — לא על imports-לא-בשימוש/f-strings (רעש). זו בדיוק מחלקת-הבאג של PR #249 (שינוי-שם תיק → 500): שם שמופנה אך לא מיובא/מוגדר, חבוי בתוך `background_tasks` עד זמן-ריצה. דורש pyflakes (ה-workflow מתקין ל-venv זמני). משותף ל-CI (`.gitea/workflows/lint.yaml`). | CI | | `check_undefined_names.py` | python | **CI gate ל-undefined names (מחלקת ה-NameError).** מריץ pyflakes על `web`, `mcp-server/src`, `scripts` ומפיל build (exit 1) רק על "undefined name"/"may be undefined" — לא על imports-לא-בשימוש/f-strings (רעש). זו בדיוק מחלקת-הבאג של PR #249 (שינוי-שם תיק → 500): שם שמופנה אך לא מיובא/מוגדר, חבוי בתוך `background_tasks` עד זמן-ריצה. דורש pyflakes (ה-workflow מתקין ל-venv זמני). משותף ל-CI (`.gitea/workflows/lint.yaml`). | CI |
| `auto-sync-cases.sh` | bash | סנכרון תיקי ערר ל-Gitea — רץ כל דקה | `* * * * *` (cron) | | `auto-sync-cases.sh` | bash | סנכרון תיקי ערר ל-Gitea — רץ כל דקה | `* * * * *` (cron) |
| `host_sync.sh` | bash | מסנכרן את עץ-המארח `~/legal-ai` ל-origin/main (ff-only) כדי שקוד-המארח (כותב/פאנלים/MCP שרצים מהעץ, לא בקונטיינר) יתעדכן אחרי merge; restart מדויק ל-chat/court-fetch/reaper רק כשקבציהם משתנים. בטוח: אף-פעם לא force; tasks.json הדירטי נשמר. סוגר את פער-פריסת-המארח (TaskMaster #160) | `* * * * *` (cron, flock) | | `host_sync.sh` | bash | מסנכרן את עץ-המארח `~/legal-ai` ל-origin/main (ff-only) כדי שקוד-המארח (כותב/פאנלים/MCP שרצים מהעץ, לא בקונטיינר) יתעדכן אחרי merge; restart מדויק ל-chat/court-fetch/reaper רק כשקבציהם משתנים. בטוח: אף-פעם לא force; tasks.json הדירטי נשמר. סוגר את פער-פריסת-המארח (TaskMaster #160) | `* * * * *` (cron, flock) |

View File

@@ -0,0 +1,206 @@
#!/usr/bin/env python3
"""INV-AG3 guard — every MCP tool an agent is TOLD to run must be GRANTED to it.
The canonical checker for INV-AG3 (docs/spec/X4-agents.md §2א): a Claude-Code
subagent's ``tools:`` frontmatter is a CLOSED allow-list. A tool that is
registered on the MCP server but absent from that list is *not callable* by the
agent, however well the server is connected.
Why this exists — the failure it is built to catch (2026-08-04):
``analyze_protocol`` shipped on 2026-06-30 (24e3e2f) touching 9 files, none of
them ``.claude/agents/*``. Later ``wake_analyst_for_protocol_analysis``
(web/paperclip_client.py, #226) started writing "הרץ
``mcp__legal-ai__analyze_protocol(...)``" straight into the analyst's issue.
The analyst therefore received an explicit instruction to run a tool it was
never granted, reported "tools exist on the connected server but aren't exposed
as callable in this session", and burned two runs working around it via raw
psql + a hand-written script. INV-AG3 already covered this on paper; its
enforcement was deferred ("אכיפה אוטומטית עתידית"), so the drift went unnoticed
for five weeks. This script is that deferred enforcement.
Three HARD rules:
1. **Backend delegation.** Every ``mcp__legal-ai__X`` named inside ``web/``
(the backend telling an agent what to run) must be granted to at least one
agent. This is the rule that catches the 2026-08-04 failure.
2. **Per-agent instructions.** Every ``mcp__legal-ai__X`` in an agent file's
BODY must be granted in that same file's frontmatter. Prefixed mentions are
imperative by convention ("הרץ `mcp__legal-ai__…`").
3. **No phantom grants.** Every granted tool must actually be registered on
the MCP server — catches typos and tools deleted out from under an agent.
Plus one reviewed-exception rule:
4. **Bare tool names.** An agent body may name a tool in backticks without the
``mcp__legal-ai__`` prefix (```get_legal_arguments```). Those are
ambiguous: some are real requirements, others are deliberately contrastive
("**לא** דרך `precedent_library_upload`"), a pointer at *another* agent's
job, or a DB column that merely shares a tool's name. Each is classified
once in ``CONTRASTIVE_OK`` below; anything new fails until reviewed.
NOT AGENTS (no frontmatter by design — the adapter sends the file as a raw
prompt, so YAML would leak into it): ``hermes-curator.md`` (deepseek_local),
``legal-analyst-gemini-critique.md`` (gemini_local). ``HEARTBEAT.md`` is a
shared checklist, not an agent. All three are skipped.
Usage:
agent_tool_grants_guard.py # exit 1 on any violation
"""
from __future__ import annotations
import re
import sys
from pathlib import Path
REPO = Path(__file__).resolve().parent.parent
AGENTS_DIR = REPO / ".claude" / "agents"
MCP_SRC = REPO / "mcp-server" / "src"
BACKEND_DIR = REPO / "web"
# Files under .claude/agents/ that are not claude_local subagent definitions.
NOT_AGENTS = {
"HEARTBEAT.md",
"hermes-curator.md",
"legal-analyst-gemini-critique.md",
}
# Bare (unprefixed) tool names in an agent body that are NOT requirements.
# Each entry is (agent file, tool, why) — reviewed 2026-08-04. Adding to this
# map is a deliberate act: it asserts "the agent is not being told to call this".
CONTRASTIVE_OK = {
("legal-analyst.md", "case_create"): "prose about the cases.practice_area CHECK constraint, not a call",
("legal-analyst.md", "search_internal_decisions"): "names the filter surface when contrasting Axis A/B",
("legal-ceo.md", "search_decisions"): "contrast — 'search_decisions = only Dafna' vs the granted search_internal_decisions",
("legal-ceo.md", "precedent_library_upload"): "explicitly the forbidden path ('לא דרך …', citation guard rejects)",
("legal-ceo.md", "document_update"): "describes the tagging chaim must fix, not a CEO call",
("legal-proofreader.md", "extraction_status"): "the documents.extraction_status DB column — name collides with a tool",
("legal-qa.md", "precedent_attach"): "explicitly the researcher's job ('דרך precedent_attach של ה-researcher')",
("legal-writer.md", "revise_draft"): "the CEO calls it ('CEO יקרא ל-revise_draft'), not the writer",
("legal-writer.md", "search_case_precedents"): "a do-not-confuse disambiguation note ('שונה! … לא לבלבל')",
}
TOOL_RE = re.compile(r"mcp__legal-ai__(\w+)")
REGISTER_RE = re.compile(r"@mcp\.tool\([^)]*\)\s*(?:async\s+)?def\s+(\w+)")
# `tool_name(` or `tool_name` inside backticks.
BARE_RE = re.compile(r"`(\w+)[(`]")
def server_tools() -> set[str]:
"""Tool names registered on the MCP server."""
out: set[str] = set()
for path in MCP_SRC.rglob("*.py"):
out |= set(REGISTER_RE.findall(path.read_text(encoding="utf-8", errors="ignore")))
return out
def split_frontmatter(text: str) -> tuple[str, str]:
"""Return (frontmatter, body). Empty frontmatter when the file has none."""
if not text.startswith("---"):
return "", text
parts = text.split("---")
if len(parts) < 3:
return "", text
return parts[1], "---".join(parts[2:])
def agent_files() -> list[Path]:
return sorted(p for p in AGENTS_DIR.glob("*.md") if p.name not in NOT_AGENTS)
def main() -> int:
registered = server_tools()
if not registered:
print("agent-tool-grants: FAIL — no @mcp.tool registrations found; is the tree complete?")
return 1
grants: dict[str, set[str]] = {}
bodies: dict[str, str] = {}
for path in agent_files():
fm, body = split_frontmatter(path.read_text(encoding="utf-8", errors="ignore"))
grants[path.name] = set(TOOL_RE.findall(fm))
bodies[path.name] = body
all_granted: set[str] = set().union(*grants.values()) if grants else set()
violations: list[str] = []
# Rule 1 — backend delegation must land on a granted tool.
for path in sorted(BACKEND_DIR.rglob("*.py")):
text = path.read_text(encoding="utf-8", errors="ignore")
for tool in sorted(set(TOOL_RE.findall(text))):
if tool not in all_granted:
rel = path.relative_to(REPO)
violations.append(
f"[1 backend] {rel} instructs an agent to run "
f"mcp__legal-ai__{tool}, but NO agent grants it.\n"
f" fix: add `- mcp__legal-ai__{tool}` to the tools: "
f"frontmatter of the agent that receives that issue."
)
# Rule 2 — a prefixed mention in an agent body is an instruction to that agent.
for name, body in bodies.items():
for tool in sorted(set(TOOL_RE.findall(body))):
if tool not in grants[name]:
violations.append(
f"[2 instructions] .claude/agents/{name} tells the agent to run "
f"mcp__legal-ai__{tool}, which its own tools: list omits.\n"
f" fix: add `- mcp__legal-ai__{tool}` to that frontmatter."
)
# Rule 3 — no grant may point at a tool the server does not register.
for name, granted in grants.items():
for tool in sorted(granted - registered):
violations.append(
f"[3 phantom] .claude/agents/{name} grants mcp__legal-ai__{tool}, "
f"which is not registered on the MCP server.\n"
f" fix: correct the name, or drop the grant if the tool was removed."
)
# Rule 4 — every bare tool name is either granted or classified as contrastive.
for name, body in bodies.items():
bare = {m for m in BARE_RE.findall(body) if m in registered}
for tool in sorted(bare - grants[name]):
if (name, tool) in CONTRASTIVE_OK:
continue
violations.append(
f"[4 bare name] .claude/agents/{name} mentions `{tool}` — a real MCP "
f"tool it is not granted.\n"
f" fix: grant it if the agent must call it, otherwise add "
f"(\"{name}\", \"{tool}\") to CONTRASTIVE_OK with the reason."
)
# Stale exceptions: a classification that no longer matches the text is noise.
for (name, tool), _why in sorted(CONTRASTIVE_OK.items()):
if name not in bodies:
violations.append(
f"[4 stale] CONTRASTIVE_OK names {name}, which is not an agent file."
)
elif tool not in {m for m in BARE_RE.findall(bodies[name])}:
violations.append(
f"[4 stale] CONTRASTIVE_OK ({name}, {tool}) no longer appears in that "
f"file — drop the exception."
)
if violations:
print(f"INV-AG3 agent-tool-grants guard: {len(violations)} violation(s)\n")
for v in violations:
print(f"{v}")
print(
"\ndocs/spec/X4-agents.md §2א INV-AG3 — the frontmatter tools: list is a "
"CLOSED allow-list.\nA tool missing from it is not callable, no matter that "
"the MCP server is connected."
)
return 1
print(
f"INV-AG3 agent-tool-grants guard: OK "
f"({len(agent_files())} agents, {len(all_granted)} distinct grants, "
f"{len(registered)} tools registered)"
)
return 0
if __name__ == "__main__":
sys.exit(main())