feat: per-role tool lists and system prompt overlays

Each role in model_registry.json can now carry two optional keys:
  system_append — injected into the system prompt at position 7 (after
                  memory, closest to the turn) for the active chat_role
  tools         — explicit tool allow-list; intersected with the user's
                  access-level filter so it can only restrict, never elevate

No changes needed for existing users — missing keys fall back to current
behavior. Add keys to a role to give it a specialty focus:

  "coder": {
    "primary": "claude_cli",
    "system_append": "You are in code-specialist mode...",
    "tools": ["web_search", "file_read", "shell_exec", "scratch_write"]
  }

Changes:
- model_registry.py: get_role_config() returns system_append + tools
- context_loader.py: role_append param appended as "--- Role Context ---"
- tools/__init__.py: get_tools_for_role/get_openai_tools_for_role accept
  optional tool_list and intersect with access-level filter
- orchestrator_engine.py: tool_list threaded through run/resume/checkpoint
- openai_orchestrator.py: tool_list threaded through run/resume/checkpoint;
  _build_client now calls get_openai_tools_for_role instead of returning
  unfiltered OPENAI_TOOL_SCHEMAS
- routers/orchestrator.py: pulls role_cfg for chat_role, passes both
  role_append and tool_list to context loader and engine

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Scott Idem
2026-05-01 20:00:38 -04:00
parent 5ad2e50d69
commit 49123cdd5c
6 changed files with 92 additions and 17 deletions

View File

@@ -1100,19 +1100,38 @@ OPENAI_TOOL_SCHEMAS: list[dict] = _build_openai_tools()
# Role-filtered tool access
# ---------------------------------------------------------------------------
def get_tools_for_role(role: str) -> tuple[list, dict]:
def get_tools_for_role(
role: str,
tool_list: list[str] | None = None,
) -> tuple[list, dict]:
"""Return (gemini_tool_declarations, callables_dict) filtered to tools the role can use.
role — user access level ("user" | "admin"); gates admin-only tools
tool_list — optional explicit allow-list from role config (e.g. coder role);
intersected with the access-level filter so it can only restrict,
never elevate privileges
Usage in orchestrator:
tool_declarations, tool_callables = get_tools_for_role(user_role)
tool_declarations, tool_callables = get_tools_for_role(user_role, tool_list)
"""
allowed = {name for name in _CALLABLES if _role_allowed(name, role)}
if tool_list is not None:
allowed &= set(tool_list)
decls = [d for d in _ALL_DECLARATIONS if d.name in allowed]
callables = {k: v for k, v in _CALLABLES.items() if k in allowed}
return [types.Tool(function_declarations=decls)], callables
def get_openai_tools_for_role(role: str) -> list[dict]:
"""Return OpenAI tool schemas filtered to tools the role can use."""
def get_openai_tools_for_role(
role: str,
tool_list: list[str] | None = None,
) -> list[dict]:
"""Return OpenAI tool schemas filtered to tools the role can use.
role — user access level ("user" | "admin")
tool_list — optional explicit allow-list from role config
"""
allowed = {name for name in _CALLABLES if _role_allowed(name, role)}
if tool_list is not None:
allowed &= set(tool_list)
return [t for t in OPENAI_TOOL_SCHEMAS if t["function"]["name"] in allowed]