feat(ops): מעבר-אדפטר בטוח לכל סוכן ← כל אדפטר (any→any, שתי החברות) — סוגר FU-8a
All checks were successful
G12 Leak-Guard / leak-guard (pull_request) Successful in 4s
All checks were successful
G12 Leak-Guard / leak-guard (pull_request) Successful in 4s
הקשר: החלפת adapter_type ב-dropdown הגולמי של Paperclip מקריסה את הסוכן מיד —
content_arg adapters (gemini_local/deepseek_local) שוברים את ה-frontmatter המוביל `---`
(yargs/arg-parser קורא אותו כדגל → "Not enough arguments following: prompt"), המודל
חייב להתאים ל-provider, ו-excludeTools של gemini גלובלי לכל הסוכנים. בנוסף — שינוי
חברה-אחת מפר את INV-MC1 (סנכרון מדלג על adapter mismatch → drift שקט).
הכלי מיישב את 3 צירי-הכשל data-driven, מעביר בשתי החברות יחד, הפיך מדויק, ומסרב
מעבר לא-בטוח ב-preflight (סוגר את פער FU-8a "אין שער-ולידציה למעבר-אדפטר").
- scripts/adapter_profiles.py — רישום-פרופילי-אדפטר (מקור-אמת יחיד; אדפטר חדש = רשומה אחת)
- scripts/migrate_agent_adapter.py — --check/--apply/--revert/--verify; frontmatter→.nofm.md
נגזר, sidecar לשחזור, --relax-tools ל-excludeTools, PATCH דרך /api/agents/{id} (לא DB)
- .gitignore + scripts/SCRIPTS.md
נבדק: --verify (9 סוכנים תקינים), --check (CEO→gemini מציג nofm+model+tool-conflict;
provider-guard תופס מודל לא-תואם, exit≠0), ובדיקות-לוגיקה טהורות (strip/canonical/recompute).
Invariants: מקיים G2 (רישום+קובץ-קנוני יחיד, גרסת-gemini נגזרת), G12 (PATCH-API דרך
המעטפת, לא DB), INV-MC1 (שתי חברות יחד). סוגר FU-8a.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
93
scripts/adapter_profiles.py
Normal file
93
scripts/adapter_profiles.py
Normal file
@@ -0,0 +1,93 @@
|
||||
#!/usr/bin/env python3
|
||||
"""adapter_profiles.py — single source of truth for Paperclip adapter capabilities.
|
||||
|
||||
Why this exists: switching a Paperclip agent's adapter is NOT a free dropdown
|
||||
flip. Each adapter family differs on three axes that, if not reconciled, crash
|
||||
the agent immediately or degrade it silently:
|
||||
|
||||
1. model id ↔ provider — each adapter expects a model from its own provider
|
||||
(claude-* / gemini-* / deepseek-*). Wrong/foreign id fails or silently
|
||||
falls back to the adapter default.
|
||||
2. instructions transport —
|
||||
• file_path adapters pass the instructions file as a *path*
|
||||
(`claude --append-system-prompt-file`); the CLI parses it,
|
||||
so a leading `---` YAML frontmatter is fine.
|
||||
• content_arg adapters read the file and splice its *content* into the
|
||||
CLI argument (`gemini --prompt "<content>"`,
|
||||
`hermes chat -q "<content>"`). yargs/arg-parsers read a
|
||||
value starting with `--` as a flag → the run dies within
|
||||
~2s with "Not enough arguments following: prompt". These
|
||||
adapters therefore require a frontmatter-FREE instructions
|
||||
file (must not start with `--`).
|
||||
3. tool availability — claude uses the frontmatter `tools:`/`--allowedTools`;
|
||||
gemini reads a GLOBAL `~/.gemini/settings.json` `excludeTools` list (one
|
||||
list for ALL gemini agents); deepseek uses `~/.hermes`.
|
||||
|
||||
Keeping these facts in one declarative registry makes "migrate any agent to any
|
||||
adapter" data-driven: adding a future adapter (codex_local, grok_local, ...) is
|
||||
one new entry here. Consumed by scripts/migrate_agent_adapter.py.
|
||||
|
||||
Verified 2026-06-13 against the installed adapter packages
|
||||
(`@paperclipai/adapter-{claude,gemini}-local`, in-repo
|
||||
`adapters/deepseek-paperclip-adapter`) and the live Paperclip agents table.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
# tool_config values:
|
||||
# "frontmatter" — tools come from the agent .md frontmatter / --allowedTools
|
||||
# "gemini_global" — tools gated by the global ~/.gemini/settings.json excludeTools
|
||||
# "hermes" — tools configured under ~/.hermes (deepseek runtime)
|
||||
ADAPTER_PROFILES: dict[str, dict] = {
|
||||
"claude_local": {
|
||||
"provider": "claude",
|
||||
"default_model": "claude-opus-4-8",
|
||||
"instructions_mode": "file_path",
|
||||
"frontmatter_safe": True,
|
||||
"tool_config": "frontmatter",
|
||||
},
|
||||
"gemini_local": {
|
||||
"provider": "gemini",
|
||||
"default_model": "gemini-3.1-pro-preview",
|
||||
"instructions_mode": "content_arg",
|
||||
"frontmatter_safe": False,
|
||||
"tool_config": "gemini_global",
|
||||
},
|
||||
"deepseek_local": {
|
||||
"provider": "deepseek",
|
||||
"default_model": "deepseek-v4-pro",
|
||||
"instructions_mode": "content_arg",
|
||||
"frontmatter_safe": False,
|
||||
"tool_config": "hermes",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
class UnknownAdapter(ValueError):
|
||||
"""Raised when an adapter type has no registered profile."""
|
||||
|
||||
|
||||
def get_profile(adapter_type: str) -> dict:
|
||||
"""Return the capability profile for an adapter type, or raise UnknownAdapter.
|
||||
|
||||
Fail loud: a typo'd or unregistered adapter must not silently pass through
|
||||
migration (that is exactly how the original crash happened)."""
|
||||
profile = ADAPTER_PROFILES.get(adapter_type)
|
||||
if profile is None:
|
||||
known = ", ".join(sorted(ADAPTER_PROFILES))
|
||||
raise UnknownAdapter(
|
||||
f"אדפטר לא מוכר: {adapter_type!r}. רשומים: {known}. "
|
||||
f"הוסף פרופיל ל-adapter_profiles.py לפני מעבר אליו."
|
||||
)
|
||||
return profile
|
||||
|
||||
|
||||
def model_matches_provider(model: str, adapter_type: str) -> bool:
|
||||
"""True iff `model` belongs to the adapter's provider family.
|
||||
|
||||
Empty model is treated as matching — the adapter falls back to its own
|
||||
default model, which is by definition in-family."""
|
||||
if not model:
|
||||
return True
|
||||
provider = get_profile(adapter_type)["provider"]
|
||||
return model.startswith(provider)
|
||||
Reference in New Issue
Block a user