ltcai 3.4.1 → 3.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,69 @@
1
+ """Local filesystem tools (require user approval via the UI) + desktop bridge status."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+ from typing import Any, Dict
7
+
8
+ from tools import ToolError, LOCAL_MAX_FILE_BYTES
9
+
10
+
11
+ def local_list(path: str) -> Dict[str, Any]:
12
+ """List any directory on the local filesystem (requires user approval via UI)."""
13
+ target = Path(path).expanduser().resolve()
14
+ if not target.exists():
15
+ raise ToolError(f"경로가 존재하지 않습니다: {path}")
16
+ if not target.is_dir():
17
+ raise ToolError(f"폴더가 아닙니다: {path}")
18
+ items = []
19
+ try:
20
+ for child in sorted(target.iterdir(), key=lambda p: (not p.is_dir(), p.name.lower())):
21
+ stat = child.stat()
22
+ items.append({
23
+ "name": child.name,
24
+ "path": str(child),
25
+ "type": "directory" if child.is_dir() else "file",
26
+ "size": stat.st_size if child.is_file() else None,
27
+ })
28
+ except PermissionError as exc:
29
+ raise ToolError(f"접근 권한 없음: {exc}") from exc
30
+ return {"path": str(target), "items": items}
31
+
32
+
33
+ def local_read(path: str) -> Dict[str, Any]:
34
+ """Read any file on the local filesystem (requires user approval via UI)."""
35
+ target = Path(path).expanduser().resolve()
36
+ if not target.exists():
37
+ raise ToolError(f"파일이 존재하지 않습니다: {path}")
38
+ if not target.is_file():
39
+ raise ToolError(f"파일이 아닙니다: {path}")
40
+ size = target.stat().st_size
41
+ if size > LOCAL_MAX_FILE_BYTES:
42
+ raise ToolError(f"파일이 너무 큽니다 ({size:,} bytes). 최대 {LOCAL_MAX_FILE_BYTES:,} bytes.")
43
+ try:
44
+ content = target.read_text(encoding="utf-8", errors="replace")
45
+ except Exception as exc:
46
+ raise ToolError(f"파일 읽기 실패: {exc}") from exc
47
+ return {"path": str(target), "size": size, "content": content}
48
+
49
+
50
+ def local_write(path: str, content: str) -> Dict[str, Any]:
51
+ """Write content to any path on the local filesystem (requires user approval via UI)."""
52
+ target = Path(path).expanduser().resolve()
53
+ if len(content.encode("utf-8")) > LOCAL_MAX_FILE_BYTES:
54
+ raise ToolError("내용이 너무 큽니다.")
55
+ try:
56
+ target.parent.mkdir(parents=True, exist_ok=True)
57
+ target.write_text(content, encoding="utf-8")
58
+ except PermissionError as exc:
59
+ raise ToolError(f"쓰기 권한 없음: {exc}") from exc
60
+ return {"path": str(target), "bytes": target.stat().st_size}
61
+
62
+
63
+
64
+ def desktop_bridge_status() -> Dict[str, Any]:
65
+ return {
66
+ "status": "requires_desktop_bridge",
67
+ "available_in_codex": True,
68
+ "note": "Chrome and Mac UI control require the Codex desktop Computer Use/Chrome bridge, not a headless FastAPI worker.",
69
+ }
@@ -0,0 +1,66 @@
1
+ """Network status probe (no shell, short timeouts)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import socket
6
+ import subprocess
7
+ from typing import Any, Dict, List
8
+
9
+ from tools import ToolError
10
+
11
+
12
+ def _run_network_command(parts: List[str], timeout: int = 5) -> str:
13
+ try:
14
+ completed = subprocess.run(parts, capture_output=True, text=True, timeout=timeout, check=False)
15
+ if completed.returncode != 0:
16
+ return ""
17
+ return completed.stdout.strip()
18
+ except Exception:
19
+ return ""
20
+
21
+
22
+ def network_status() -> Dict[str, Any]:
23
+ """현재 Mac의 내부 IP, 외부 IP, 주요 네트워크 정보를 반환합니다."""
24
+ local_ips: Dict[str, str] = {}
25
+ for interface in ["en0", "en1", "bridge100"]:
26
+ value = _run_network_command(["ipconfig", "getifaddr", interface])
27
+ if value:
28
+ local_ips[interface] = value
29
+
30
+ ifconfig_text = _run_network_command(["ifconfig"])
31
+ current_interface = ""
32
+ for line in ifconfig_text.splitlines():
33
+ if line and not line.startswith(("\t", " ")):
34
+ current_interface = line.split(":", 1)[0]
35
+ continue
36
+ match = re.search(r"\binet\s+(\d+\.\d+\.\d+\.\d+)\b", line)
37
+ if match and current_interface and match.group(1) != "127.0.0.1":
38
+ local_ips.setdefault(current_interface, match.group(1))
39
+
40
+ hostname = socket.gethostname()
41
+ guessed_ip = ""
42
+ try:
43
+ with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as sock:
44
+ sock.connect(("8.8.8.8", 80))
45
+ guessed_ip = sock.getsockname()[0]
46
+ except Exception:
47
+ pass
48
+ if guessed_ip and guessed_ip not in local_ips.values():
49
+ local_ips["default_route"] = guessed_ip
50
+
51
+ public_ip = _run_network_command(["curl", "-sS", "--max-time", "3", "https://api.ipify.org"])
52
+ wifi_info = _run_network_command(["networksetup", "-getinfo", "Wi-Fi"])
53
+
54
+ primary_local_ip = local_ips.get("en0") or local_ips.get("en1") or guessed_ip or ""
55
+ return {
56
+ "hostname": hostname,
57
+ "local_ip": primary_local_ip,
58
+ "local_ips": local_ips,
59
+ "public_ip": public_ip,
60
+ "wifi_info": wifi_info,
61
+ "ifconfig_available": bool(ifconfig_text),
62
+ "note": "local_ip은 같은 네트워크 안에서 보이는 내부 IP이고, public_ip는 인터넷에서 보이는 외부 IP입니다.",
63
+ }
64
+
65
+
66
+ _BLOCKED_FIND_FLAGS = {"-exec", "-execdir", "-delete", "-ok", "-okdir"}