mirror of
https://github.com/hansjone/oclaw.git
synced 2026-10-09 04:40:45 +08:00
完善 agent/workspace 运行链路与工具结果治理测试覆盖,确保路由、标题生成与上下文截断行为稳定可回归。
同时将 data/wiki 用户数据纳入忽略规则,避免运行态数据被误纳入版本管理。 Made-with: Cursor
This commit is contained in:
parent
45e412b4ab
commit
20362527e2
17 changed files with 974 additions and 41 deletions
|
|
@ -1,15 +1,26 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import subprocess
|
||||
import re
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from oclaw.platform.config.paths import db_path
|
||||
from oclaw.platform.persistence.sqlite_store import SqliteStore
|
||||
from oclaw.runtime.tools.base import ToolSpec
|
||||
from oclaw.runtime.tools.experts.workspace.workspace_base import resolve_workspace_path, truncate_text
|
||||
from oclaw.runtime.tools.experts.workspace.workspace_base import (
|
||||
resolve_workspace_path,
|
||||
truncate_text,
|
||||
workspace_root,
|
||||
)
|
||||
|
||||
|
||||
def run_command_tool() -> ToolSpec:
|
||||
_LEADING_CD_CHAIN_RE = re.compile(
|
||||
r"^\s*(?:(?:[A-Za-z]:)\s*&&\s*)?(?:@echo\s+off\s*&&\s*)?cd\s+(?:/d\s+)?(?:\"[^\"]+\"|[^&]+?)\s*&&\s*(.+)$",
|
||||
re.IGNORECASE | re.DOTALL,
|
||||
)
|
||||
|
||||
def _run_command_enabled() -> bool:
|
||||
import os
|
||||
|
||||
|
|
@ -33,6 +44,84 @@ def run_command_tool() -> ToolSpec:
|
|||
def handler(args: dict[str, Any]) -> dict[str, Any]:
|
||||
import os
|
||||
|
||||
def _default_exec_dir() -> str:
|
||||
return str(resolve_workspace_path("data/workspace"))
|
||||
|
||||
def _strip_leading_cd_chain(cmd: str) -> tuple[str, bool]:
|
||||
raw = str(cmd or "")
|
||||
changed = False
|
||||
out = raw
|
||||
# Strip repeated leading "cd ... &&" so default sandbox cwd cannot be bypassed by habit.
|
||||
for _ in range(3):
|
||||
m = _LEADING_CD_CHAIN_RE.match(out)
|
||||
if not m:
|
||||
break
|
||||
tail = str(m.group(1) or "").strip()
|
||||
if not tail:
|
||||
break
|
||||
out = tail
|
||||
changed = True
|
||||
return out, changed
|
||||
|
||||
def _rewrite_workspace_absolute_refs(cmd: str, *, workdir: str) -> tuple[str, bool]:
|
||||
raw = str(cmd or "")
|
||||
root = str(workspace_root())
|
||||
if not raw or not root:
|
||||
return raw, False
|
||||
root_norm = root.rstrip("\\/")
|
||||
changed = False
|
||||
out = raw
|
||||
marker = root_norm + "\\"
|
||||
if marker.lower() not in out.lower():
|
||||
return out, False
|
||||
idx = 0
|
||||
rebuilt = []
|
||||
low = out.lower()
|
||||
marker_low = marker.lower()
|
||||
while True:
|
||||
pos = low.find(marker_low, idx)
|
||||
if pos < 0:
|
||||
rebuilt.append(out[idx:])
|
||||
break
|
||||
rebuilt.append(out[idx:pos])
|
||||
tail_start = pos + len(marker)
|
||||
tail_end = tail_start
|
||||
while tail_end < len(out) and out[tail_end] not in ('"', "'", " ", "\t", "\r", "\n"):
|
||||
tail_end += 1
|
||||
rel_tail = out[tail_start:tail_end]
|
||||
candidate = str(Path(workdir) / rel_tail)
|
||||
if Path(candidate).exists():
|
||||
rebuilt.append(candidate)
|
||||
changed = True
|
||||
else:
|
||||
rebuilt.append(out[pos:tail_end])
|
||||
idx = tail_end
|
||||
return "".join(rebuilt), changed
|
||||
|
||||
def _rewrite_python_script_arg(cmd: str, *, workdir: str) -> tuple[str, bool]:
|
||||
raw = str(cmd or "").strip()
|
||||
if not raw:
|
||||
return raw, False
|
||||
m = re.match(r'^\s*(python|py)\s+("([^"]+\.py)"|([^\s]+\.py))(\s+.*)?$', raw, flags=re.IGNORECASE)
|
||||
if not m:
|
||||
return raw, False
|
||||
script = str(m.group(3) or m.group(4) or "").strip()
|
||||
if not script:
|
||||
return raw, False
|
||||
# Absolute path is handled by workspace-absolute rewrite already.
|
||||
sp = Path(script)
|
||||
if sp.is_absolute():
|
||||
return raw, False
|
||||
base = str(Path(script).name or "").strip()
|
||||
if not base:
|
||||
return raw, False
|
||||
# Deterministic policy: always bind python script arg to sandbox root.
|
||||
rel = base
|
||||
quote = '"' if " " in rel else ""
|
||||
prefix = str(m.group(1) or "python")
|
||||
rest = str(m.group(5) or "")
|
||||
return f"{prefix} {quote}{rel}{quote}{rest}", True
|
||||
|
||||
if not _run_command_enabled():
|
||||
return {
|
||||
"ok": False,
|
||||
|
|
@ -45,7 +134,21 @@ def run_command_tool() -> ToolSpec:
|
|||
max_output_chars = int(args.get("max_output_chars") or 20000)
|
||||
if not command:
|
||||
return {"ok": False, "error": "command_required"}
|
||||
workdir = resolve_workspace_path(cwd or ".")
|
||||
normalized_cd_removed = False
|
||||
command_rewritten = False
|
||||
cwd_redirected_to_sandbox = False
|
||||
script_path_rewritten = False
|
||||
original_command = command
|
||||
# Hard policy: always execute in sandbox root, ignore caller-supplied cwd.
|
||||
workdir = _default_exec_dir()
|
||||
cwd_redirected_to_sandbox = bool(str(cwd or "").strip() and str(cwd).strip() not in {".", "./", ".\\"})
|
||||
command, normalized_cd_removed = _strip_leading_cd_chain(command)
|
||||
command, command_rewritten = _rewrite_workspace_absolute_refs(command, workdir=workdir)
|
||||
command, script_path_rewritten = _rewrite_python_script_arg(command, workdir=workdir)
|
||||
try:
|
||||
os.makedirs(workdir, exist_ok=True)
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
run_kwargs: dict[str, Any] = {
|
||||
"cwd": str(workdir),
|
||||
|
|
@ -64,14 +167,39 @@ def run_command_tool() -> ToolSpec:
|
|||
command,
|
||||
**run_kwargs,
|
||||
)
|
||||
out = (cp.stdout or "") + (("\n" + cp.stderr) if cp.stderr else "")
|
||||
out = truncate_text(out, limit=max(1000, min(max_output_chars, 200000)))
|
||||
stdout_text = str(cp.stdout or "")
|
||||
stderr_text = str(cp.stderr or "")
|
||||
out_raw = stdout_text + (("\n" + stderr_text) if stderr_text else "")
|
||||
out_limit = max(1000, min(max_output_chars, 200000))
|
||||
out = truncate_text(out_raw, limit=out_limit)
|
||||
out_truncated = len(out_raw) > out_limit
|
||||
out_empty = (len(str(out_raw or "").strip()) == 0)
|
||||
exit_code = int(cp.returncode)
|
||||
ok_flag = exit_code == 0
|
||||
return {
|
||||
"ok": True,
|
||||
"ok": bool(ok_flag),
|
||||
"command": command,
|
||||
"cwd": str(workdir),
|
||||
"exit_code": int(cp.returncode),
|
||||
"exit_code": exit_code,
|
||||
"stdout": stdout_text,
|
||||
"stderr": stderr_text,
|
||||
"output": out,
|
||||
"output_chars": int(len(out_raw)),
|
||||
"output_empty": bool(out_empty),
|
||||
"output_truncated": bool(out_truncated),
|
||||
"output_not_truncated": bool(not out_truncated),
|
||||
"normalized_cd_removed": bool(normalized_cd_removed),
|
||||
"cwd_redirected_to_sandbox": bool(cwd_redirected_to_sandbox),
|
||||
"command_rewritten": bool(command_rewritten),
|
||||
"script_path_rewritten": bool(script_path_rewritten),
|
||||
"original_command": original_command,
|
||||
"error_code": ("" if ok_flag else "command_exit_nonzero"),
|
||||
"output_hint": (
|
||||
"Command produced empty stdout/stderr; this is not system truncation. "
|
||||
"Do not claim truncation unless output_truncated=true."
|
||||
if out_empty
|
||||
else ""
|
||||
),
|
||||
}
|
||||
except subprocess.TimeoutExpired as e:
|
||||
partial = ""
|
||||
|
|
@ -86,9 +214,26 @@ def run_command_tool() -> ToolSpec:
|
|||
"cwd": str(workdir),
|
||||
"timeout_s": timeout_s,
|
||||
"output": truncate_text(partial, limit=max_output_chars),
|
||||
"output_empty": len(str(partial or "").strip()) == 0,
|
||||
"output_truncated": len(str(partial or "")) > int(max_output_chars or 0),
|
||||
"normalized_cd_removed": bool(normalized_cd_removed),
|
||||
"cwd_redirected_to_sandbox": bool(cwd_redirected_to_sandbox),
|
||||
"command_rewritten": bool(command_rewritten),
|
||||
"script_path_rewritten": bool(script_path_rewritten),
|
||||
"original_command": original_command,
|
||||
}
|
||||
except Exception as e:
|
||||
return {"ok": False, "error": f"{type(e).__name__}: {e}", "command": command, "cwd": str(workdir)}
|
||||
return {
|
||||
"ok": False,
|
||||
"error": f"{type(e).__name__}: {e}",
|
||||
"command": command,
|
||||
"cwd": str(workdir),
|
||||
"normalized_cd_removed": bool(normalized_cd_removed),
|
||||
"cwd_redirected_to_sandbox": bool(cwd_redirected_to_sandbox),
|
||||
"command_rewritten": bool(command_rewritten),
|
||||
"script_path_rewritten": bool(script_path_rewritten),
|
||||
"original_command": original_command,
|
||||
}
|
||||
|
||||
return ToolSpec(
|
||||
name="run_command",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue