aicortexpy 0.1.0__py3-none-any.whl
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.
- aicortexpy/__init__.py +3 -0
- aicortexpy/__main__.py +5 -0
- aicortexpy/acp_client.py +559 -0
- aicortexpy/acp_conn.py +853 -0
- aicortexpy/acp_errors.py +107 -0
- aicortexpy/agent_env.py +88 -0
- aicortexpy/artifact_ws.py +233 -0
- aicortexpy/artifacts.py +67 -0
- aicortexpy/attachments.py +85 -0
- aicortexpy/auto_commit.py +106 -0
- aicortexpy/capabilities.py +92 -0
- aicortexpy/chat_acp_pool.py +321 -0
- aicortexpy/cli.py +133 -0
- aicortexpy/client.py +438 -0
- aicortexpy/codex_config.py +98 -0
- aicortexpy/config.py +298 -0
- aicortexpy/daemon.py +677 -0
- aicortexpy/data/default_skills/agent-memory/SKILL.md +67 -0
- aicortexpy/data/default_skills/aicorkit-atlassian/SKILL.md +144 -0
- aicortexpy/data/default_skills/aicorkit-graph/SKILL.md +145 -0
- aicortexpy/data/default_skills/aicorkit-outlook/SKILL.md +87 -0
- aicortexpy/data/default_skills/aicorkit-wxmsg/SKILL.md +96 -0
- aicortexpy/data/default_skills/aicortex-basics/SKILL.md +10 -0
- aicortexpy/data/default_skills/aicortex-bridge-cdp/SKILL.md +118 -0
- aicortexpy/data/default_skills/aicortex-cli-ops/SKILL.md +100 -0
- aicortexpy/data/default_skills/aicortex-graph/SKILL.md +30 -0
- aicortexpy/data/default_skills/graphify/SKILL.md +97 -0
- aicortexpy/data/default_skills/question-form/SKILL.md +162 -0
- aicortexpy/data/default_skills/test-report-generator/SKILL.md +179 -0
- aicortexpy/data/default_skills/test-report-generator/scripts/generate_cases.py +146 -0
- aicortexpy/data/default_skills/test-report-generator/scripts/generate_report.py +275 -0
- aicortexpy/design_seed.py +59 -0
- aicortexpy/diskusage.py +209 -0
- aicortexpy/execenv.py +264 -0
- aicortexpy/gc.py +286 -0
- aicortexpy/handoff_bundle.py +65 -0
- aicortexpy/health.py +178 -0
- aicortexpy/local_skills.py +157 -0
- aicortexpy/models.py +385 -0
- aicortexpy/openclaw_config.py +153 -0
- aicortexpy/pinned_workdir.py +60 -0
- aicortexpy/platform_info.py +61 -0
- aicortexpy/poisoned.py +44 -0
- aicortexpy/process.py +168 -0
- aicortexpy/procutil.py +61 -0
- aicortexpy/prompt.py +235 -0
- aicortexpy/repo.py +462 -0
- aicortexpy/runner.py +969 -0
- aicortexpy/runtime_config.py +540 -0
- aicortexpy/sandbox.py +264 -0
- aicortexpy/stream.py +422 -0
- aicortexpy/terminal.py +147 -0
- aicortexpy/tunnel.py +82 -0
- aicortexpy/wakeup.py +222 -0
- aicortexpy-0.1.0.dist-info/METADATA +106 -0
- aicortexpy-0.1.0.dist-info/RECORD +58 -0
- aicortexpy-0.1.0.dist-info/WHEEL +5 -0
- aicortexpy-0.1.0.dist-info/top_level.txt +1 -0
aicortexpy/terminal.py
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"""Remote terminal sessions over wakeup WS (MVP; opt-in via aicortex_TERMINAL_PTY).
|
|
2
|
+
|
|
3
|
+
Uses a pipe-based shell session (not full ConPTY/unix pty) so Windows/macOS/Linux
|
|
4
|
+
share one code path. Good enough for command I/O; not a true interactive TTY.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import base64
|
|
10
|
+
import logging
|
|
11
|
+
import os
|
|
12
|
+
import subprocess
|
|
13
|
+
import threading
|
|
14
|
+
from typing import Any, Callable
|
|
15
|
+
|
|
16
|
+
from .procutil import kill_process_tree, popen_kwargs_for_killable_group
|
|
17
|
+
|
|
18
|
+
log = logging.getLogger("aicortexpy.terminal")
|
|
19
|
+
|
|
20
|
+
MAX_SESSIONS = 10
|
|
21
|
+
SendFn = Callable[[str, dict[str, Any]], None]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def terminal_pty_enabled() -> bool:
|
|
25
|
+
raw = (os.environ.get("aicortex_TERMINAL_PTY") or "").strip().lower()
|
|
26
|
+
return raw in ("1", "true", "yes", "on")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class TerminalManager:
|
|
30
|
+
def __init__(self, send: SendFn | None = None):
|
|
31
|
+
self._send = send
|
|
32
|
+
self._mu = threading.Lock()
|
|
33
|
+
self._sessions: dict[str, dict[str, Any]] = {}
|
|
34
|
+
|
|
35
|
+
def set_send(self, send: SendFn) -> None:
|
|
36
|
+
self._send = send
|
|
37
|
+
|
|
38
|
+
def handle_open(self, payload: dict[str, Any]) -> None:
|
|
39
|
+
if not terminal_pty_enabled():
|
|
40
|
+
self._error(payload, "terminal pty disabled (set aicortex_TERMINAL_PTY=true)")
|
|
41
|
+
return
|
|
42
|
+
sid = str(payload.get("session_id") or payload.get("id") or "").strip()
|
|
43
|
+
if not sid:
|
|
44
|
+
self._error(payload, "session id required")
|
|
45
|
+
return
|
|
46
|
+
with self._mu:
|
|
47
|
+
if len(self._sessions) >= MAX_SESSIONS:
|
|
48
|
+
self._error(payload, "too many terminal sessions")
|
|
49
|
+
return
|
|
50
|
+
if sid in self._sessions:
|
|
51
|
+
self._error(payload, "session already open")
|
|
52
|
+
return
|
|
53
|
+
cwd = str(payload.get("cwd") or os.getcwd())
|
|
54
|
+
shell = os.environ.get("COMSPEC") if os.name == "nt" else (os.environ.get("SHELL") or "/bin/bash")
|
|
55
|
+
try:
|
|
56
|
+
proc = subprocess.Popen(
|
|
57
|
+
[shell],
|
|
58
|
+
stdin=subprocess.PIPE,
|
|
59
|
+
stdout=subprocess.PIPE,
|
|
60
|
+
stderr=subprocess.STDOUT,
|
|
61
|
+
cwd=cwd if os.path.isdir(cwd) else None,
|
|
62
|
+
text=False,
|
|
63
|
+
bufsize=0,
|
|
64
|
+
**popen_kwargs_for_killable_group(),
|
|
65
|
+
)
|
|
66
|
+
except OSError as e:
|
|
67
|
+
self._error(payload, str(e))
|
|
68
|
+
return
|
|
69
|
+
with self._mu:
|
|
70
|
+
self._sessions[sid] = {"proc": proc}
|
|
71
|
+
threading.Thread(
|
|
72
|
+
target=self._reader, args=(sid, proc), name=f"term-{sid[:8]}", daemon=True
|
|
73
|
+
).start()
|
|
74
|
+
if self._send:
|
|
75
|
+
self._send("terminal:attached", {"session_id": sid})
|
|
76
|
+
|
|
77
|
+
def handle_data(self, payload: dict[str, Any]) -> None:
|
|
78
|
+
sid = str(payload.get("session_id") or payload.get("id") or "").strip()
|
|
79
|
+
data_b64 = str(payload.get("data") or "")
|
|
80
|
+
with self._mu:
|
|
81
|
+
sess = self._sessions.get(sid)
|
|
82
|
+
if not sess:
|
|
83
|
+
return
|
|
84
|
+
proc: subprocess.Popen = sess["proc"]
|
|
85
|
+
try:
|
|
86
|
+
raw = base64.b64decode(data_b64) if data_b64 else b""
|
|
87
|
+
if proc.stdin and raw:
|
|
88
|
+
proc.stdin.write(raw)
|
|
89
|
+
proc.stdin.flush()
|
|
90
|
+
except OSError as e:
|
|
91
|
+
self._error(payload, str(e))
|
|
92
|
+
|
|
93
|
+
def handle_resize(self, payload: dict[str, Any]) -> None:
|
|
94
|
+
# Pipe-based MVP has no PTY size; acknowledge silently.
|
|
95
|
+
return
|
|
96
|
+
|
|
97
|
+
def handle_close(self, payload: dict[str, Any]) -> None:
|
|
98
|
+
sid = str(payload.get("session_id") or payload.get("id") or "").strip()
|
|
99
|
+
with self._mu:
|
|
100
|
+
sess = self._sessions.pop(sid, None)
|
|
101
|
+
if not sess:
|
|
102
|
+
return
|
|
103
|
+
proc: subprocess.Popen = sess["proc"]
|
|
104
|
+
try:
|
|
105
|
+
kill_process_tree(proc.pid)
|
|
106
|
+
except Exception:
|
|
107
|
+
pass
|
|
108
|
+
|
|
109
|
+
def handle_attach(self, payload: dict[str, Any]) -> None:
|
|
110
|
+
sid = str(payload.get("session_id") or payload.get("id") or "").strip()
|
|
111
|
+
with self._mu:
|
|
112
|
+
exists = sid in self._sessions
|
|
113
|
+
if exists and self._send:
|
|
114
|
+
self._send("terminal:attached", {"session_id": sid})
|
|
115
|
+
elif not exists:
|
|
116
|
+
self._error(payload, "session not found")
|
|
117
|
+
|
|
118
|
+
def handle_detach(self, payload: dict[str, Any]) -> None:
|
|
119
|
+
return
|
|
120
|
+
|
|
121
|
+
def _reader(self, sid: str, proc: subprocess.Popen) -> None:
|
|
122
|
+
assert proc.stdout is not None
|
|
123
|
+
try:
|
|
124
|
+
while True:
|
|
125
|
+
chunk = proc.stdout.read(4096)
|
|
126
|
+
if not chunk:
|
|
127
|
+
break
|
|
128
|
+
if self._send:
|
|
129
|
+
self._send(
|
|
130
|
+
"terminal:data",
|
|
131
|
+
{
|
|
132
|
+
"session_id": sid,
|
|
133
|
+
"data": base64.b64encode(chunk).decode("ascii"),
|
|
134
|
+
},
|
|
135
|
+
)
|
|
136
|
+
except Exception as e:
|
|
137
|
+
log.debug("terminal reader %s: %s", sid, e)
|
|
138
|
+
finally:
|
|
139
|
+
with self._mu:
|
|
140
|
+
self._sessions.pop(sid, None)
|
|
141
|
+
if self._send:
|
|
142
|
+
self._send("terminal:close", {"session_id": sid})
|
|
143
|
+
|
|
144
|
+
def _error(self, payload: dict[str, Any], message: str) -> None:
|
|
145
|
+
sid = str(payload.get("session_id") or payload.get("id") or "")
|
|
146
|
+
if self._send:
|
|
147
|
+
self._send("terminal:error", {"session_id": sid, "error": message})
|
aicortexpy/tunnel.py
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""Loopback HTTP tunnel proxy (Go daemon/tunnel/proxy.go)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import base64
|
|
6
|
+
import json
|
|
7
|
+
import logging
|
|
8
|
+
import threading
|
|
9
|
+
import urllib.error
|
|
10
|
+
import urllib.request
|
|
11
|
+
from typing import Any, Callable
|
|
12
|
+
|
|
13
|
+
log = logging.getLogger("aicortexpy.tunnel")
|
|
14
|
+
|
|
15
|
+
MAX_BODY = 2 * 1024 * 1024
|
|
16
|
+
PROXY_TIMEOUT = 30.0
|
|
17
|
+
_HOP_BY_HOP = {
|
|
18
|
+
"connection",
|
|
19
|
+
"keep-alive",
|
|
20
|
+
"proxy-authenticate",
|
|
21
|
+
"proxy-authorization",
|
|
22
|
+
"te",
|
|
23
|
+
"trailers",
|
|
24
|
+
"transfer-encoding",
|
|
25
|
+
"upgrade",
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
SendFn = Callable[[str, dict[str, Any]], None]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class TunnelProxy:
|
|
32
|
+
def __init__(self, send: SendFn | None = None):
|
|
33
|
+
self._send = send
|
|
34
|
+
|
|
35
|
+
def set_send(self, send: SendFn) -> None:
|
|
36
|
+
self._send = send
|
|
37
|
+
|
|
38
|
+
def handle_request(self, payload: dict[str, Any]) -> None:
|
|
39
|
+
threading.Thread(
|
|
40
|
+
target=self._handle, args=(payload,), name="tunnel-req", daemon=True
|
|
41
|
+
).start()
|
|
42
|
+
|
|
43
|
+
def _handle(self, payload: dict[str, Any]) -> None:
|
|
44
|
+
req_id = str(payload.get("request_id") or payload.get("requestId") or "")
|
|
45
|
+
resp: dict[str, Any] = {"request_id": req_id}
|
|
46
|
+
try:
|
|
47
|
+
port = int(payload.get("port") or 0)
|
|
48
|
+
if port < 1 or port > 65535:
|
|
49
|
+
raise ValueError(f"invalid port {port}")
|
|
50
|
+
method = str(payload.get("method") or "GET").upper()
|
|
51
|
+
path = str(payload.get("path") or "/")
|
|
52
|
+
if not path.startswith("/"):
|
|
53
|
+
path = "/" + path
|
|
54
|
+
body_b64 = str(payload.get("body") or "")
|
|
55
|
+
body = base64.b64decode(body_b64) if body_b64 else b""
|
|
56
|
+
if len(body) > MAX_BODY:
|
|
57
|
+
raise ValueError("request body too large")
|
|
58
|
+
headers = payload.get("headers") if isinstance(payload.get("headers"), dict) else {}
|
|
59
|
+
url = f"http://127.0.0.1:{port}{path}"
|
|
60
|
+
req = urllib.request.Request(url, data=body or None, method=method)
|
|
61
|
+
for k, v in headers.items():
|
|
62
|
+
if str(k).lower() in _HOP_BY_HOP:
|
|
63
|
+
continue
|
|
64
|
+
req.add_header(str(k), str(v))
|
|
65
|
+
with urllib.request.urlopen(req, timeout=PROXY_TIMEOUT) as http_resp:
|
|
66
|
+
raw = http_resp.read(MAX_BODY + 1)
|
|
67
|
+
if len(raw) > MAX_BODY:
|
|
68
|
+
raise ValueError("response body too large")
|
|
69
|
+
resp["status"] = http_resp.status
|
|
70
|
+
resp_headers: dict[str, str] = {}
|
|
71
|
+
for k, v in http_resp.headers.items():
|
|
72
|
+
if k.lower() in _HOP_BY_HOP:
|
|
73
|
+
continue
|
|
74
|
+
resp_headers[k] = v
|
|
75
|
+
resp["headers"] = resp_headers
|
|
76
|
+
if raw:
|
|
77
|
+
resp["body"] = base64.b64encode(raw).decode("ascii")
|
|
78
|
+
except Exception as e:
|
|
79
|
+
resp["error"] = str(e)
|
|
80
|
+
log.debug("tunnel request failed id=%s: %s", req_id, e)
|
|
81
|
+
if self._send:
|
|
82
|
+
self._send("tunnel:response", resp)
|
aicortexpy/wakeup.py
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
"""WebSocket task-wakeup + tunnel/artifact/terminal mux (poll fallback remains)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import logging
|
|
7
|
+
import threading
|
|
8
|
+
import time
|
|
9
|
+
from typing import Any, Callable
|
|
10
|
+
from urllib.parse import urlencode, urlparse, urlunparse
|
|
11
|
+
|
|
12
|
+
from .artifact_ws import ArtifactBrowser
|
|
13
|
+
from .platform_info import host_os
|
|
14
|
+
from .terminal import TerminalManager, terminal_pty_enabled
|
|
15
|
+
from .tunnel import TunnelProxy
|
|
16
|
+
|
|
17
|
+
log = logging.getLogger("aicortexpy.wakeup")
|
|
18
|
+
|
|
19
|
+
EVENT_TASK_AVAILABLE = "daemon:task_available"
|
|
20
|
+
EVENT_HEARTBEAT = "daemon:heartbeat"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def task_wakeup_url(base_url: str, runtime_ids: list[str]) -> str:
|
|
24
|
+
u = urlparse(base_url.strip())
|
|
25
|
+
scheme = u.scheme.lower()
|
|
26
|
+
if scheme == "http":
|
|
27
|
+
scheme = "ws"
|
|
28
|
+
elif scheme == "https":
|
|
29
|
+
scheme = "wss"
|
|
30
|
+
elif scheme not in ("ws", "wss"):
|
|
31
|
+
raise ValueError(f"server URL must be http(s)/ws(s), got {u.scheme!r}")
|
|
32
|
+
path = (u.path or "").rstrip("/") + "/api/daemon/ws"
|
|
33
|
+
ids = sorted({r for r in runtime_ids if r})
|
|
34
|
+
query = urlencode({"runtime_ids": ",".join(ids)})
|
|
35
|
+
return urlunparse((scheme, u.netloc, path, "", query, ""))
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class WakeupHub:
|
|
39
|
+
"""Owns the daemon WS connection write mux + sideband handlers."""
|
|
40
|
+
|
|
41
|
+
def __init__(self) -> None:
|
|
42
|
+
self._ws = None
|
|
43
|
+
self._write_mu = threading.Lock()
|
|
44
|
+
self.tunnel = TunnelProxy(self.send)
|
|
45
|
+
self.artifacts = ArtifactBrowser(self.send)
|
|
46
|
+
self.terminal = TerminalManager(self.send)
|
|
47
|
+
|
|
48
|
+
def bind_ws(self, ws: Any) -> None:
|
|
49
|
+
self._ws = ws
|
|
50
|
+
self.tunnel.set_send(self.send)
|
|
51
|
+
self.artifacts.set_send(self.send)
|
|
52
|
+
self.terminal.set_send(self.send)
|
|
53
|
+
|
|
54
|
+
def clear_ws(self) -> None:
|
|
55
|
+
self._ws = None
|
|
56
|
+
|
|
57
|
+
def send(self, typ: str, payload: dict[str, Any]) -> None:
|
|
58
|
+
ws = self._ws
|
|
59
|
+
if ws is None:
|
|
60
|
+
return
|
|
61
|
+
msg = {"type": typ, "payload": payload}
|
|
62
|
+
raw = json.dumps(msg, ensure_ascii=False)
|
|
63
|
+
with self._write_mu:
|
|
64
|
+
try:
|
|
65
|
+
ws.send(raw)
|
|
66
|
+
except Exception as e:
|
|
67
|
+
log.debug("wakeup send failed type=%s: %s", typ, e)
|
|
68
|
+
|
|
69
|
+
def dispatch(self, msg: dict[str, Any], wake: threading.Event) -> None:
|
|
70
|
+
typ = str(msg.get("type") or "")
|
|
71
|
+
payload = msg.get("payload")
|
|
72
|
+
if isinstance(payload, str):
|
|
73
|
+
try:
|
|
74
|
+
payload = json.loads(payload)
|
|
75
|
+
except json.JSONDecodeError:
|
|
76
|
+
payload = {}
|
|
77
|
+
if not isinstance(payload, dict):
|
|
78
|
+
payload = msg if typ and typ != EVENT_TASK_AVAILABLE else {}
|
|
79
|
+
|
|
80
|
+
if typ == EVENT_TASK_AVAILABLE:
|
|
81
|
+
log.debug("task wakeup received")
|
|
82
|
+
wake.set()
|
|
83
|
+
return
|
|
84
|
+
if typ == "tunnel:request":
|
|
85
|
+
self.tunnel.handle_request(payload)
|
|
86
|
+
return
|
|
87
|
+
if typ == "artifact:request":
|
|
88
|
+
self.artifacts.handle_request(payload)
|
|
89
|
+
return
|
|
90
|
+
if typ.startswith("terminal:"):
|
|
91
|
+
handlers = {
|
|
92
|
+
"terminal:open": self.terminal.handle_open,
|
|
93
|
+
"terminal:attach": self.terminal.handle_attach,
|
|
94
|
+
"terminal:data": self.terminal.handle_data,
|
|
95
|
+
"terminal:resize": self.terminal.handle_resize,
|
|
96
|
+
"terminal:detach": self.terminal.handle_detach,
|
|
97
|
+
"terminal:close": self.terminal.handle_close,
|
|
98
|
+
}
|
|
99
|
+
fn = handlers.get(typ)
|
|
100
|
+
if fn:
|
|
101
|
+
fn(payload)
|
|
102
|
+
return
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def run_wakeup_loop(
|
|
106
|
+
*,
|
|
107
|
+
stop: threading.Event,
|
|
108
|
+
wake: threading.Event,
|
|
109
|
+
get_runtime_ids: Callable[[], list[str]],
|
|
110
|
+
base_url: str,
|
|
111
|
+
token: str,
|
|
112
|
+
version: str = "",
|
|
113
|
+
hub: WakeupHub | None = None,
|
|
114
|
+
) -> None:
|
|
115
|
+
"""Connect to daemon WS; set `wake` when task_available arrives."""
|
|
116
|
+
try:
|
|
117
|
+
import websocket # type: ignore[import-untyped]
|
|
118
|
+
except ImportError:
|
|
119
|
+
log.warning(
|
|
120
|
+
"websocket-client not installed; wakeup WS disabled "
|
|
121
|
+
"(pip install websocket-client). Polling still works."
|
|
122
|
+
)
|
|
123
|
+
stop.wait()
|
|
124
|
+
return
|
|
125
|
+
|
|
126
|
+
hub = hub or WakeupHub()
|
|
127
|
+
backoff = 1.0
|
|
128
|
+
|
|
129
|
+
while not stop.is_set():
|
|
130
|
+
ids = get_runtime_ids()
|
|
131
|
+
if not ids:
|
|
132
|
+
stop.wait(5.0)
|
|
133
|
+
continue
|
|
134
|
+
id_key = tuple(sorted(ids))
|
|
135
|
+
try:
|
|
136
|
+
url = task_wakeup_url(base_url, ids)
|
|
137
|
+
except ValueError as e:
|
|
138
|
+
log.warning("wakeup URL invalid: %s", e)
|
|
139
|
+
stop.wait(30.0)
|
|
140
|
+
continue
|
|
141
|
+
|
|
142
|
+
headers = [
|
|
143
|
+
f"Authorization: Bearer {token}",
|
|
144
|
+
"X-Client-Platform: daemon",
|
|
145
|
+
f"X-Client-OS: {host_os()}",
|
|
146
|
+
]
|
|
147
|
+
if version:
|
|
148
|
+
headers.append(f"X-Client-Version: {version}")
|
|
149
|
+
|
|
150
|
+
closed = threading.Event()
|
|
151
|
+
|
|
152
|
+
def on_message(_ws: object, message: str | bytes) -> None:
|
|
153
|
+
try:
|
|
154
|
+
raw = message.decode("utf-8") if isinstance(message, bytes) else message
|
|
155
|
+
msg = json.loads(raw)
|
|
156
|
+
except Exception:
|
|
157
|
+
return
|
|
158
|
+
if not isinstance(msg, dict):
|
|
159
|
+
return
|
|
160
|
+
hub.dispatch(msg, wake)
|
|
161
|
+
|
|
162
|
+
def on_error(_ws: object, error: object) -> None:
|
|
163
|
+
log.debug("wakeup ws error: %s", error)
|
|
164
|
+
|
|
165
|
+
def on_close(_ws: object, *_args: object) -> None:
|
|
166
|
+
hub.clear_ws()
|
|
167
|
+
closed.set()
|
|
168
|
+
|
|
169
|
+
def on_open(ws: object) -> None:
|
|
170
|
+
nonlocal backoff
|
|
171
|
+
backoff = 1.0
|
|
172
|
+
hub.bind_ws(ws)
|
|
173
|
+
log.info(
|
|
174
|
+
"task wakeup websocket connected runtimes=%s terminal_pty=%s",
|
|
175
|
+
len(ids),
|
|
176
|
+
terminal_pty_enabled(),
|
|
177
|
+
)
|
|
178
|
+
wake.set()
|
|
179
|
+
# Periodic WS heartbeat (best-effort; server may ignore).
|
|
180
|
+
def _hb() -> None:
|
|
181
|
+
while not stop.is_set() and not closed.is_set():
|
|
182
|
+
hub.send(EVENT_HEARTBEAT, {"ts": int(time.time())})
|
|
183
|
+
stop.wait(25.0)
|
|
184
|
+
|
|
185
|
+
threading.Thread(target=_hb, name="wakeup-hb", daemon=True).start()
|
|
186
|
+
|
|
187
|
+
ws_app = websocket.WebSocketApp(
|
|
188
|
+
url,
|
|
189
|
+
header=headers,
|
|
190
|
+
on_message=on_message,
|
|
191
|
+
on_error=on_error,
|
|
192
|
+
on_close=on_close,
|
|
193
|
+
on_open=on_open,
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
def _run() -> None:
|
|
197
|
+
ws_app.run_forever(ping_interval=30, ping_timeout=10)
|
|
198
|
+
|
|
199
|
+
t = threading.Thread(target=_run, name="wakeup-ws", daemon=True)
|
|
200
|
+
t.start()
|
|
201
|
+
|
|
202
|
+
while not stop.is_set() and not closed.is_set():
|
|
203
|
+
if tuple(sorted(get_runtime_ids())) != id_key:
|
|
204
|
+
try:
|
|
205
|
+
ws_app.close()
|
|
206
|
+
except Exception:
|
|
207
|
+
pass
|
|
208
|
+
break
|
|
209
|
+
stop.wait(1.0)
|
|
210
|
+
|
|
211
|
+
try:
|
|
212
|
+
ws_app.close()
|
|
213
|
+
except Exception:
|
|
214
|
+
pass
|
|
215
|
+
hub.clear_ws()
|
|
216
|
+
t.join(timeout=5.0)
|
|
217
|
+
|
|
218
|
+
if stop.is_set():
|
|
219
|
+
return
|
|
220
|
+
log.debug("wakeup ws reconnect in %.1fs", backoff)
|
|
221
|
+
stop.wait(backoff)
|
|
222
|
+
backoff = min(backoff * 2, 30.0)
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
Metadata-Version: 2.2
|
|
2
|
+
Name: aicortexpy
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: AICortex Python local agent runtime daemon (no Docker)
|
|
5
|
+
Author: AICortex
|
|
6
|
+
License: Apache-2.0
|
|
7
|
+
Project-URL: Homepage, https://gitee.com/seacozz2007/aicortex
|
|
8
|
+
Project-URL: Repository, https://gitee.com/seacozz2007/aicortex
|
|
9
|
+
Project-URL: Issues, https://gitee.com/seacozz2007/aicortex/issues
|
|
10
|
+
Requires-Python: >=3.10
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
Requires-Dist: websocket-client>=1.6
|
|
13
|
+
Provides-Extra: dev
|
|
14
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
15
|
+
Requires-Dist: build>=1.0; extra == "dev"
|
|
16
|
+
Requires-Dist: twine>=5.0; extra == "dev"
|
|
17
|
+
|
|
18
|
+
# aicortexpy(Python daemon)
|
|
19
|
+
|
|
20
|
+
用已批准的 `python.exe` 跑本地 agent runtime(跨 Windows / macOS / Linux),**无需 Docker**。
|
|
21
|
+
|
|
22
|
+
| | 名称 |
|
|
23
|
+
|--|--|
|
|
24
|
+
| PyPI | `aicortexpy`(`aicortex` 与已有包 `ai-cortex` 过近被拒) |
|
|
25
|
+
| 模块 | `aicortexpy`(不能叫 `aicortex`:本仓库目录也叫 `aicortex`,在父目录执行会撞 PEP 420 命名空间包) |
|
|
26
|
+
| 调用 | `python -m aicortexpy`(不注册 console script,避免与 Go CLI `aicortex` 抢 PATH) |
|
|
27
|
+
|
|
28
|
+
```powershell
|
|
29
|
+
pip install aicortexpy
|
|
30
|
+
python -m aicortexpy --help
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## 安装
|
|
34
|
+
|
|
35
|
+
### 从 PyPI
|
|
36
|
+
|
|
37
|
+
```powershell
|
|
38
|
+
pip install -U aicortexpy
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### 从源码(本仓库)
|
|
42
|
+
|
|
43
|
+
```powershell
|
|
44
|
+
cd python-daemon
|
|
45
|
+
pip install -e .
|
|
46
|
+
pip install -e ".[dev]" # 含 build / twine
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## 运行
|
|
50
|
+
|
|
51
|
+
```powershell
|
|
52
|
+
# 方式 A:经 Go aicortex CLI(推荐;需已 pip 安装本包)
|
|
53
|
+
$env:AICORTEX_DAEMON_BACKEND = "python"
|
|
54
|
+
aicortex daemon start
|
|
55
|
+
aicortex daemon status
|
|
56
|
+
aicortex daemon stop
|
|
57
|
+
|
|
58
|
+
# 方式 B:直接启动
|
|
59
|
+
python -m aicortexpy start --foreground
|
|
60
|
+
python -m aicortexpy capabilities
|
|
61
|
+
python -m aicortexpy --version
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
需要 `~/.aicortex/config.json`(token + server_url)以及 PATH 上的 agent CLI。
|
|
65
|
+
|
|
66
|
+
Health 默认端口 **19514**(与 Go / `aicortex daemon status` 一致)。**不要**与 Go daemon 同时跑在同一端口。
|
|
67
|
+
|
|
68
|
+
## 发布到 PyPI(对齐 aicorkit / aicortex-bridge)
|
|
69
|
+
|
|
70
|
+
在 `python-daemon/` 目录:
|
|
71
|
+
|
|
72
|
+
```powershell
|
|
73
|
+
python -m pip install -U build twine
|
|
74
|
+
python -m build
|
|
75
|
+
python -m twine upload dist/*
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
版本号改 `pyproject.toml` 的 `version` 与 `aicortexpy/__init__.py` 的 `__version__`(保持一致),再重新 build / upload。
|
|
79
|
+
|
|
80
|
+
## 已对齐能力(相对 Go daemon)
|
|
81
|
+
|
|
82
|
+
- 注册/心跳/wakeup/claim/cancel、agentEnv、prior_work_dir 复用
|
|
83
|
+
- Claude(stream-json + **MCP `--mcp-config`**)、Codex(+ semantic inactivity)
|
|
84
|
+
- Cursor/Gemini/OpenCode/Copilot **stream-json/json 事件流**
|
|
85
|
+
- Cursor/Kiro chat ACP 池;Kiro/Hermes/Kimi/Dsh ACP oneshot
|
|
86
|
+
- OpenClaw / pi 启动参数;OpenClaw config **fail-closed**
|
|
87
|
+
- Repo:**allowlist** + per-workspace bare + **worktree 锁** + co-authored hook
|
|
88
|
+
- Design:**handoff zip** + **example seed**
|
|
89
|
+
- Model list:**ACP discovery** + 60s cache
|
|
90
|
+
- GC:`.gc_meta.json` + **active-root 保护**
|
|
91
|
+
- Wakeup 侧带:**tunnel**、**artifact browse/chunk upload**、terminal(pipe MVP,需 `AICORTEX_DAEMON_TERMINAL_PTY=true`)
|
|
92
|
+
|
|
93
|
+
**仍偏薄 / partial**:真正的 ConPTY/unix PTY(现为 pipe shell)、OS sandbox wrapper(bwrap/Seatbelt)、Go 式 auto-update(用 `pip install -U aicortexpy`)。
|
|
94
|
+
|
|
95
|
+
## 环境变量(节选)
|
|
96
|
+
|
|
97
|
+
| 变量 | 含义 |
|
|
98
|
+
|------|------|
|
|
99
|
+
| `AICORTEX_DAEMON_BACKEND` | `python` 时 `aicortex daemon start` 启 Python daemon |
|
|
100
|
+
| `AICORTEX_DAEMON_HEALTH_PORT` | 默认 19514 |
|
|
101
|
+
| `AICORTEX_DAEMON_TERMINAL_PTY` | `true` 启用远程终端(pipe MVP) |
|
|
102
|
+
| `AICORTEX_AGENT_IDLE_WATCHDOG` | 无活动强停秒数(默认 300;`0` 关闭) |
|
|
103
|
+
| `AICORTEX_CODEX_SEMANTIC_INACTIVITY_TIMEOUT` | Codex 无输出强停(默认 600;`0` 关闭) |
|
|
104
|
+
| `AICORTEX_CODEX_MULTI_AGENT` | `1` 保留 Codex native multi_agent |
|
|
105
|
+
| `AICORTEX_INJECT_TENCENT_DOCS` | `1` 注入完整 tencent-docs 包 |
|
|
106
|
+
| `AICORTEX_GC_*` | GC 开关与 TTL |
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
aicortexpy/__init__.py,sha256=JLvtEW8nsZgV2d81Kd4lekdVD3AsBXgF8GH6ZHM6IF0,76
|
|
2
|
+
aicortexpy/__main__.py,sha256=0JBt6BfnwPz39k7xe6r7RsoyW5_XlsefeixJPkuheW0,101
|
|
3
|
+
aicortexpy/acp_client.py,sha256=-pf5QwvZbHUW7a7oKVVRohbDOvfKiOTC5wWhL58pCcY,20424
|
|
4
|
+
aicortexpy/acp_conn.py,sha256=JVVByMD3leiIRbJRD_n8jpkbulMAiwfAbVZj9Tbytv0,28933
|
|
5
|
+
aicortexpy/acp_errors.py,sha256=3kJwwHvk6H8EjVWOU0yj3Uc9rMVF3F05qqfSY5cnVt8,3796
|
|
6
|
+
aicortexpy/agent_env.py,sha256=w54dZS-g5lZNWejnk2wS3UgP9eCdwktAlqm0YotmLU8,2519
|
|
7
|
+
aicortexpy/artifact_ws.py,sha256=KZXSsq1MPiD5nUzTI27vEUoZSbjKLOslJ5HM8VpuzQ4,9385
|
|
8
|
+
aicortexpy/artifacts.py,sha256=0Q-dMq3m_nK7rqpAIAsVTtPQ0TV0AztmSgT8gu3qkv8,1724
|
|
9
|
+
aicortexpy/attachments.py,sha256=khowvPc2jMuDWzhbXTOOOTieiOVnSuoCeispAsE70WE,2547
|
|
10
|
+
aicortexpy/auto_commit.py,sha256=s-Hk1Wk1npk4AaMov0PS4D4_QVDvpzi378Viuqdw86U,3006
|
|
11
|
+
aicortexpy/capabilities.py,sha256=TC8xIYYWOyzg_OfiBJ2MmyK8wFCNnrJ75tOmPD__NEs,6055
|
|
12
|
+
aicortexpy/chat_acp_pool.py,sha256=wYL986M-PJzEAaEKJ5IA19tzu72HuUDsWfEgoaVcvdk,10674
|
|
13
|
+
aicortexpy/cli.py,sha256=W0qTKd7f3tu7Z3K6Exixki1TKS9P-i6njhicN8JxFIk,4315
|
|
14
|
+
aicortexpy/client.py,sha256=7PbG6c-_o86JO7ar85XbP9Q6WcTJQKHZk45-IQICuws,14282
|
|
15
|
+
aicortexpy/codex_config.py,sha256=TNusK1gD0Df9B5ErrSr22peOiZ8MUdLpj8kpc6SncRM,3559
|
|
16
|
+
aicortexpy/config.py,sha256=l4jtSl6ircxk-95LzxZjZv7X8D5OP3W38e13p3vYGWE,10665
|
|
17
|
+
aicortexpy/daemon.py,sha256=FHjVFwTGYtDNvFvsrZibEKA7TXwtMcGFFr8vFqCG388,25283
|
|
18
|
+
aicortexpy/design_seed.py,sha256=GT-7YFoxdx7yBzHK9aV6p1KLmXHoMDhl5RNRjZXTsN8,1981
|
|
19
|
+
aicortexpy/diskusage.py,sha256=lPTxRk6Wez4BRAQLeXlMF4BsLtkvEnKFx5_Iddhu71w,6712
|
|
20
|
+
aicortexpy/execenv.py,sha256=ILnGTDEmtP-XgIIGiNGR6zSEuid9oIUYCq_TFYfC6jg,9639
|
|
21
|
+
aicortexpy/gc.py,sha256=8h1KewYIzdL1NXVX4-WG0owt_P2iD3Lr3ilja-aPPkc,8754
|
|
22
|
+
aicortexpy/handoff_bundle.py,sha256=2DNm4QyiRbVP-J1qbhknoep7Odio5Gyzwrq03BWIcdY,2205
|
|
23
|
+
aicortexpy/health.py,sha256=7btOmCmbW22ZU2Yz7f8JZOQ3m8R7W1DpBnKblbmnTyI,6838
|
|
24
|
+
aicortexpy/local_skills.py,sha256=J7E9ey_hSEnbACG-4b_gfOdLXP3RRyuXDcQ_Xfz1WA8,4968
|
|
25
|
+
aicortexpy/models.py,sha256=4WLGvDc1apbf9SD0gddulgpNNH5lkRUssga1QYxpJN8,12498
|
|
26
|
+
aicortexpy/openclaw_config.py,sha256=FxQlAHcspbRUUMHFrn_bdG8xrXqxl9yGYA-CCbYFnws,5229
|
|
27
|
+
aicortexpy/pinned_workdir.py,sha256=6PHrFJ9YlSvnDEiqs6RSB7dgmKeizgHC9Un-eSADB3I,1689
|
|
28
|
+
aicortexpy/platform_info.py,sha256=AlFztNUlZC4OclTSXjpGd_UwHb-87dQSj58NWObzKfc,1441
|
|
29
|
+
aicortexpy/poisoned.py,sha256=6P6CTi7eTxiJD_709xfNgeO_2qA-A1bQ525P2MNhYEw,1400
|
|
30
|
+
aicortexpy/process.py,sha256=TWX7h9Cp1_lOJ_m0C8EK6_dbcAVmqWFvKj9jrzyrgII,5055
|
|
31
|
+
aicortexpy/procutil.py,sha256=_EftI5v9Yer7T5exd7bPIpLxR5x08tRvu_5C9Mn6gEQ,1596
|
|
32
|
+
aicortexpy/prompt.py,sha256=1s5R7jLPY-n9zmTL1jfPPDDFflLeNymJf5S8WsQHVxM,10280
|
|
33
|
+
aicortexpy/repo.py,sha256=tnNh96q2_UcV3yELOLDufTCkrg9gs81Vmq9WGl-Or6c,15513
|
|
34
|
+
aicortexpy/runner.py,sha256=Aei1m5JUUYwpMP6yYN3xd4VPm5Z22XkP0o3gyB6nPPk,31268
|
|
35
|
+
aicortexpy/runtime_config.py,sha256=W94jPYXP7A7v1-QT6Br_ixlKAB176Ior1RbxUm2YDCI,22725
|
|
36
|
+
aicortexpy/sandbox.py,sha256=jVhByEcYxXcw7nEj2mTl6FyoU8cQ1TA9WxyAxmdtNiQ,9114
|
|
37
|
+
aicortexpy/stream.py,sha256=T7XyYxIoPTdXBTxzP3DUVjPJqTM5KXDtvFy0F8mUXgI,17503
|
|
38
|
+
aicortexpy/terminal.py,sha256=Ml3i0IFJu8ZjSU5S4q8tptV4jBFHdOr8Q0LLx6rV9nk,5405
|
|
39
|
+
aicortexpy/tunnel.py,sha256=A4Va42KxdxSQuVZhNK5MU1N_DTzLOtOAlYweGXaHPEA,3010
|
|
40
|
+
aicortexpy/wakeup.py,sha256=3Rqjs4fkSHw4c3R8Tikm6WZR8u6AfAIWoxsbpwDrB3U,7231
|
|
41
|
+
aicortexpy/data/default_skills/agent-memory/SKILL.md,sha256=c1oEhINeduE8UV5T27RPBWfB0l65U1WoOpOgMu_EGfQ,2992
|
|
42
|
+
aicortexpy/data/default_skills/aicorkit-atlassian/SKILL.md,sha256=J9axzhdu-G5O1Crr0488S79UKt4IHD82iY-PCQ-VEY8,4939
|
|
43
|
+
aicortexpy/data/default_skills/aicorkit-graph/SKILL.md,sha256=d438JVjCCiX8V8hkAtQdz3uUfRREG1vuoXFWZvGeQp4,6011
|
|
44
|
+
aicortexpy/data/default_skills/aicorkit-outlook/SKILL.md,sha256=rR8RyNbIx3eK0ktRvxB5Xj4JBnWsu5fY1vLeA3diF0U,3221
|
|
45
|
+
aicortexpy/data/default_skills/aicorkit-wxmsg/SKILL.md,sha256=ku8sBuVyy3h4K6L5B5GZjnOEFnfBhZeYAvsSiJWxhWA,3563
|
|
46
|
+
aicortexpy/data/default_skills/aicortex-basics/SKILL.md,sha256=1_9fhqvBVy5_Ml7LdVfXVmSiMERoOzCndN8wWbI52gA,385
|
|
47
|
+
aicortexpy/data/default_skills/aicortex-bridge-cdp/SKILL.md,sha256=7cr078IzMq4nVmefok7DJ5YsjSKOI051fCH53sJWaso,4198
|
|
48
|
+
aicortexpy/data/default_skills/aicortex-cli-ops/SKILL.md,sha256=YrFCxogq-rKPa8IY6PIv_Yk_qha6hN0SBbQ9HNYyZ_M,3333
|
|
49
|
+
aicortexpy/data/default_skills/aicortex-graph/SKILL.md,sha256=oiFyZRijBYK-Nfw0Yl2sbYljwlsR7DvnUnn6QdkRtUA,1389
|
|
50
|
+
aicortexpy/data/default_skills/graphify/SKILL.md,sha256=rRF9IZ2WGoAe6vpIena8eUpKRaKojElsjBexQndmHHE,4861
|
|
51
|
+
aicortexpy/data/default_skills/question-form/SKILL.md,sha256=pucPRblzVLHxugONJ1p-e7qgFCS8XAUEiHTQ6nj9bVY,5680
|
|
52
|
+
aicortexpy/data/default_skills/test-report-generator/SKILL.md,sha256=-Oe8UdgCZvbeb-2Rk14N9D60NULvJredWmb-HL-X0Qw,8705
|
|
53
|
+
aicortexpy/data/default_skills/test-report-generator/scripts/generate_cases.py,sha256=hfwgujxS6d-FkaSwjEWIgsP2XFWsHglawtee9gwW8yw,5442
|
|
54
|
+
aicortexpy/data/default_skills/test-report-generator/scripts/generate_report.py,sha256=AD4CONpoidzpZStSYZaHw16_3pijyeKWl8y0lqpyjVY,10102
|
|
55
|
+
aicortexpy-0.1.0.dist-info/METADATA,sha256=W_ZFodUFIf7JfR0G3XXjwAubAO8v2ePPd1gf57El14k,3917
|
|
56
|
+
aicortexpy-0.1.0.dist-info/WHEEL,sha256=beeZ86-EfXScwlR_HKu4SllMC9wUEj_8Z_4FJ3egI2w,91
|
|
57
|
+
aicortexpy-0.1.0.dist-info/top_level.txt,sha256=5qT2YcsQBb-n7HvUk94dAgMOStEeRDJIlDFuEZ7Q5To,11
|
|
58
|
+
aicortexpy-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
aicortexpy
|