|
|
@@ -0,0 +1,95 @@
|
|
|
+"""Role-aware tool policy for explicit-validation runs."""
|
|
|
+
|
|
|
+from __future__ import annotations
|
|
|
+
|
|
|
+from dataclasses import dataclass
|
|
|
+from typing import Any, FrozenSet, List, Optional
|
|
|
+
|
|
|
+from .models import AgentRole, CompletionPolicy
|
|
|
+
|
|
|
+
|
|
|
+PLANNER_TOOLS = frozenset({"task_plan", "dispatch_tasks", "task_decide", "validate_attempt"})
|
|
|
+WORKER_TOOLS = frozenset({"submit_attempt"})
|
|
|
+VALIDATOR_TOOLS = frozenset({"submit_validation"})
|
|
|
+
|
|
|
+WRITE_TOOL_NAMES = frozenset({
|
|
|
+ "write", "write_file", "write_json", "edit", "edit_file", "bash", "bash_command",
|
|
|
+ "agent", "evaluate", "goal", "task_plan", "dispatch_tasks", "task_decide",
|
|
|
+ "validate_attempt", "submit_attempt", "publish", "send_message", "import_content",
|
|
|
+ "knowledge_save", "knowledge_update", "knowledge_batch_update", "knowledge_save_pending",
|
|
|
+})
|
|
|
+
|
|
|
+
|
|
|
+ROLE_HARD_DENIES = {
|
|
|
+ AgentRole.PLANNER: WORKER_TOOLS | VALIDATOR_TOOLS,
|
|
|
+ AgentRole.WORKER: frozenset({"agent", "evaluate", "goal"}) | PLANNER_TOOLS | VALIDATOR_TOOLS,
|
|
|
+ AgentRole.VALIDATOR: WRITE_TOOL_NAMES | WORKER_TOOLS,
|
|
|
+ AgentRole.LEGACY: frozenset(),
|
|
|
+}
|
|
|
+
|
|
|
+
|
|
|
+@dataclass(frozen=True)
|
|
|
+class ResolvedAgentPolicy:
|
|
|
+ role: AgentRole
|
|
|
+ effective_tools: FrozenSet[str]
|
|
|
+ max_iterations: int
|
|
|
+ temperature: float
|
|
|
+ skills: Optional[List[str]]
|
|
|
+ system_prompt: Optional[str]
|
|
|
+ completion_policy: CompletionPolicy
|
|
|
+
|
|
|
+
|
|
|
+@dataclass(frozen=True)
|
|
|
+class AuthorizationResult:
|
|
|
+ allowed: bool
|
|
|
+ reason: Optional[str] = None
|
|
|
+
|
|
|
+
|
|
|
+class DefaultToolPolicy:
|
|
|
+ """Resolve schemas and enforce the same authorization at execution time."""
|
|
|
+
|
|
|
+ def resolve(self, config: Any, preset: Any, registry: Any) -> ResolvedAgentPolicy:
|
|
|
+ policy = CompletionPolicy(getattr(config, "completion_policy", CompletionPolicy.LEGACY_AUTO.value))
|
|
|
+ role = AgentRole(getattr(preset, "role", AgentRole.LEGACY.value))
|
|
|
+
|
|
|
+ if getattr(config, "tools", None) is not None:
|
|
|
+ candidates = set(config.tools)
|
|
|
+ elif getattr(config, "tool_groups", None) is not None:
|
|
|
+ candidates = set(registry.get_tool_names(groups=config.tool_groups))
|
|
|
+ else:
|
|
|
+ candidates = set(registry.get_tool_names())
|
|
|
+
|
|
|
+ if preset.allowed_tools is not None:
|
|
|
+ candidates &= set(preset.allowed_tools)
|
|
|
+ if preset.denied_tools:
|
|
|
+ candidates -= set(preset.denied_tools)
|
|
|
+ candidates -= set(getattr(config, "exclude_tools", []) or [])
|
|
|
+ candidates -= set(ROLE_HARD_DENIES[role])
|
|
|
+
|
|
|
+ return ResolvedAgentPolicy(
|
|
|
+ role=role,
|
|
|
+ effective_tools=frozenset(candidates),
|
|
|
+ max_iterations=min(int(config.max_iterations), int(preset.max_iterations)),
|
|
|
+ temperature=preset.temperature if preset.temperature is not None else float(config.temperature),
|
|
|
+ skills=config.skills if config.skills is not None else preset.skills,
|
|
|
+ system_prompt=config.system_prompt or preset.system_prompt,
|
|
|
+ completion_policy=policy,
|
|
|
+ )
|
|
|
+
|
|
|
+ def authorize(
|
|
|
+ self,
|
|
|
+ role: AgentRole,
|
|
|
+ tool_name: str,
|
|
|
+ resolved_policy: ResolvedAgentPolicy,
|
|
|
+ ) -> AuthorizationResult:
|
|
|
+ if tool_name in ROLE_HARD_DENIES[role]:
|
|
|
+ return AuthorizationResult(False, f"Tool '{tool_name}' is forbidden for role '{role.value}'")
|
|
|
+ if tool_name not in resolved_policy.effective_tools:
|
|
|
+ return AuthorizationResult(False, f"Tool '{tool_name}' is not authorized for role '{role.value}'")
|
|
|
+ return AuthorizationResult(True)
|
|
|
+
|
|
|
+
|
|
|
+__all__ = [
|
|
|
+ "AuthorizationResult", "ResolvedAgentPolicy", "DefaultToolPolicy",
|
|
|
+ "PLANNER_TOOLS", "WORKER_TOOLS", "VALIDATOR_TOOLS", "ROLE_HARD_DENIES",
|
|
|
+]
|