Initial commit

This commit is contained in:
LZH-YS1998
2026-07-01 17:56:31 +08:00
commit d78931979d
731 changed files with 311088 additions and 0 deletions
+5
View File
@@ -0,0 +1,5 @@
"""Native Runtime V2 exports."""
from .runtime import NativeRuntimeV2
__all__ = ["NativeRuntimeV2"]
+823
View File
@@ -0,0 +1,823 @@
"""Permission helpers for Native Runtime V2."""
from __future__ import annotations
import json
import os
import re
import shlex
from pathlib import Path
from typing import Any
from opc.core.config import PermissionsV2Config, get_opc_home
from opc.core.models import PermissionResolution, PermissionScope, RiskLevel, RuntimePermissionDecision
from opc.llm.retry import LLMRetryError, call_llm_json_with_retry
from opc.layer2_organization.data_acquisition_policy import (
ACQUISITION_SHELL_PREFIXES,
is_projection_scoped_acquisition_shell_command,
)
from opc.layer4_tools.registry import ToolDefinition
_DEFAULT_PATH_KEYS = (
"path",
"file_path",
"directory",
"working_directory",
"target_output_dir",
"workspace_path",
)
_DEFAULT_COMMAND_KEYS = ("command", "cmd")
_DEFAULT_URL_KEYS = ("url",)
_READ_ONLY_PREFIXES = {
"cat",
"echo",
"find",
"git diff",
"git log",
"git show",
"git status",
"head",
"ls",
"node -v",
"npm -v",
"pwd",
"python -V",
"python3 -V",
"rg",
"tail",
"wc",
}
_RISKY_SHELL_KEYWORDS = (
"curl ",
"wget ",
"invoke-webrequest",
"invoke-restmethod",
"mv ",
"cp ",
"rm ",
"del ",
"remove-item",
"git commit",
"git push",
"npm install",
"pip install",
"pnpm install",
"cargo test",
"pytest",
"tee ",
"sed -i",
">",
">>",
)
_ACQUISITION_SHELL_PREFIXES = {str(item) for item in ACQUISITION_SHELL_PREFIXES}
_ANY_GRANT_VALUE = "*"
class ToolPermissionResolver:
"""Runtime permission gate with persisted session/project/global grants."""
def __init__(
self,
config: PermissionsV2Config | None = None,
*,
store: Any = None,
runtime_session_id: str = "",
project_id: str = "default",
llm: Any | None = None,
) -> None:
self.config = config or PermissionsV2Config()
self.store = store
self.runtime_session_id = runtime_session_id
self.project_id = project_id or "default"
self.llm = llm
self._loaded = False
self._session_grants: set[tuple[str, str, str, str, str]] = set()
self._project_grants: set[tuple[str, str, str, str, str]] = set()
self._global_grants: set[tuple[str, str, str, str, str]] = set()
self._denial_counts: dict[str, int] = {}
async def warmup(self) -> None:
if self._loaded or not self.store or not hasattr(self.store, "list_runtime_permission_grants"):
self._loaded = True
return
session_rows = await self.store.list_runtime_permission_grants(
runtime_session_id=self.runtime_session_id or None,
scopes=["session"],
)
project_rows = await self.store.list_runtime_permission_grants(
project_id=self.project_id,
scopes=["project"],
)
global_rows = await self.store.list_runtime_permission_grants(scopes=["global"])
self._session_grants = {self._grant_key_from_row(row) for row in session_rows}
self._project_grants = {self._grant_key_from_row(row) for row in project_rows}
self._global_grants = {self._grant_key_from_row(row) for row in global_rows}
self._loaded = True
def _candidate_extractors(self) -> tuple[tuple[str, ...], tuple[str, ...], tuple[str, ...]]:
keys = [str(item or "").strip() for item in self.config.candidate_extractors if str(item or "").strip()]
if not keys:
keys = [*_DEFAULT_PATH_KEYS, *_DEFAULT_COMMAND_KEYS, *_DEFAULT_URL_KEYS]
path_keys = tuple(item for item in keys if item in _DEFAULT_PATH_KEYS)
command_keys = tuple(item for item in keys if item in _DEFAULT_COMMAND_KEYS)
url_keys = tuple(item for item in keys if item in _DEFAULT_URL_KEYS)
return (
path_keys or _DEFAULT_PATH_KEYS,
command_keys or _DEFAULT_COMMAND_KEYS,
url_keys or _DEFAULT_URL_KEYS,
)
def _grant_key_from_row(self, row: dict[str, Any]) -> tuple[str, str, str, str, str]:
tool_name = str(row.get("tool_name", "") or "").strip()
candidate = str(row.get("candidate", "") or "").strip()
metadata = dict(row.get("metadata", {}) or {})
sandbox_mode = str(metadata.get("sandbox_mode", "") or "").strip() or _ANY_GRANT_VALUE
allow_network = str(metadata.get("allow_network", "") or "").strip().lower() or _ANY_GRANT_VALUE
workspace_class = str(metadata.get("workspace_class", "") or "").strip() or _ANY_GRANT_VALUE
return self._grant_key(
tool_name,
candidate,
sandbox_mode=sandbox_mode,
allow_network=allow_network,
workspace_class=workspace_class,
)
def _grant_key(
self,
tool_name: str,
candidate: str,
*,
sandbox_mode: str,
allow_network: str,
workspace_class: str,
) -> tuple[str, str, str, str, str]:
normalized = candidate.strip() or _ANY_GRANT_VALUE
return (
tool_name,
normalized,
sandbox_mode.strip() or _ANY_GRANT_VALUE,
allow_network.strip().lower() or _ANY_GRANT_VALUE,
workspace_class.strip() or _ANY_GRANT_VALUE,
)
def _candidate(self, arguments: dict[str, Any] | None = None) -> str:
if not arguments:
return _ANY_GRANT_VALUE
path_keys, command_keys, url_keys = self._candidate_extractors()
for key in (*path_keys, *command_keys, *url_keys):
value = str(arguments.get(key, "") or "").strip()
if value:
return value
return _ANY_GRANT_VALUE
def _grant_context(self, task: Any = None) -> tuple[str, str, str]:
sandbox_mode = _ANY_GRANT_VALUE
allow_network = _ANY_GRANT_VALUE
workspace_class = _ANY_GRANT_VALUE
if task is None:
return sandbox_mode, allow_network, workspace_class
metadata = getattr(task, "metadata", {}) or {}
execution_context = dict(metadata.get("_execution_context", {}) or {})
sandbox = dict(execution_context.get("sandbox", {}) or {})
sandbox_mode = str(sandbox.get("mode", "") or "").strip() or _ANY_GRANT_VALUE
allow_network = str(bool(sandbox.get("allow_network", True))).lower()
workspace_root = (
str(execution_context.get("workspace_root", "") or "").strip()
or str(metadata.get("workspace_root", "") or "").strip()
or str(metadata.get("comms_workspace_root", "") or "").strip()
or str(metadata.get("target_output_dir", "") or "").strip()
)
workspace_class = "workspace" if workspace_root else "default"
return sandbox_mode, allow_network, workspace_class
@staticmethod
def _risk(value: Any, default: RiskLevel) -> RiskLevel:
try:
return RiskLevel(str(value or default.value))
except Exception:
return default
@staticmethod
def _looks_like_shell_tool(tool_name: str) -> bool:
return tool_name in {"shell_exec", "python_exec", "git_commit"}
def _normalized_tool_set(self, values: list[str]) -> set[str]:
return {str(item or "").strip() for item in values if str(item or "").strip()}
def _matches_path_rule(self, candidate: str, rules: list[str]) -> bool:
if not candidate or candidate == "*":
return False
raw = str(candidate).strip()
for rule in rules:
token = str(rule or "").strip()
if not token:
continue
if token == "*" or raw == token:
return True
try:
rule_path = Path(token).resolve()
candidate_path = Path(raw).resolve()
except Exception:
if raw.startswith(token.rstrip("\\/")):
return True
continue
if candidate_path == rule_path or rule_path in candidate_path.parents:
return True
return False
def _workspace_paths(self, task: Any = None) -> list[Path]:
roots: list[Path] = []
metadata = getattr(task, "metadata", {}) or {} if task else {}
for raw in (
str(metadata.get("workspace_root", "") or "").strip(),
str(metadata.get("comms_workspace_root", "") or "").strip(),
str(metadata.get("output_root", "") or "").strip(),
str(metadata.get("target_output_dir", "") or "").strip(),
):
if not raw:
continue
try:
path = Path(raw).resolve()
except Exception:
continue
if path not in roots:
roots.append(path)
try:
memory_root = (Path(get_opc_home()) / "memory").resolve()
if memory_root not in roots:
roots.append(memory_root)
except Exception:
pass
if not roots:
try:
roots.append(Path.cwd().resolve())
except Exception:
pass
return roots
def _path_decision(
self,
tool: ToolDefinition,
arguments: dict[str, Any] | None,
task: Any = None,
) -> RuntimePermissionDecision | None:
if not arguments:
return None
path_keys, _, _ = self._candidate_extractors()
candidate = ""
for key in path_keys:
value = str(arguments.get(key, "") or "").strip()
if value:
candidate = value
break
if not candidate:
return None
if self._matches_path_rule(candidate, self.config.denied_paths):
return RuntimePermissionDecision(
resolution=PermissionResolution.DENY,
scope=PermissionScope.ONCE,
risk_level=RiskLevel.HIGH,
rationale="Target path matches a denied runtime permission rule.",
source="permission_rules",
)
if self._matches_path_rule(candidate, self.config.allowed_paths):
return RuntimePermissionDecision(
resolution=PermissionResolution.ALLOW,
scope=PermissionScope.PROJECT,
risk_level=RiskLevel.LOW,
rationale="Target path matches an explicit runtime allow rule.",
source="permission_rules",
)
try:
resolved = Path(candidate).resolve()
except Exception:
return None
if tool.read_only:
return None
for root in self._workspace_paths(task):
if resolved == root or root in resolved.parents:
return None
risk = RiskLevel.HIGH if self.config.sandbox_policy.treat_external_paths_as_high_risk else RiskLevel.MEDIUM
return RuntimePermissionDecision(
resolution=PermissionResolution.ASK if self.config.fail_closed else PermissionResolution.DENY,
scope=PermissionScope.ONCE,
risk_level=risk,
rationale="Target path is outside the current runtime workspace roots.",
source="path_guard",
metadata={"candidate": candidate},
)
def _split_command_prefix(self, command: str) -> str:
text = str(command or "").strip()
if not text:
return ""
try:
parts = shlex.split(text, posix=os.name != "nt")
except Exception:
parts = text.split()
if not parts:
return ""
if len(parts) >= 2:
return f"{parts[0]} {parts[1]}".strip()
return parts[0]
def _matches_command_prefix(self, command: str, prefixes: list[str]) -> bool:
raw = str(command or "").strip()
prefix = self._split_command_prefix(raw)
candidates = {raw, prefix}
for item in prefixes:
token = str(item or "").strip()
if not token:
continue
if raw == token or prefix == token:
return True
if raw.startswith(f"{token} ") or prefix.startswith(f"{token} "):
return True
return False
def _split_shell_command_segments(self, command: str) -> list[list[str]]:
text = str(command or "").replace("\r\n", "\n").replace("\n", " ; ").strip()
if not text:
return []
try:
lexer = shlex.shlex(text, posix=os.name != "nt", punctuation_chars=";&|")
lexer.whitespace_split = True
lexer.commenters = ""
tokens = list(lexer)
except Exception:
try:
tokens = shlex.split(text, posix=os.name != "nt")
except Exception:
tokens = text.split()
segments: list[list[str]] = []
current: list[str] = []
for token in tokens:
if token in {"&&", "||", ";", "|", "&"}:
if current:
segments.append(current)
current = []
continue
current.append(token)
if current:
segments.append(current)
return segments
def _command_has_redirection(self, command: str) -> bool:
text = str(command or "").replace("\r\n", "\n").replace("\n", " ; ").strip()
if not text:
return False
try:
lexer = shlex.shlex(text, posix=os.name != "nt", punctuation_chars=";&|<>")
lexer.whitespace_split = True
lexer.commenters = ""
tokens = list(lexer)
except Exception:
return any(marker in text for marker in (">", "<"))
return any(token in {">", ">>", "<", "<<"} for token in tokens)
def _matches_safe_shell_prefix(self, command: str, prefixes: list[str]) -> bool:
cleaned = " ".join(str(command or "").split()).strip()
if not cleaned or self._command_has_redirection(cleaned):
return False
segments = self._split_shell_command_segments(cleaned)
if len(segments) != 1:
return False
return self._matches_command_prefix(" ".join(segments[0]).strip(), prefixes)
def _shell_ast_reason(self, command: str) -> tuple[RiskLevel, str] | None:
lowered = str(command or "").strip().lower()
if not lowered:
return None
if lowered in _READ_ONLY_PREFIXES or self._matches_command_prefix(lowered, list(_READ_ONLY_PREFIXES)):
return RiskLevel.LOW, "Command matches a read-only shell prefix."
for keyword in _RISKY_SHELL_KEYWORDS:
if keyword in lowered:
risk = RiskLevel.HIGH
if keyword in {"curl ", "wget ", "invoke-webrequest", "invoke-restmethod"} and self.config.sandbox_policy.treat_network_as_risky:
risk = RiskLevel.CRITICAL
return risk, f"Command contains risky shell operation `{keyword.strip()}`."
return RiskLevel.MEDIUM, "Shell AST classifier could not prove the command is read-only."
def _shell_decision(
self,
tool: ToolDefinition,
arguments: dict[str, Any] | None,
*,
task: Any = None,
) -> RuntimePermissionDecision | None:
if not self._looks_like_shell_tool(tool.name) or not arguments:
return None
_, command_keys, _ = self._candidate_extractors()
command = ""
for key in command_keys:
value = str(arguments.get(key, "") or "").strip()
if value:
command = value
break
if not command:
return None
projection_scoped_low_risk = is_projection_scoped_acquisition_shell_command(
command=command,
task=task,
working_directory=str(arguments.get("working_directory", "") or arguments.get("workdir", "") or "").strip(),
target_output_dir=str(getattr(task, "metadata", {}).get("target_output_dir", "") or "").strip() if task else "",
)
if projection_scoped_low_risk:
return RuntimePermissionDecision(
resolution=PermissionResolution.ALLOW,
scope=PermissionScope.ONCE,
risk_level=RiskLevel.LOW,
rationale="Command matches a work-item-scoped acquisition prefix inside the assigned workspace.",
source="shell_prefix",
metadata={"candidate": command},
)
for pattern in self.config.dangerous_shell_patterns:
if pattern and re.search(pattern, command, flags=re.IGNORECASE):
return RuntimePermissionDecision(
resolution=PermissionResolution.ASK,
scope=PermissionScope.ONCE,
risk_level=RiskLevel.CRITICAL,
rationale=f"Command matched dangerous shell pattern `{pattern}`.",
source="shell_pattern",
metadata={"candidate": command},
)
filtered_safe_prefixes = [
item for item in self.config.safe_shell_prefixes
if str(item or "").strip() not in _ACQUISITION_SHELL_PREFIXES
]
if self._matches_safe_shell_prefix(command, filtered_safe_prefixes):
return RuntimePermissionDecision(
resolution=PermissionResolution.ALLOW,
scope=PermissionScope.ONCE,
risk_level=RiskLevel.LOW,
rationale="Command matches a safe shell prefix.",
source="shell_prefix",
metadata={"candidate": command},
)
if self._matches_command_prefix(command, self.config.ask_shell_prefixes):
return RuntimePermissionDecision(
resolution=PermissionResolution.ASK,
scope=PermissionScope.ONCE,
risk_level=RiskLevel.MEDIUM,
rationale="Command matches an ask-first shell prefix.",
source="shell_prefix",
metadata={"candidate": command},
)
if self.config.shell_ast_validation:
risk, rationale = self._shell_ast_reason(command) or (RiskLevel.MEDIUM, "Shell command requires manual review.")
return RuntimePermissionDecision(
resolution=PermissionResolution.ALLOW if risk == RiskLevel.LOW else PermissionResolution.ASK,
scope=PermissionScope.ONCE,
risk_level=risk,
rationale=rationale,
source="shell_ast",
metadata={"candidate": command},
)
if tool.requires_confirmation or self.config.fail_closed:
return RuntimePermissionDecision(
resolution=PermissionResolution.ASK,
scope=PermissionScope.ONCE,
risk_level=RiskLevel.HIGH if not tool.read_only else RiskLevel.MEDIUM,
rationale="Shell command requires explicit approval under runtime_v2.",
source="shell_guard",
metadata={"candidate": command},
)
return None
def _candidate_matches(self, candidate: str, granted_candidate: str) -> bool:
if granted_candidate == _ANY_GRANT_VALUE:
return True
if candidate == granted_candidate:
return True
return candidate.startswith(granted_candidate.rstrip("\\/"))
@staticmethod
def _sandbox_rank(mode: str) -> int:
return {
"workspace-write": 1,
"elevated": 2,
"off": 3,
}.get(str(mode or "").strip().lower(), 0)
def _match_grant(self, grants: set[tuple[str, str, str, str, str]], tool_name: str, candidate: str, *, task: Any = None) -> bool:
if not grants:
return False
sandbox_mode, allow_network, workspace_class = self._grant_context(task)
for grant_tool, grant_candidate, grant_sandbox_mode, grant_allow_network, grant_workspace_class in grants:
if grant_tool != tool_name:
continue
if not self._candidate_matches(candidate, grant_candidate):
continue
if grant_sandbox_mode not in {_ANY_GRANT_VALUE, sandbox_mode}:
if not (
self.config.guardian.cache_upgrade_context
and self._sandbox_rank(sandbox_mode) >= self._sandbox_rank(grant_sandbox_mode)
):
continue
if grant_allow_network not in {_ANY_GRANT_VALUE, allow_network}:
continue
if grant_workspace_class not in {_ANY_GRANT_VALUE, workspace_class}:
continue
return True
return False
def _denial_memory_key(self, tool_name: str, arguments: dict[str, Any] | None) -> str:
return f"{tool_name}:{self._candidate(arguments)}"
def record_denial(self, tool_name: str, arguments: dict[str, Any] | None) -> None:
if not self.config.denial_memory.enabled:
return
key = self._denial_memory_key(tool_name, arguments)
self._denial_counts[key] = self._denial_counts.get(key, 0) + 1
def _repeat_denial_decision(self, tool_name: str, arguments: dict[str, Any] | None) -> RuntimePermissionDecision | None:
if not self.config.denial_memory.enabled:
return None
key = self._denial_memory_key(tool_name, arguments)
repeats = self._denial_counts.get(key, 0)
if repeats < max(1, self.config.denial_memory.repeat_threshold):
return None
return RuntimePermissionDecision(
resolution=PermissionResolution.DENY,
scope=PermissionScope.ONCE,
risk_level=RiskLevel.HIGH,
rationale="Repeated denial memory indicates this action should stop and ask for a new plan.",
source="denial_memory",
metadata={"repeated_denials": repeats},
)
def build_blocked_result(
self,
decision: RuntimePermissionDecision,
*,
tool_name: str,
arguments: dict[str, Any] | None = None,
) -> dict[str, Any]:
action = "reject" if decision.resolution == PermissionResolution.DENY else "require_input"
candidate = self._candidate(arguments)
return {
"error": decision.rationale or f"Runtime permission blocked `{tool_name}`.",
"success": False,
"approval": {
"action": action,
"risk_level": decision.risk_level.value,
"policy_source": decision.source,
"scope": decision.scope.value,
"candidate": candidate,
"explanation": decision.rationale,
"metadata": dict(decision.metadata or {}),
},
"permission_context": {
"tool_name": tool_name,
"candidate": candidate,
"resolution": decision.resolution.value,
"risk_level": decision.risk_level.value,
"source": decision.source,
},
}
def predicted_decision(
self,
tool: ToolDefinition | None,
arguments: dict[str, Any] | None = None,
*,
task: Any = None,
) -> RuntimePermissionDecision:
if tool is None:
return RuntimePermissionDecision(
resolution=PermissionResolution.ASK if self.config.fail_closed else PermissionResolution.DENY,
scope=PermissionScope.ONCE,
risk_level=RiskLevel.HIGH,
rationale="Unknown tool requires manual review.",
source="runtime_prediction",
)
tool_name = tool.name
candidate = self._candidate(arguments)
repeated_denial = self._repeat_denial_decision(tool_name, arguments)
if repeated_denial is not None:
return repeated_denial
if tool_name in self._normalized_tool_set(self.config.deny_tools):
return RuntimePermissionDecision(
resolution=PermissionResolution.DENY,
scope=PermissionScope.ONCE,
risk_level=RiskLevel.HIGH,
rationale="Tool is explicitly denied by runtime permission rules.",
source="permission_rules",
)
if self._match_grant(self._session_grants, tool_name, candidate, task=task):
return RuntimePermissionDecision(
resolution=PermissionResolution.ALLOW,
scope=PermissionScope.SESSION,
risk_level=RiskLevel.LOW,
rationale="Allowed by runtime session grant.",
source="runtime_session_grant",
)
if self._match_grant(self._project_grants, tool_name, candidate, task=task):
return RuntimePermissionDecision(
resolution=PermissionResolution.ALLOW,
scope=PermissionScope.PROJECT,
risk_level=RiskLevel.LOW,
rationale="Allowed by persisted project grant.",
source="runtime_project_grant",
)
if self._match_grant(self._global_grants, tool_name, candidate, task=task):
return RuntimePermissionDecision(
resolution=PermissionResolution.ALLOW,
scope=PermissionScope.GLOBAL,
risk_level=RiskLevel.LOW,
rationale="Allowed by persisted global grant.",
source="runtime_global_grant",
)
if tool_name in self._normalized_tool_set(self.config.allow_tools):
return RuntimePermissionDecision(
resolution=PermissionResolution.ALLOW,
scope=PermissionScope.PROJECT,
risk_level=RiskLevel.LOW,
rationale="Tool is explicitly allowed by runtime permission rules.",
source="permission_rules",
)
path_decision = self._path_decision(tool, arguments, task=task)
if path_decision is not None:
return path_decision
shell_decision = self._shell_decision(tool, arguments, task=task)
if shell_decision is not None:
return shell_decision
if tool.requires_confirmation:
return RuntimePermissionDecision(
resolution=PermissionResolution.ASK,
scope=PermissionScope.ONCE,
risk_level=RiskLevel.MEDIUM,
rationale="Tool is marked as requiring confirmation.",
source="runtime_prediction",
metadata={"candidate": candidate},
)
if self.config.guardian.enabled and self.config.guardian.auto_allow_read_only and tool.read_only:
return RuntimePermissionDecision(
resolution=PermissionResolution.ALLOW,
scope=PermissionScope.ONCE,
risk_level=RiskLevel.LOW,
rationale="Guardian pre-check marked the tool as deterministic read-only.",
source="guardian",
metadata={"candidate": candidate},
)
return RuntimePermissionDecision(
resolution=PermissionResolution.ALLOW,
scope=PermissionScope.ONCE,
risk_level=RiskLevel.LOW,
rationale="No runtime permission warning triggered.",
source="runtime_prediction",
metadata={"candidate": candidate},
)
async def refine_decision(
self,
decision: RuntimePermissionDecision,
*,
tool: ToolDefinition | None,
arguments: dict[str, Any] | None,
task: Any = None,
) -> RuntimePermissionDecision:
if decision.resolution != PermissionResolution.ASK:
return decision
if not self.config.classifier_enabled or not self.config.llm_classifier_model or self.llm is None or tool is None:
return decision
payload = {
"tool_name": tool.name,
"arguments": arguments or {},
"candidate": self._candidate(arguments),
"project_id": getattr(task, "project_id", self.project_id),
"heuristic_rationale": decision.rationale,
}
def _validate_classifier(parsed: Any) -> str | None:
if not isinstance(parsed, dict):
return "Top-level response must be a JSON object."
try:
score_val = float(parsed.get("score", 0.5) or 0.5)
except (TypeError, ValueError):
return "`score` must be a number between 0 and 1."
if score_val < 0 or score_val > 1:
return "`score` must be a number between 0 and 1."
return None
try:
parsed = await call_llm_json_with_retry(
self.llm,
system=(
"You are a runtime permission classifier.\n"
"Return strict JSON with keys `score` and `reason`.\n"
"`score` is a float between 0 and 1 where 0 is clearly safe and 1 is clearly unsafe.\n"
"Classify file mutation, shell execution, path escape risk, and network side effects conservatively."
),
payload=payload,
task_type="quick_tasks",
validator=_validate_classifier,
label="runtime_permission_classifier",
)
except LLMRetryError:
return decision
score = float(parsed.get("score", 0.5) or 0.5)
reason = str(parsed.get("reason", "") or decision.rationale)
thresholds = self.config.classifier_thresholds
if score <= thresholds.allow:
return RuntimePermissionDecision(
resolution=PermissionResolution.ALLOW,
scope=decision.scope,
risk_level=RiskLevel.LOW,
rationale=reason or "Classifier marked the action safe.",
source="llm_classifier",
metadata={**dict(decision.metadata or {}), "classifier_score": score},
)
if score >= thresholds.deny:
return RuntimePermissionDecision(
resolution=PermissionResolution.DENY,
scope=decision.scope,
risk_level=RiskLevel.HIGH,
rationale=reason or "Classifier marked the action unsafe.",
source="llm_classifier",
metadata={**dict(decision.metadata or {}), "classifier_score": score},
)
return RuntimePermissionDecision(
resolution=PermissionResolution.ASK,
scope=decision.scope,
risk_level=RiskLevel.MEDIUM if score < thresholds.ask else RiskLevel.HIGH,
rationale=reason or decision.rationale,
source="llm_classifier",
metadata={**dict(decision.metadata or {}), "classifier_score": score},
)
def decision_from_result(
self,
tool_name: str,
arguments: dict[str, Any] | None,
result: dict[str, Any],
) -> RuntimePermissionDecision:
approval = dict(result.get("approval", {}) or {})
action = str(approval.get("action", "") or "").strip().lower()
if action in {"require_input", "escalate"}:
return RuntimePermissionDecision(
resolution=PermissionResolution.ASK,
scope=PermissionScope.ONCE,
risk_level=self._risk(approval.get("risk_level"), RiskLevel.MEDIUM),
rationale=str(result.get("error", "") or "Awaiting explicit permission."),
source="approval_engine",
metadata=approval,
)
if action == "reject":
self.record_denial(tool_name, arguments)
return RuntimePermissionDecision(
resolution=PermissionResolution.DENY,
scope=PermissionScope.ONCE,
risk_level=self._risk(approval.get("risk_level"), RiskLevel.HIGH),
rationale=str(result.get("error", "") or "Permission denied."),
source="approval_engine",
metadata=approval,
)
human_reply = str(approval.get("human_reply") or result.get("human_reply") or "").strip().lower()
candidate = self._candidate(arguments)
grant = self._grant_key(
tool_name,
candidate,
sandbox_mode=_ANY_GRANT_VALUE,
allow_network=_ANY_GRANT_VALUE,
workspace_class=_ANY_GRANT_VALUE,
)
if human_reply == "approve_session":
self._session_grants.add(grant)
return RuntimePermissionDecision(
resolution=PermissionResolution.ALLOW,
scope=PermissionScope.SESSION,
risk_level=RiskLevel.LOW,
rationale="Approved for this runtime session.",
source="human_escalation",
metadata=approval,
)
if human_reply == "always_project":
self._project_grants.add(grant)
return RuntimePermissionDecision(
resolution=PermissionResolution.ALLOW,
scope=PermissionScope.PROJECT,
risk_level=RiskLevel.LOW,
rationale="Approved for this project.",
source="human_escalation",
metadata=approval,
)
if human_reply == "always_global":
self._global_grants.add(grant)
return RuntimePermissionDecision(
resolution=PermissionResolution.ALLOW,
scope=PermissionScope.GLOBAL,
risk_level=RiskLevel.LOW,
rationale="Approved globally.",
source="human_escalation",
metadata=approval,
)
return RuntimePermissionDecision(
resolution=PermissionResolution.ALLOW,
scope=PermissionScope.ONCE,
risk_level=RiskLevel.LOW,
rationale="Tool execution allowed.",
source="approval_engine",
metadata=approval,
)
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,514 @@
"""Streaming-friendly tool executor for Native Runtime V2."""
from __future__ import annotations
import asyncio
import json
import time
import uuid
from typing import Any, Awaitable, Callable
from opc.core.models import PermissionResolution
from opc.layer3_agent.runtime_v2.permissions import ToolPermissionResolver
from opc.layer3_agent.runtime_v2.tool_hooks import RuntimeToolHookBus, RuntimeToolHookContext
from opc.layer3_agent.runtime_v2.tool_planner import ToolBatch, ToolPlanner
from opc.layer4_tools.registry import ToolRegistry
RuntimeToolHandler = Callable[[str, dict[str, Any]], Awaitable[dict[str, Any]]]
RuntimeEventCallback = Callable[[str, dict[str, Any]], Awaitable[None]]
_HEARTBEAT_INTERVAL_SECONDS = 1.0
def _now_ms() -> int:
return int(time.time() * 1000)
def _result_summary(result: dict[str, Any], *, limit: int = 240) -> str:
if not result:
return ""
if result.get("error"):
summary = str(result.get("error", "") or "").strip()
else:
payload = result.get("result", {})
summary = ""
if isinstance(payload, dict):
for key in ("summary", "rendered", "stdout", "stderr", "content", "message"):
value = payload.get(key)
if value:
summary = str(value).strip()
break
if not summary:
summary = json.dumps(result, ensure_ascii=False, default=str)
if len(summary) <= limit:
return summary
return summary[:limit].rstrip() + "..."
class StreamingToolExecutor:
def __init__(
self,
*,
registry: ToolRegistry,
planner: ToolPlanner,
permission_resolver: ToolPermissionResolver,
hook_bus: RuntimeToolHookBus | None = None,
runtime_tool_handler: RuntimeToolHandler | None = None,
emit_event: RuntimeEventCallback | None = None,
max_parallel_read_tools: int = 6,
converge_on_parallel_failure: bool = True,
) -> None:
self.registry = registry
self.planner = planner
self.permission_resolver = permission_resolver
self.hook_bus = hook_bus
self.runtime_tool_handler = runtime_tool_handler
self.emit_event = emit_event
self.max_parallel_read_tools = max(1, int(max_parallel_read_tools or 1))
self.converge_on_parallel_failure = bool(converge_on_parallel_failure)
async def execute(
self,
tool_calls: list[dict[str, Any]],
*,
task: Any = None,
on_progress: Any = None,
) -> list[dict[str, Any]]:
ordered_results: list[dict[str, Any]] = []
for batch in self.planner.partition(tool_calls):
batch_id = f"tb_{uuid.uuid4().hex[:12]}"
batch_started_at_ms = _now_ms()
if self.emit_event:
await self.emit_event(
"tool_batch_started",
{
"batch_id": batch_id,
"started_at_ms": batch_started_at_ms,
"concurrency_safe": batch.concurrency_safe,
"tool_names": [str(call.get("function", "") or "") for call in batch.calls],
"tool_call_ids": [str(call.get("id", "") or "") for call in batch.calls],
},
)
if batch.concurrency_safe:
batch_results = await self._run_parallel(batch, task=task, on_progress=on_progress, batch_id=batch_id)
else:
batch_results: list[dict[str, Any]] = []
for call in batch.calls:
batch_results.append(await self._run_one(call, task=task, on_progress=on_progress, batch_id=batch_id))
ordered_results.extend(batch_results)
if self.emit_event:
batch_completed_at_ms = _now_ms()
await self.emit_event(
"tool_batch_completed",
{
"batch_id": batch_id,
"started_at_ms": batch_started_at_ms,
"completed_at_ms": batch_completed_at_ms,
"elapsed_ms": max(0, batch_completed_at_ms - batch_started_at_ms),
"concurrency_safe": batch.concurrency_safe,
"success": all(bool(item.get("result", {}).get("success", True)) for item in batch_results),
"tool_count": len(batch_results),
},
)
return ordered_results
async def _run_parallel(
self,
batch: ToolBatch,
*,
task: Any = None,
on_progress: Any = None,
batch_id: str = "",
) -> list[dict[str, Any]]:
semaphore = asyncio.Semaphore(self.max_parallel_read_tools)
batch_state: dict[str, Any] = {
"cascade_event": asyncio.Event(),
"failed_call_id": "",
"failed_tool_name": "",
}
async def _wrapped(call: dict[str, Any]) -> dict[str, Any]:
async with semaphore:
if self.converge_on_parallel_failure and batch_state["cascade_event"].is_set():
return await self._build_converged_result(call, batch_state, batch_id=batch_id)
result = await self._run_one(call, task=task, on_progress=on_progress, batch_state=batch_state, batch_id=batch_id)
if self.converge_on_parallel_failure and self._should_converge_batch(result):
batch_state["failed_call_id"] = str(call.get("id", "") or "")
batch_state["failed_tool_name"] = str(call.get("function", "") or "")
batch_state["cascade_event"].set()
return result
return list(await asyncio.gather(*[_wrapped(call) for call in batch.calls]))
async def _run_one(
self,
call: dict[str, Any],
*,
task: Any = None,
on_progress: Any = None,
batch_state: dict[str, Any] | None = None,
batch_id: str = "",
) -> dict[str, Any]:
tool_name = str(call.get("function", "") or "")
arguments = dict(call.get("arguments", {}) or {})
tool = self.registry.get(tool_name)
predicted = self.permission_resolver.predicted_decision(tool, arguments, task=task)
started_at_ms = _now_ms()
started_at_monotonic = time.monotonic()
if self.emit_event:
await self.emit_event(
"permission_predicted",
{
"batch_id": batch_id,
"tool_call_id": call.get("id", ""),
"tool_name": tool_name,
"arguments": arguments,
"resolution": predicted.resolution.value,
"scope": predicted.scope.value,
"risk_level": predicted.risk_level.value,
"rationale": predicted.rationale,
"source": predicted.source,
"started_at_ms": started_at_ms,
},
)
if self.emit_event and predicted.resolution != PermissionResolution.ALLOW:
await self.emit_event(
"permission_requested",
{
"batch_id": batch_id,
"tool_call_id": call.get("id", ""),
"tool_name": tool_name,
"arguments": arguments,
"resolution": predicted.resolution.value,
"scope": predicted.scope.value,
"risk_level": predicted.risk_level.value,
"rationale": predicted.rationale,
"source": predicted.source,
},
)
if self.emit_event:
await self.emit_event(
"tool_started",
{
"batch_id": batch_id,
"tool_call_id": call.get("id", ""),
"tool_name": tool_name,
"arguments": arguments,
"predicted_permission": predicted.resolution.value,
"started_at_ms": started_at_ms,
},
)
if batch_state is not None and self.converge_on_parallel_failure and batch_state["cascade_event"].is_set():
return await self._build_converged_result(call, batch_state, batch_id=batch_id)
hook_context = RuntimeToolHookContext(
phase="pre",
tool_name=tool_name,
call=call,
task=task,
tool=tool,
arguments=dict(arguments),
predicted_permission=predicted,
)
if self.hook_bus is not None:
hook_context = await self.hook_bus.run_pre_hooks(hook_context)
arguments = dict(hook_context.arguments)
elif predicted.resolution == PermissionResolution.DENY:
hook_context.result = self.permission_resolver.build_blocked_result(
predicted,
tool_name=tool_name,
arguments=arguments,
)
hook_context.state["stop_batch_on_failure"] = True
if hook_context.result is not None:
result = dict(hook_context.result)
decision = self.permission_resolver.decision_from_result(tool_name, arguments, result)
elif call.get("arguments_parse_error"):
result = {
"error": str(call.get("arguments_parse_error", "")),
"invalid_arguments": True,
"success": False,
"raw_arguments": str(call.get("arguments_raw", "")),
}
decision = self.permission_resolver.decision_from_result(tool_name, arguments, result)
else:
last_progress: dict[str, str] = {"stream": "", "text": ""}
last_progress_at = {"value": time.monotonic()}
heartbeat_active = {"value": True}
async def _heartbeat() -> None:
while heartbeat_active["value"]:
await asyncio.sleep(_HEARTBEAT_INTERVAL_SECONDS)
if not heartbeat_active["value"]:
return
now = time.monotonic()
if now - last_progress_at["value"] < _HEARTBEAT_INTERVAL_SECONDS:
continue
if self.emit_event:
await self.emit_event(
"tool_progress",
{
"batch_id": batch_id,
"tool_call_id": call.get("id", ""),
"tool_name": tool_name,
"phase": "running",
"message": f"{tool_name} still running",
"heartbeat": True,
"elapsed_ms": int((now - started_at_monotonic) * 1000),
},
)
async def _tool_progress(progress: Any, **progress_kw: Any) -> None:
if isinstance(progress, dict):
payload = dict(progress)
text = str(payload.get("text", "") or payload.get("message", "") or "").strip()
stream_name = str(payload.get("stream", "") or "").strip()
else:
text = str(progress or "").strip()
stream_name = str(progress_kw.get("stream", "") or "").strip()
payload = {
"text": text,
"stream": stream_name,
}
if not text:
return
if last_progress["text"] == text and last_progress["stream"] == stream_name:
return
last_progress["text"] = text
last_progress["stream"] = stream_name
last_progress_at["value"] = time.monotonic()
if self.emit_event:
await self.emit_event(
"tool_progress",
{
"batch_id": batch_id,
"tool_call_id": call.get("id", ""),
"tool_name": tool_name,
"stream": stream_name,
"elapsed_ms": int((last_progress_at["value"] - started_at_monotonic) * 1000),
**payload,
},
)
if on_progress:
try:
await on_progress(text, task_id=getattr(task, "id", None))
except TypeError:
await on_progress(text)
heartbeat_task = asyncio.create_task(_heartbeat())
try:
if tool is not None and tool.runtime_managed and self.runtime_tool_handler is not None:
result = await self.runtime_tool_handler(tool_name, arguments)
else:
result = await self.registry.execute(
tool_name,
arguments,
task=task,
on_progress=_tool_progress,
skip_approval=True,
)
result = await self._maybe_retry_with_escalated_sandbox(
tool_name=tool_name,
arguments=arguments,
task=task,
result=result,
on_progress=_tool_progress,
batch_id=batch_id,
call=call,
)
finally:
heartbeat_active["value"] = False
heartbeat_task.cancel()
await asyncio.gather(heartbeat_task, return_exceptions=True)
hook_context.phase = "post"
hook_context.arguments = dict(arguments)
hook_context.result = dict(result)
if self.hook_bus is not None:
hook_context = await self.hook_bus.run_post_hooks(hook_context)
result = dict(hook_context.result or result)
if not bool(result.get("success", True)):
hook_context.phase = "failure"
hook_context.result = dict(result)
hook_context = await self.hook_bus.run_failure_hooks(hook_context)
result = dict(hook_context.result or result)
decision = self.permission_resolver.decision_from_result(tool_name, arguments, result)
if self.emit_event:
await self.emit_event(
"permission_resolved",
{
"batch_id": batch_id,
"tool_call_id": call.get("id", ""),
"tool_name": tool_name,
"arguments": arguments,
"resolution": decision.resolution.value,
"scope": decision.scope.value,
"rationale": decision.rationale,
},
)
await self.emit_event(
"tool_completed",
{
"batch_id": batch_id,
"tool_call_id": call.get("id", ""),
"tool_name": tool_name,
"started_at_ms": started_at_ms,
"completed_at_ms": _now_ms(),
"elapsed_ms": int((time.monotonic() - started_at_monotonic) * 1000),
"success": bool(result.get("success", True)),
"result_summary": _result_summary(result),
"result_preview": json.dumps(result, ensure_ascii=False, default=str)[:800],
},
)
return {
"tool_call": call,
"result": result,
"permission_decision": decision,
"stop_batch_on_failure": bool(hook_context.state.get("stop_batch_on_failure")),
"hook_metadata": {"batch_id": batch_id, **dict(hook_context.state.get("metadata", {}))},
}
async def _maybe_retry_with_escalated_sandbox(
self,
*,
tool_name: str,
arguments: dict[str, Any],
task: Any,
result: dict[str, Any],
on_progress: Any,
batch_id: str,
call: dict[str, Any],
) -> dict[str, Any]:
guardian = getattr(self.permission_resolver.config, "guardian", None)
if not guardian or not bool(getattr(guardian, "auto_retry_sandbox", False)):
return result
payload = result.get("result", {})
if not isinstance(payload, dict):
return result
exit_code = payload.get("exit_code")
if bool(result.get("success", True)) and exit_code in (None, 0):
return result
if tool_name not in {"shell_exec", "python_exec"}:
return result
sandbox_meta = dict(payload.get("sandbox", {}) or {})
error_text = str(result.get("error", "") or payload.get("error", "") or "").lower()
if not sandbox_meta and "sandbox" not in error_text:
return result
if task is None:
return result
execution_context = dict((getattr(task, "metadata", {}) or {}).get("_execution_context", {}) or {})
sandbox_context = dict(execution_context.get("sandbox", {}) or {})
current_mode = str(sandbox_context.get("mode", "") or "").strip().lower() or "off"
next_mode = self._next_sandbox_mode(current_mode)
if not next_mode:
return result
original_context = dict(execution_context)
original_sandbox = dict(sandbox_context)
retry_started_at_ms = _now_ms()
if self.emit_event:
await self.emit_event(
"sandbox_retry_requested",
{
"batch_id": batch_id,
"tool_call_id": call.get("id", ""),
"tool_name": tool_name,
"from_mode": current_mode,
"to_mode": next_mode,
"started_at_ms": retry_started_at_ms,
},
)
sandbox_context["mode"] = next_mode
execution_context["sandbox"] = sandbox_context
task.metadata = dict(getattr(task, "metadata", {}) or {})
task.metadata["_execution_context"] = execution_context
try:
if self.registry.get(tool_name) is not None and getattr(self.registry.get(tool_name), "runtime_managed", False) and self.runtime_tool_handler is not None:
retry_result = await self.runtime_tool_handler(tool_name, arguments)
else:
retry_result = await self.registry.execute(
tool_name,
arguments,
task=task,
on_progress=on_progress,
skip_approval=True,
)
finally:
original_context["sandbox"] = original_sandbox
task.metadata["_execution_context"] = original_context
if self.emit_event:
await self.emit_event(
"sandbox_retry_completed",
{
"batch_id": batch_id,
"tool_call_id": call.get("id", ""),
"tool_name": tool_name,
"from_mode": current_mode,
"to_mode": next_mode,
"started_at_ms": retry_started_at_ms,
"completed_at_ms": _now_ms(),
"success": bool(retry_result.get("success", True)),
"result_summary": _result_summary(retry_result),
},
)
return retry_result
@staticmethod
def _next_sandbox_mode(current_mode: str) -> str:
normalized = str(current_mode or "").strip().lower()
if normalized == "workspace-write":
return "elevated"
if normalized == "elevated":
return "off"
return ""
async def _build_converged_result(
self,
call: dict[str, Any],
batch_state: dict[str, Any],
*,
batch_id: str = "",
) -> dict[str, Any]:
tool_name = str(call.get("function", "") or "")
result = {
"error": (
"Skipped because a concurrent sibling tool failed and the runtime converged the batch. "
f"Source: {batch_state.get('failed_tool_name', '') or 'unknown'}"
),
"success": False,
"converged": True,
"converged_from_tool": batch_state.get("failed_tool_name", ""),
"converged_from_call_id": batch_state.get("failed_call_id", ""),
}
if self.emit_event:
await self.emit_event(
"tool_skipped",
{
"batch_id": batch_id,
"tool_call_id": call.get("id", ""),
"tool_name": tool_name,
"reason": "parallel_batch_converged",
"source_tool_name": batch_state.get("failed_tool_name", ""),
"source_call_id": batch_state.get("failed_call_id", ""),
},
)
decision = self.permission_resolver.decision_from_result(tool_name, dict(call.get("arguments", {}) or {}), result)
return {
"tool_call": call,
"result": result,
"permission_decision": decision,
"stop_batch_on_failure": False,
"hook_metadata": {"converged": True, "batch_id": batch_id},
}
@staticmethod
def _should_converge_batch(result: dict[str, Any]) -> bool:
if bool(result.get("stop_batch_on_failure")):
return True
payload = result.get("result", {})
if isinstance(payload, dict) and payload.get("prevent_continuation"):
return True
if isinstance(payload, dict):
return not bool(payload.get("success", True))
return False
+766
View File
@@ -0,0 +1,766 @@
"""Runtime-managed native subagents."""
from __future__ import annotations
import asyncio
import inspect
import time
import uuid
from dataclasses import dataclass, field
from typing import Any, Callable, Awaitable
from opc.core.config import OPCConfig, NativeSubagentProfileConfig
from opc.core.models import OPCEvent, Task, TaskResult, TaskStatus
from opc.layer2_organization.work_item_identity import projection_id_for_task, work_item_identity_payload_for_task
from opc.layer3_agent.runtime_v2.worktree import cleanup_worktree, create_worktree
ChildAgentFactory = Callable[..., Any]
@dataclass
class SubagentState:
agent_id: str
profile: str
name: str = ""
description: str = ""
model: str = ""
mode: str = "default"
isolation: str = "shared"
max_iterations: int = 24
status: str = "running"
background: bool = False
resident: bool = False
fork_mode: bool = False
created_at: float = field(default_factory=time.time)
latest_result: str = ""
pending_messages_count: int = 0
last_notification_kind: str = ""
worktree: dict[str, Any] | None = None
fork_system_prompt: str = ""
fork_context_messages: list[dict[str, Any]] = field(default_factory=list)
fork_allowed_tools: list[str] = field(default_factory=list)
runtime_task: asyncio.Task[Any] | None = None
inbox: asyncio.Queue[dict[str, Any]] = field(default_factory=asyncio.Queue)
completion: asyncio.Event = field(default_factory=asyncio.Event)
update_event: asyncio.Event = field(default_factory=asyncio.Event)
task_result: TaskResult | None = None
class SubagentManager:
def __init__(
self,
*,
parent_task: Task | None,
config: OPCConfig | None,
child_agent_factory: ChildAgentFactory | None,
event_bus: Any = None,
store: Any = None,
runtime_session_id: str = "",
) -> None:
self.parent_task = parent_task
self.config = config or OPCConfig()
self.child_agent_factory = child_agent_factory
self.event_bus = event_bus
self.store = store
self.runtime_session_id = runtime_session_id
self.states: dict[str, SubagentState] = {}
self.agent_names: dict[str, str] = {}
async def spawn(
self,
*,
profile: str,
prompt: str,
background: bool | None = None,
isolation: str | None = None,
description: str = "",
name: str = "",
model: str = "",
mode: str = "default",
fork_context_messages: list[dict[str, Any]] | None = None,
fork_system_prompt: str = "",
fork_allowed_tools: list[str] | None = None,
fork_mode: bool = False,
resident: bool = False,
) -> dict[str, Any]:
if self.child_agent_factory is None:
return {"error": "Native subagent factory is not configured", "success": False}
parent_depth = int(getattr(self.parent_task, "metadata", {}).get("_native_runtime_depth", 0) or 0)
max_depth = int(self.config.system.native_runtime.subagent_max_depth or 3)
if parent_depth >= max_depth:
return {
"error": f"Maximum native subagent depth ({max_depth}) reached",
"success": False,
}
profile_cfg = self._profile_config(profile)
effective_mode = str(mode or "default").strip() or "default"
default_isolation = profile_cfg.default_isolation
if not default_isolation:
default_isolation = "shared" if profile in {"explore", "plan"} else "worktree"
effective_isolation = str(isolation or default_isolation or "shared").strip().lower() or "shared"
if effective_mode == "plan":
effective_isolation = "shared"
if effective_isolation not in {"shared", "worktree"}:
effective_isolation = str(profile_cfg.default_isolation or "shared").strip().lower() or "shared"
effective_background = profile_cfg.background if background is None else bool(background)
effective_resident = bool(resident) and bool(effective_background)
effective_model = str(model or profile_cfg.model or "").strip()
effective_description = str(description or prompt or "").strip()
effective_name = str(name or "").strip()
if effective_name and effective_name in self.agent_names:
existing_state = self.states.get(self.agent_names[effective_name])
if existing_state is not None and not existing_state.completion.is_set():
return {"error": f"Subagent name `{effective_name}` is already in use", "success": False}
agent_id = f"na_{uuid.uuid4().hex[:10]}"
worktree = None
if effective_isolation == "worktree":
base_path = str(getattr(self.parent_task, "metadata", {}).get("target_output_dir", "") or "").strip()
worktree = await create_worktree(base_path or None, config=self.config)
state = SubagentState(
agent_id=agent_id,
profile=profile,
name=effective_name,
description=effective_description,
model=effective_model,
mode=effective_mode,
isolation=effective_isolation,
max_iterations=max(1, int(profile_cfg.max_iterations or 24)),
background=effective_background,
resident=effective_resident,
fork_mode=bool(fork_mode),
worktree=worktree,
fork_system_prompt=str(fork_system_prompt or ""),
fork_context_messages=list(fork_context_messages or []),
fork_allowed_tools=list(fork_allowed_tools or []),
)
self.states[agent_id] = state
if effective_name:
self.agent_names[effective_name] = agent_id
self._ensure_comms_endpoint(state)
await self._save_state(state, "running")
if state.worktree and self.store and hasattr(self.store, "save_runtime_worktree_session"):
await self.store.save_runtime_worktree_session(
worktree_session_id=f"wt_{agent_id}",
runtime_session_id=self.runtime_session_id,
task_id=self.parent_task.id if self.parent_task else None,
path=str(state.worktree.get("path", "") or ""),
status="active",
metadata=dict(state.worktree or {}),
)
await self._emit(
"subagent_started",
state,
{
"prompt": prompt,
"description": effective_description,
"name": effective_name,
"isolation": effective_isolation,
"mode": effective_mode,
"model": effective_model,
"fork_mode": state.fork_mode,
"resident": state.resident,
},
)
async def _execute_turn(turn_prompt: str) -> None:
try:
state.status = "running"
state.update_event.set()
await self._save_state(state, state.status)
child = self._build_child_task(state, turn_prompt)
child.metadata["_permission_bridge_runtime_session_id"] = self.runtime_session_id
setattr(child, "_runtime_permission_bridge", self._build_permission_bridge(state))
child_agent = self._build_child_agent(
profile=profile,
allowed_tools=list(state.fork_allowed_tools) or self._resolve_allowed_tools(profile, mode=effective_mode),
prompt_addendum=self._profile_prompt(profile, mode=effective_mode),
state=state,
)
setattr(child, "_runtime_inbox_queue", state.inbox)
result = await child_agent.execute(child)
state.task_result = result
state.latest_result = result.content
terminal_status = result.status.value
state.last_notification_kind = self._resident_notification_kind(result)
state.status = "idle" if state.resident else terminal_status
await self._save_state(state, state.status, {
"turn_status": terminal_status,
"notification_kind": state.last_notification_kind,
})
await self._emit(
"subagent_completed",
state,
{
"status": terminal_status,
"resident": state.resident,
"resident_status": state.status,
"accepts_followups": bool(state.resident or not state.completion.is_set()),
"pending_messages_count": state.pending_messages_count,
"content_preview": result.content[:500],
},
)
if state.resident:
await self._emit_worker_notification(
state,
notification_kind=state.last_notification_kind or "idle",
summary=result.content or f"{state.name or state.agent_id} is idle",
)
except Exception as exc: # pragma: no cover - defensive
state.latest_result = str(exc)
state.last_notification_kind = "error"
state.task_result = TaskResult(status=TaskStatus.FAILED, content=str(exc))
state.status = "idle" if state.resident else TaskStatus.FAILED.value
await self._save_state(state, state.status, {"error": str(exc), "notification_kind": state.last_notification_kind})
await self._emit(
"subagent_completed",
state,
{
"status": TaskStatus.FAILED.value,
"resident": state.resident,
"resident_status": state.status,
"accepts_followups": bool(state.resident),
"pending_messages_count": state.pending_messages_count,
"content_preview": str(exc)[:500],
},
)
if state.resident:
await self._emit_worker_notification(
state,
notification_kind="error",
summary=str(exc),
)
finally:
state.update_event.set()
async def _runner() -> None:
current_prompt = prompt
try:
while True:
await _execute_turn(current_prompt)
if not state.resident:
return
state.status = "idle"
state.update_event.set()
await self._save_state(state, state.status, {"notification_kind": state.last_notification_kind or "idle"})
next_message = await state.inbox.get()
state.pending_messages_count = max(0, state.pending_messages_count - 1)
current_prompt = str(next_message.get("body", "") or "").strip()
if not current_prompt:
current_prompt = str(next_message.get("message", "") or "").strip()
if not current_prompt:
current_prompt = str(next_message)
finally:
state.completion.set()
state.update_event.set()
if state.worktree and self.store and hasattr(self.store, "save_runtime_worktree_session"):
await self.store.save_runtime_worktree_session(
worktree_session_id=f"wt_{agent_id}",
runtime_session_id=self.runtime_session_id,
task_id=self.parent_task.id if self.parent_task else None,
path=str(state.worktree.get("path", "") or ""),
status="closed",
metadata=dict(state.worktree or {}),
)
await cleanup_worktree(state.worktree)
if effective_background:
state.runtime_task = asyncio.create_task(_runner())
return {
"success": True,
"agent_id": agent_id,
"name": effective_name,
"status": "running",
"background": True,
"resident": state.resident,
"resident_status": state.status,
"accepts_followups": bool(state.resident),
"worktree_path": (state.worktree or {}).get("path", ""),
}
await _runner()
return self._result_payload(state)
async def wait(self, agent_id: str, timeout_seconds: int = 300) -> dict[str, Any]:
resolved_id = self._resolve_agent_id(agent_id)
state = self.states.get(resolved_id)
if state is None:
return {"error": f"Unknown subagent: {agent_id}", "success": False}
if state.resident and state.status != "running":
return self._result_payload(state)
deadline = time.time() + max(1, int(timeout_seconds or 1))
while True:
remaining = deadline - time.time()
if remaining <= 0:
return self._result_payload(state)
if state.completion.is_set():
return self._result_payload(state)
if state.resident and state.status != "running":
return self._result_payload(state)
try:
await asyncio.wait_for(state.update_event.wait(), timeout=remaining)
except asyncio.TimeoutError:
return self._result_payload(state)
state.update_event.clear()
return self._result_payload(state)
async def send(self, agent_id: str, message: str) -> dict[str, Any]:
resolved_id = self._resolve_agent_id(agent_id)
state = self.states.get(resolved_id)
if state is None:
return {"error": f"Unknown subagent: {agent_id}", "success": False}
if state.completion.is_set():
return {"error": f"Subagent {agent_id} has already completed", "success": False}
rendered = str(message or "").strip()
self._persist_follow_up_message(state, rendered)
await state.inbox.put(
{
"body": rendered,
"message_class": "chat",
"actionable": True,
"worker_id": state.agent_id,
"origin_task_id": str(getattr(self.parent_task, "id", "") or "").strip(),
"origin_session_id": str(getattr(self.parent_task, "session_id", "") or "").strip(),
}
)
state.pending_messages_count += 1
state.update_event.set()
await self._save_state(state, state.status, {"queued_message": rendered[:500]})
await self._emit(
"subagent_updated",
state,
{
"message": rendered[:500],
"resident": state.resident,
"resident_status": state.status,
"pending_messages_count": state.pending_messages_count,
},
)
return {"success": True, "agent_id": state.agent_id, "name": state.name, "status": state.status}
def list_agents(self) -> dict[str, Any]:
return {
"success": True,
"agents": [self._result_payload(state) for state in self.states.values()],
}
def _build_child_task(self, state: SubagentState, prompt: str) -> Task:
parent = self.parent_task or Task()
metadata = dict(parent.metadata or {})
metadata.pop("_fork_allowed_tools", None)
metadata["_native_runtime_depth"] = int(metadata.get("_native_runtime_depth", 0) or 0) + 1
metadata["subagent_profile"] = state.profile
metadata["_subagent_name"] = state.name
metadata["_subagent_description"] = state.description
metadata["_subagent_model"] = state.model
metadata["_subagent_mode"] = state.mode
metadata["_subagent_max_iterations"] = state.max_iterations
metadata["_fork_mode"] = state.fork_mode
if state.profile == "verify":
metadata[self.config.system.native_runtime.verification_policy.skip_metadata_key] = True
metadata["work_item_verification_required"] = False
if state.worktree and state.worktree.get("path"):
metadata["target_output_dir"] = state.worktree["path"]
execution_context = dict((state.worktree or {}).get("execution_context", {}) or {})
if execution_context:
metadata["_execution_context"] = execution_context
if state.fork_system_prompt:
metadata["_runtime_system_prompt_override"] = state.fork_system_prompt
if state.fork_context_messages:
metadata["_fork_context_messages"] = list(state.fork_context_messages)
if state.fork_allowed_tools:
metadata["_fork_allowed_tools"] = list(state.fork_allowed_tools)
metadata["_comms_endpoint_id"] = state.agent_id
metadata["_comms_parent_endpoint_id"] = self._parent_endpoint_id()
metadata["_subagent_resident"] = state.resident
return Task(
title=state.name or state.description or f"{state.profile} subagent",
description=prompt,
assigned_to=parent.assigned_to,
project_id=parent.project_id,
session_id=f"{parent.session_id or 'session'}:{state.agent_id}",
parent_session_id=parent.session_id,
parent_id=parent.id,
tags=list(parent.tags),
metadata=metadata,
)
def _resolve_allowed_tools(self, profile: str, mode: str = "default") -> list[str]:
profiles = self.config.agents.native_subagents or {}
profile_cfg: NativeSubagentProfileConfig = profiles.get(profile) or profiles.get("general") or NativeSubagentProfileConfig()
if profile_cfg.allowed_tools:
return self._apply_mode_tool_filter(list(profile_cfg.allowed_tools), mode)
read_only = [
"file_read",
"file_search",
"list_dir",
"web_search",
"web_fetch",
"todo_read",
"todo_write",
"request_user_input",
"agent_spawn",
"agent_wait",
"agent_send",
"agent_list",
]
implement = [
"shell_exec",
"file_read",
"file_write",
"file_edit",
"file_search",
"list_dir",
"web_search",
"web_fetch",
"python_exec",
"todo_read",
"todo_write",
"request_user_input",
"agent_spawn",
"agent_wait",
"agent_send",
"agent_list",
]
verify = [
"shell_exec",
"file_read",
"file_search",
"list_dir",
"web_search",
"web_fetch",
"python_exec",
"browser_navigate",
"browser_snapshot",
"browser_click",
"browser_type",
"browser_wait_for",
"browser_scroll",
"browser_take_screenshot",
"browser_close",
"todo_read",
"todo_write",
"request_user_input",
"agent_spawn",
"agent_wait",
"agent_send",
"agent_list",
]
mapping = {
"general": implement,
"explore": read_only,
"plan": read_only,
"implement": implement,
"verify": verify,
}
return self._apply_mode_tool_filter(mapping.get(profile, implement), mode)
def _profile_prompt(self, profile: str, mode: str = "default") -> str:
prompts = {
"general": "Complete the task directly and report only the essential outcome.",
"explore": "Read-only exploration only. Do not modify files or system state.",
"plan": "Read-only planning only. Produce a concise implementation plan with critical files.",
"implement": "Implement directly in the assigned workspace, then verify your changes with commands.",
"verify": (
"Try to break the implementation. Prefer executable checks over code reading. "
"For every check you actually ran, emit `Check:`, `Command:`, `Observed Output:`, and `Result:` lines. "
"End with exactly one line `VERDICT: PASS`, `VERDICT: FAIL`, or `VERDICT: PARTIAL`. "
"You may start the final summary with `VERIFIED:` if the work is acceptable, or `ISSUES:` if blocking problems remain."
),
}
base = prompts.get(profile, prompts["general"])
normalized_mode = str(mode or "default").strip().lower()
if normalized_mode == "plan":
return base + " Operate in plan mode: do not make filesystem or shell changes."
if normalized_mode in {"accept_edits", "bypass_permissions", "dont_ask", "acceptedits", "bypasspermissions", "dontask"}:
return base + f" Runtime spawn mode hint: {mode}."
return base
def _profile_config(self, profile: str) -> NativeSubagentProfileConfig:
profiles = self.config.agents.native_subagents or {}
return profiles.get(profile) or profiles.get("general") or NativeSubagentProfileConfig()
def _apply_mode_tool_filter(self, tools: list[str], mode: str) -> list[str]:
if str(mode or "default").strip().lower() != "plan":
return list(tools)
plan_safe = {
"file_read",
"file_search",
"list_dir",
"web_search",
"web_fetch",
"todo_read",
"todo_write",
"request_user_input",
"agent_spawn",
"agent_wait",
"agent_send",
"agent_list",
}
return [tool for tool in tools if tool in plan_safe]
def _build_child_agent(
self,
*,
profile: str,
allowed_tools: list[str],
prompt_addendum: str,
state: SubagentState,
) -> Any:
overrides = {
"name": state.name,
"description": state.description,
"model": state.model,
"mode": state.mode,
"max_iterations": state.max_iterations,
}
signature = inspect.signature(self.child_agent_factory)
accepts_overrides = len(signature.parameters) >= 4 or any(
parameter.kind == inspect.Parameter.VAR_POSITIONAL
for parameter in signature.parameters.values()
)
if accepts_overrides:
return self.child_agent_factory(profile, allowed_tools, prompt_addendum, overrides)
return self.child_agent_factory(profile, allowed_tools, prompt_addendum)
def _build_permission_bridge(self, state: SubagentState) -> Callable[..., Awaitable[tuple[bool, Any]]]:
async def _bridge(
*,
tool: Any,
arguments: dict[str, Any],
approval_engine: Any,
on_progress: Any = None,
) -> tuple[bool, Any]:
parent_task = self.parent_task or Task(project_id="default")
metadata = {
"category": getattr(tool, "category", "general"),
"requires_confirmation": getattr(tool, "requires_confirmation", False),
"description": getattr(tool, "description", ""),
"subagent_id": state.agent_id,
"subagent_profile": state.profile,
"subagent_name": state.name,
"subagent_mode": state.mode,
"bridged_runtime_session_id": self.runtime_session_id,
}
return await approval_engine.authorize_tool_call(
task=parent_task,
tool_name=getattr(tool, "name", ""),
arguments=arguments,
metadata=metadata,
on_progress=on_progress,
)
return _bridge
def _resolve_agent_id(self, agent_id: str) -> str:
raw = str(agent_id or "").strip()
if raw in self.states:
return raw
return self.agent_names.get(raw, raw)
def _parent_endpoint_id(self) -> str:
if self.parent_task is None:
return "runtime-parent"
session_id = str(getattr(self.parent_task, "session_id", "") or "").strip()
task_id = str(getattr(self.parent_task, "id", "") or "").strip()
return f"task::{session_id or task_id or 'runtime-parent'}"
def _comms_layout(self):
if self.parent_task is None:
return None
workspace_root = (
str(getattr(self.parent_task, "metadata", {}).get("comms_workspace_root", "") or "").strip()
or str(getattr(self.parent_task, "metadata", {}).get("workspace_root", "") or "").strip()
or str(getattr(self.parent_task, "metadata", {}).get("target_output_dir", "") or "").strip()
)
if not workspace_root:
return None
try:
from opc.layer2_organization import comms as _comms
return _comms.resolve_layout(
workspace_root,
str(getattr(self.parent_task, "project_id", "") or "default").strip() or "default",
str(getattr(self.parent_task, "parent_session_id", "") or getattr(self.parent_task, "session_id", "") or "default").strip() or "default",
)
except Exception:
return None
def _ensure_comms_endpoint(self, state: SubagentState) -> None:
layout = self._comms_layout()
if layout is None:
return
try:
from opc.layer2_organization import comms as _comms
_comms.ensure_layout(layout, [self._parent_endpoint_id(), state.agent_id])
except Exception:
return
def _persist_follow_up_message(self, state: SubagentState, message: str) -> None:
if not message:
return
layout = self._comms_layout()
if layout is None:
return
try:
from opc.layer2_organization import comms as _comms
_comms.send_message(
layout,
from_role=self._parent_endpoint_id(),
to_role=state.agent_id,
subject=f"Follow-up for {state.name or state.agent_id}",
body=message,
priority="normal",
extra_frontmatter={
"transport_kind": "system",
"semantic_type": "work_update",
"message_class": "chat",
"actionable": True,
"worker_id": state.agent_id,
"origin_task_id": str(getattr(self.parent_task, "id", "") or "").strip(),
"origin_session_id": str(getattr(self.parent_task, "session_id", "") or "").strip(),
"comms_state": "open",
"from_endpoint_type": "native_subagent",
"to_endpoint_type": "native_subagent",
"refs": {
"task_id": str(getattr(self.parent_task, "id", "") or "").strip(),
"runtime_session_id": self.runtime_session_id,
},
},
)
except Exception:
return
async def _emit(self, event_type: str, state: SubagentState, payload: dict[str, Any]) -> None:
if not self.event_bus:
return
await self.event_bus.publish(OPCEvent(
event_type="runtime_event",
payload={
"type": event_type,
"agent_id": state.agent_id,
"profile": state.profile,
"task_id": str(getattr(self.parent_task, "id", "") or "").strip(),
"session_id": str(getattr(self.parent_task, "session_id", "") or "").strip(),
"resident": state.resident,
"resident_status": state.status,
"pending_messages_count": state.pending_messages_count,
**payload,
},
))
async def _save_state(self, state: SubagentState, status: str, metadata: dict[str, Any] | None = None) -> None:
if not self.store or not hasattr(self.store, "save_runtime_subagent_run"):
return
merged_metadata = {
"name": state.name,
"description": state.description,
"model": state.model,
"mode": state.mode,
"isolation": state.isolation,
"max_iterations": state.max_iterations,
"fork_mode": state.fork_mode,
"fork_context_messages": len(state.fork_context_messages),
"resident": state.resident,
"resident_status": state.status,
"accepts_followups": bool(state.resident and not state.completion.is_set()),
"pending_messages_count": state.pending_messages_count,
"last_notification_kind": state.last_notification_kind,
}
merged_metadata.update(metadata or {})
await self.store.save_runtime_subagent_run(
subagent_run_id=state.agent_id,
runtime_session_id=self.runtime_session_id,
task_id=self.parent_task.id if self.parent_task else None,
agent_id=state.agent_id,
profile=state.profile,
status=status,
worktree_path=str((state.worktree or {}).get("path", "") or ""),
metadata=merged_metadata,
)
async def _emit_worker_notification(
self,
state: SubagentState,
*,
notification_kind: str,
summary: str,
actionable: bool = False,
) -> None:
payload = {
"worker_id": state.agent_id,
"worker_type": "native_subagent",
"notification_kind": str(notification_kind or "idle").strip() or "idle",
"summary": str(summary or "").strip(),
"task_id": str(getattr(self.parent_task, "id", "") or "").strip(),
"session_id": str(getattr(self.parent_task, "session_id", "") or "").strip(),
**work_item_identity_payload_for_task(self.parent_task),
"projection_id": projection_id_for_task(self.parent_task) if self.parent_task is not None else "",
"details_ref": state.agent_id,
"actionable": bool(actionable),
"resident_status": state.status,
"pending_messages_count": state.pending_messages_count,
"name": state.name,
}
await self._emit("worker_notification", state, payload)
@staticmethod
def _resident_notification_kind(result: TaskResult) -> str:
if result.status in {TaskStatus.AWAITING_HUMAN, TaskStatus.AWAITING_REVIEW}:
return "permission_needed"
if result.status == TaskStatus.AWAITING_PEER:
return "blocked"
if result.status == TaskStatus.FAILED:
return "error"
if result.status == TaskStatus.DONE:
return "task_complete"
return "idle"
def _result_payload(self, state: SubagentState) -> dict[str, Any]:
payload = {
"success": state.task_result is not None and state.task_result.status == TaskStatus.DONE,
"agent_id": state.agent_id,
"profile": state.profile,
"name": state.name,
"description": state.description,
"model": state.model,
"mode": state.mode,
"isolation": state.isolation,
"max_iterations": state.max_iterations,
"status": state.status,
"background": state.background,
"fork_mode": state.fork_mode,
"resident": state.resident,
"resident_status": state.status,
"accepts_followups": bool(state.resident and not state.completion.is_set()),
"pending_messages_count": state.pending_messages_count,
"last_notification_kind": state.last_notification_kind,
"result": state.latest_result,
"worktree_path": (state.worktree or {}).get("path", ""),
"venv_path": (state.worktree or {}).get("venv_path", ""),
"python_executable": (state.worktree or {}).get("python_executable", ""),
}
if state.task_result and state.task_result.status in {TaskStatus.AWAITING_HUMAN, TaskStatus.AWAITING_REVIEW}:
artifacts = dict(state.task_result.artifacts or {})
payload.update(
{
"requires_user_input": True,
"reason": state.task_result.content,
"approval": dict(artifacts.get("approval", {}) or {}),
"permission_requests": list(artifacts.get("permission_requests", []) or []),
}
)
if state.task_result and state.task_result.status == TaskStatus.AWAITING_PEER:
artifacts = dict(state.task_result.artifacts or {})
payload.update(
{
"requires_peer_wait": True,
"reason": state.task_result.content,
"permission_requests": list(artifacts.get("permission_requests", []) or []),
}
)
return payload
+96
View File
@@ -0,0 +1,96 @@
"""Runtime-managed tool hook bus for Native Runtime V2."""
from __future__ import annotations
from dataclasses import dataclass, field
from typing import Any, Awaitable, Callable, Optional
@dataclass
class RuntimeToolHookContext:
phase: str
tool_name: str
call: dict[str, Any]
task: Any = None
tool: Any = None
arguments: dict[str, Any] = field(default_factory=dict)
predicted_permission: Any = None
result: dict[str, Any] | None = None
state: dict[str, Any] = field(default_factory=dict)
RuntimeToolHook = Callable[[RuntimeToolHookContext], Awaitable[Optional[dict[str, Any]]]]
RuntimeHookEmitter = Callable[[str, dict[str, Any]], Awaitable[None]]
class RuntimeToolHookBus:
"""Composable pre/post/failure hook bus for runtime-managed tool execution."""
def __init__(self, *, emit_event: RuntimeHookEmitter | None = None) -> None:
self.emit_event = emit_event
self._pre_hooks: list[tuple[str, RuntimeToolHook]] = []
self._post_hooks: list[tuple[str, RuntimeToolHook]] = []
self._failure_hooks: list[tuple[str, RuntimeToolHook]] = []
def register_pre_hook(self, name: str, hook: RuntimeToolHook) -> None:
self._pre_hooks.append((name, hook))
def register_post_hook(self, name: str, hook: RuntimeToolHook) -> None:
self._post_hooks.append((name, hook))
def register_failure_hook(self, name: str, hook: RuntimeToolHook) -> None:
self._failure_hooks.append((name, hook))
async def run_pre_hooks(self, context: RuntimeToolHookContext) -> RuntimeToolHookContext:
return await self._run_hooks(self._pre_hooks, context)
async def run_post_hooks(self, context: RuntimeToolHookContext) -> RuntimeToolHookContext:
return await self._run_hooks(self._post_hooks, context)
async def run_failure_hooks(self, context: RuntimeToolHookContext) -> RuntimeToolHookContext:
return await self._run_hooks(self._failure_hooks, context)
async def _run_hooks(
self,
hooks: list[tuple[str, RuntimeToolHook]],
context: RuntimeToolHookContext,
) -> RuntimeToolHookContext:
for hook_name, hook in hooks:
patch = await hook(context) or {}
self._apply_patch(context, patch)
if self.emit_event:
await self.emit_event(
"tool_hook",
{
"phase": context.phase,
"tool_name": context.tool_name,
"tool_call_id": context.call.get("id", ""),
"hook_name": hook_name,
"stopped": bool(context.state.get("stop_execution")),
"result_overridden": context.result is not None,
},
)
if context.state.get("stop_execution"):
break
return context
@staticmethod
def _apply_patch(context: RuntimeToolHookContext, patch: dict[str, Any]) -> None:
if not patch:
return
if isinstance(patch.get("arguments"), dict):
context.arguments = dict(patch["arguments"])
if isinstance(patch.get("result"), dict):
context.result = dict(patch["result"])
if isinstance(patch.get("metadata"), dict):
context.state.setdefault("metadata", {}).update(dict(patch["metadata"]))
if isinstance(patch.get("approval"), dict):
context.state.setdefault("approval", {}).update(dict(patch["approval"]))
if "stop_execution" in patch:
context.state["stop_execution"] = bool(patch["stop_execution"])
if "stop_batch_on_failure" in patch:
context.state["stop_batch_on_failure"] = bool(patch["stop_batch_on_failure"])
if "prevent_continuation" in patch:
context.state["prevent_continuation"] = bool(patch["prevent_continuation"])
if patch.get("stop_reason"):
context.state["stop_reason"] = str(patch["stop_reason"])
@@ -0,0 +1,77 @@
"""Tool planning helpers for Native Runtime V2."""
from __future__ import annotations
from dataclasses import dataclass
from typing import Any
from opc.layer4_tools.registry import ToolDefinition, ToolRegistry
_READ_ONLY_TOOL_NAMES = {
"file_read",
"file_search",
"list_dir",
"grep",
"glob",
"web_search",
"web_fetch",
"todo_read",
"agent_list",
"agent_wait",
}
_NON_CONCURRENT_TOOL_NAMES = {
"shell_exec",
"file_write",
"file_edit",
"apply_patch",
"python_exec",
"git_commit",
"agent_spawn",
"agent_wait",
"agent_send",
}
@dataclass
class ToolBatch:
concurrency_safe: bool
calls: list[dict[str, Any]]
class ToolPlanner:
"""Determine tool execution ordering and concurrency."""
def __init__(self, registry: ToolRegistry, max_parallel_read_tools: int = 6) -> None:
self.registry = registry
self.max_parallel_read_tools = max(1, int(max_parallel_read_tools or 1))
def is_read_only(self, tool: ToolDefinition | None) -> bool:
if tool is None:
return False
if tool.read_only is not None:
return bool(tool.read_only)
if tool.name in _READ_ONLY_TOOL_NAMES:
return True
return tool.category in {"search", "read"} or tool.name.endswith("_read")
def is_concurrency_safe(self, tool: ToolDefinition | None) -> bool:
if tool is None:
return False
if tool.concurrency_safe is not None:
return bool(tool.concurrency_safe)
if tool.name in _NON_CONCURRENT_TOOL_NAMES:
return False
return self.is_read_only(tool)
def partition(self, tool_calls: list[dict[str, Any]]) -> list[ToolBatch]:
batches: list[ToolBatch] = []
for call in tool_calls:
tool = self.registry.get(str(call.get("function", "") or ""))
concurrency_safe = self.is_concurrency_safe(tool)
if concurrency_safe and batches and batches[-1].concurrency_safe:
batches[-1].calls.append(call)
else:
batches.append(ToolBatch(concurrency_safe=concurrency_safe, calls=[call]))
return batches
+260
View File
@@ -0,0 +1,260 @@
"""Local worktree helpers for runtime-managed subagents."""
from __future__ import annotations
import asyncio
import os
import shutil
import sys
import tempfile
from pathlib import Path
from typing import Any
from opc.core.config import OPCConfig
from opc.layer4_tools.execution_context import build_task_execution_context, venv_python_path
async def create_worktree(
base_path: str | None,
*,
config: OPCConfig | None = None,
on_progress: Any = None,
) -> dict[str, Any]:
source = Path(base_path or os.getcwd()).resolve()
root = await _find_git_root(source)
temp_dir = Path(tempfile.mkdtemp(prefix="opc-native-v2-"))
if root is not None:
proc = await asyncio.create_subprocess_exec(
"git",
"-C",
str(root),
"worktree",
"add",
"--detach",
str(temp_dir),
"HEAD",
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
)
stdout, stderr = await proc.communicate()
if proc.returncode == 0:
info = {
"path": str(temp_dir),
"git_root": str(root),
"mode": "git_worktree",
"stdout": stdout.decode("utf-8", errors="replace"),
}
return await _prepare_execution_environment(info, config=config, on_progress=on_progress)
shutil.rmtree(temp_dir, ignore_errors=True)
temp_dir = Path(tempfile.mkdtemp(prefix="opc-native-v2-copy-"))
shutil.copytree(
source,
temp_dir,
dirs_exist_ok=True,
ignore=shutil.ignore_patterns(".git", ".venv", "__pycache__", ".pytest_cache"),
)
info = {
"path": str(temp_dir),
"git_root": str(root) if root else "",
"mode": "copy",
}
return await _prepare_execution_environment(info, config=config, on_progress=on_progress)
async def cleanup_worktree(info: dict[str, Any] | None) -> None:
if not info:
return
path = Path(str(info.get("path", "") or "")).resolve()
mode = str(info.get("mode", "") or "")
git_root = str(info.get("git_root", "") or "")
if not path.exists():
return
if mode == "git_worktree" and git_root:
proc = await asyncio.create_subprocess_exec(
"git",
"-C",
git_root,
"worktree",
"remove",
"--force",
str(path),
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
)
await proc.communicate()
return
shutil.rmtree(path, ignore_errors=True)
async def _find_git_root(path: Path) -> Path | None:
current = path
if current.is_file():
current = current.parent
for candidate in [current, *current.parents]:
if (candidate / ".git").exists():
return candidate
return None
async def _prepare_execution_environment(
info: dict[str, Any],
*,
config: OPCConfig | None = None,
on_progress: Any = None,
) -> dict[str, Any]:
workspace = Path(str(info.get("path", "") or "")).resolve()
info["execution_context"] = build_task_execution_context(workspace_root=workspace, config=config)
if config is None:
return info
venv_cfg = config.system.native_runtime.execution_environment.worktree_venv
if not venv_cfg.enabled:
return info
provider = _resolve_venv_provider(venv_cfg.provider)
venv_path = workspace / str(venv_cfg.venv_dir or ".opc-venv")
python_path = venv_python_path(venv_path)
try:
if not python_path.exists():
create_args = _venv_create_args(provider, venv_path, system_site_packages=venv_cfg.system_site_packages)
await _run_process(create_args, cwd=workspace, on_progress=on_progress)
sync_commands = _build_sync_commands(
workspace=workspace,
python_path=python_path,
provider=provider,
editable_project=venv_cfg.editable_project,
requirements_files=_detect_requirements_files(
workspace=workspace,
configured=venv_cfg.requirements_files,
auto_detect=venv_cfg.auto_detect_requirements,
),
)
for command in sync_commands:
await _run_process(command, cwd=workspace, on_progress=on_progress)
info.update(
{
"venv_path": str(venv_path),
"python_executable": str(python_path),
"venv_provider": provider,
"environment_prepared": True,
"environment_sync_commands": len(sync_commands),
}
)
info["execution_context"] = build_task_execution_context(
workspace_root=workspace,
config=config,
venv_path=venv_path,
python_executable=python_path,
venv_provider=provider,
)
except Exception as exc:
error = str(exc)
info.update(
{
"venv_path": str(venv_path),
"python_executable": str(python_path),
"venv_provider": provider,
"environment_prepared": False,
"environment_error": error,
}
)
info["execution_context"] = build_task_execution_context(
workspace_root=workspace,
config=config,
venv_path=venv_path,
python_executable=python_path,
venv_provider=provider,
preparation_error=error,
)
if venv_cfg.fail_if_prepare_fails:
raise
return info
def _resolve_venv_provider(provider: str) -> str:
normalized = str(provider or "auto").strip().lower() or "auto"
if normalized == "uv":
return "uv"
if normalized == "venv":
return "venv"
return "uv" if shutil.which("uv") else "venv"
def _venv_create_args(provider: str, venv_path: Path, *, system_site_packages: bool) -> list[str]:
if provider == "uv":
return ["uv", "venv", str(venv_path), "--python", sys.executable]
args = [sys.executable, "-m", "venv", str(venv_path)]
if system_site_packages:
args.append("--system-site-packages")
return args
def _detect_requirements_files(
*,
workspace: Path,
configured: list[str],
auto_detect: bool,
) -> list[Path]:
candidates: list[Path] = []
for entry in configured:
text = str(entry or "").strip()
if text:
candidates.append((workspace / text).resolve())
if auto_detect:
for name in ("requirements.txt", "requirements-dev.txt", "requirements-test.txt", "requirements-ci.txt"):
candidate = (workspace / name).resolve()
if candidate.exists():
candidates.append(candidate)
seen: set[str] = set()
resolved: list[Path] = []
for item in candidates:
key = str(item)
if key in seen or not item.exists():
continue
seen.add(key)
resolved.append(item)
return resolved
def _build_sync_commands(
*,
workspace: Path,
python_path: Path,
provider: str,
editable_project: bool,
requirements_files: list[Path],
) -> list[list[str]]:
commands: list[list[str]] = []
if editable_project and (workspace / "pyproject.toml").exists():
if provider == "uv" and shutil.which("uv"):
commands.append(["uv", "pip", "install", "--python", str(python_path), "-e", str(workspace)])
else:
commands.append([str(python_path), "-m", "pip", "install", "-e", str(workspace)])
for item in requirements_files:
if provider == "uv" and shutil.which("uv"):
commands.append(["uv", "pip", "install", "--python", str(python_path), "-r", str(item)])
else:
commands.append([str(python_path), "-m", "pip", "install", "-r", str(item)])
return commands
async def _run_process(args: list[str], *, cwd: Path, on_progress: Any = None) -> None:
if on_progress:
try:
await on_progress(f"[worktree-env] {' '.join(args[:4])}")
except TypeError:
await on_progress(f"[worktree-env] {' '.join(args[:4])}")
proc = await asyncio.create_subprocess_exec(
*args,
cwd=str(cwd),
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
)
stdout, stderr = await proc.communicate()
if proc.returncode == 0:
return
stdout_text = stdout.decode("utf-8", errors="replace").strip()
stderr_text = stderr.decode("utf-8", errors="replace").strip()
detail = stderr_text or stdout_text or f"exit code {proc.returncode}"
raise RuntimeError(f"{' '.join(args[:4])} failed: {detail}")