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:
@@ -45,6 +45,7 @@ async def run(
|
||||
model_cfg: dict | None = None,
|
||||
respond_with_final: bool = True,
|
||||
user_role: str = "user",
|
||||
tool_list: list[str] | None = None,
|
||||
confirm_allow: set[str] | None = None,
|
||||
confirm_deny: set[str] | None = None,
|
||||
) -> OrchestratorResult:
|
||||
@@ -71,7 +72,7 @@ async def run(
|
||||
_confirm_deny = frozenset(confirm_deny or ())
|
||||
effective_confirm = (CONFIRM_REQUIRED - set(_confirm_allow)) | set(_confirm_deny)
|
||||
|
||||
client, model_name, active_tools = _build_client(model_cfg)
|
||||
client, model_name, active_tools = _build_client(model_cfg, user_role, tool_list)
|
||||
|
||||
sys_content = (system_prompt or "") + _TOOL_INSTRUCTION
|
||||
messages: list[dict] = [{"role": "system", "content": sys_content}]
|
||||
@@ -95,6 +96,7 @@ async def run(
|
||||
model_cfg=model_cfg,
|
||||
respond_with_final=respond_with_final,
|
||||
user_role=user_role,
|
||||
tool_list=tool_list,
|
||||
confirm_allow=_confirm_allow,
|
||||
confirm_deny=_confirm_deny,
|
||||
starting_round=0,
|
||||
@@ -121,7 +123,7 @@ async def run(
|
||||
|
||||
async def resume(checkpoint: OrchestrateCheckpoint, confirmed: bool) -> OrchestratorResult:
|
||||
"""Continue an OpenAI orchestrator job that was paused at a confirmation gate."""
|
||||
client, model_name, active_tools = _build_client(checkpoint.model_cfg)
|
||||
client, model_name, active_tools = _build_client(checkpoint.model_cfg, checkpoint.user_role, checkpoint.tool_list)
|
||||
|
||||
effective_confirm = (CONFIRM_REQUIRED - set(checkpoint.confirm_allow)) | set(checkpoint.confirm_deny)
|
||||
|
||||
@@ -138,8 +140,7 @@ async def resume(checkpoint: OrchestrateCheckpoint, confirmed: bool) -> Orchestr
|
||||
|
||||
for pt in checkpoint.pending_tools:
|
||||
if confirmed:
|
||||
_, callables = get_tools_for_role(checkpoint.user_role)
|
||||
result_str = await _execute_tool_dict(pt["name"], pt["args"], checkpoint.user_role)
|
||||
result_str = await _execute_tool_dict(pt["name"], pt["args"], checkpoint.user_role, checkpoint.tool_list)
|
||||
logger.info("Confirmed tool %s → %d chars", pt["name"], len(result_str))
|
||||
else:
|
||||
result_str = "Action denied by user."
|
||||
@@ -162,6 +163,7 @@ async def resume(checkpoint: OrchestrateCheckpoint, confirmed: bool) -> Orchestr
|
||||
model_cfg=checkpoint.model_cfg,
|
||||
respond_with_final=checkpoint.respond_with_final,
|
||||
user_role=checkpoint.user_role,
|
||||
tool_list=checkpoint.tool_list,
|
||||
confirm_allow=checkpoint.confirm_allow,
|
||||
confirm_deny=checkpoint.confirm_deny,
|
||||
starting_round=checkpoint.rounds_used,
|
||||
@@ -200,6 +202,7 @@ async def _run_from_messages(
|
||||
confirm_allow: frozenset,
|
||||
confirm_deny: frozenset,
|
||||
starting_round: int = 0,
|
||||
tool_list: list[str] | None = None,
|
||||
) -> tuple[str, OrchestrateCheckpoint | None]:
|
||||
"""
|
||||
Run the OpenAI ReAct loop from the current messages state.
|
||||
@@ -253,7 +256,7 @@ async def _run_from_messages(
|
||||
pending_tools.append({"name": name, "args": args_parsed, "tool_call_id": tc.id})
|
||||
logger.info("Tool %s blocked — confirmation required", name)
|
||||
else:
|
||||
result_str = await _execute_tool(name, tc.function.arguments, user_role)
|
||||
result_str = await _execute_tool(name, tc.function.arguments, user_role, tool_list)
|
||||
logger.info("Tool %s → %d chars", name, len(result_str))
|
||||
executed_results.append({"name": name, "args": args_parsed, "result": result_str, "tool_call_id": tc.id})
|
||||
tool_call_log.append({"tool": name, "args": args_parsed, "result": result_str})
|
||||
@@ -286,6 +289,7 @@ async def _run_from_messages(
|
||||
model_cfg=model_cfg,
|
||||
respond_with_final=respond_with_final,
|
||||
user_role=user_role,
|
||||
tool_list=tool_list,
|
||||
confirm_allow=confirm_allow,
|
||||
confirm_deny=confirm_deny,
|
||||
rounds_used=round_num + 2,
|
||||
@@ -311,7 +315,11 @@ async def _run_from_messages(
|
||||
return final_response, None
|
||||
|
||||
|
||||
def _build_client(model_cfg: dict | None) -> tuple:
|
||||
def _build_client(
|
||||
model_cfg: dict | None,
|
||||
user_role: str = "user",
|
||||
tool_list: list[str] | None = None,
|
||||
) -> tuple:
|
||||
"""Build AsyncOpenAI client and return (client, model_name, active_tools)."""
|
||||
if not model_cfg:
|
||||
raise RuntimeError("model_cfg is required for the OpenAI orchestrator")
|
||||
@@ -327,12 +335,18 @@ def _build_client(model_cfg: dict | None) -> tuple:
|
||||
if host_type == "openwebui":
|
||||
base_url = base_url + "/api"
|
||||
client = AsyncOpenAI(base_url=base_url, api_key=api_key)
|
||||
return client, model_name, OPENAI_TOOL_SCHEMAS
|
||||
active_tools = get_openai_tools_for_role(user_role, tool_list)
|
||||
return client, model_name, active_tools
|
||||
|
||||
|
||||
async def _execute_tool(name: str, arguments_json: str, user_role: str = "user") -> str:
|
||||
async def _execute_tool(
|
||||
name: str,
|
||||
arguments_json: str,
|
||||
user_role: str = "user",
|
||||
tool_list: list[str] | None = None,
|
||||
) -> str:
|
||||
"""Parse tool arguments and execute with role-filtered callables."""
|
||||
_, callables = get_tools_for_role(user_role)
|
||||
_, callables = get_tools_for_role(user_role, tool_list)
|
||||
try:
|
||||
args = json.loads(arguments_json)
|
||||
except json.JSONDecodeError:
|
||||
@@ -344,9 +358,14 @@ async def _execute_tool(name: str, arguments_json: str, user_role: str = "user")
|
||||
return f"Tool error: {e}"
|
||||
|
||||
|
||||
async def _execute_tool_dict(name: str, args: dict, user_role: str = "user") -> str:
|
||||
async def _execute_tool_dict(
|
||||
name: str,
|
||||
args: dict,
|
||||
user_role: str = "user",
|
||||
tool_list: list[str] | None = None,
|
||||
) -> str:
|
||||
"""Execute a tool from a pre-parsed args dict."""
|
||||
_, callables = get_tools_for_role(user_role)
|
||||
_, callables = get_tools_for_role(user_role, tool_list)
|
||||
try:
|
||||
return await call_tool(name, args, callables)
|
||||
except Exception as e:
|
||||
|
||||
Reference in New Issue
Block a user