agent-runtime-py 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.
@@ -0,0 +1,219 @@
1
+ """Detached shell observations and a durable event ledger independent of model turns."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import math
7
+ import os
8
+ import subprocess
9
+ import sys
10
+ import time
11
+ import uuid
12
+ from pathlib import Path
13
+ from typing import Any
14
+
15
+ import psutil
16
+
17
+ from agent_runtime.claude import process_alive
18
+ from agent_runtime.sessions import Registry, current_identity, permission_class
19
+
20
+
21
+ class Monitors:
22
+ def __init__(self, directory: Path | None = None) -> None:
23
+ self.registry = Registry(directory)
24
+ self.store = self.registry.store
25
+ with self.store.connect() as db:
26
+ db.executescript("""
27
+ CREATE TABLE IF NOT EXISTS monitors (
28
+ id TEXT PRIMARY KEY, session_id TEXT, value TEXT);
29
+ CREATE TABLE IF NOT EXISTS monitor_events (
30
+ id INTEGER PRIMARY KEY AUTOINCREMENT, monitor_id TEXT, session_id TEXT,
31
+ text TEXT, status TEXT, created REAL, error TEXT
32
+ );
33
+ CREATE INDEX IF NOT EXISTS monitor_delivery ON monitor_events(session_id,status,id);
34
+ """)
35
+
36
+ def get(self, monitor_id: str) -> dict[str, Any]:
37
+ with self.store.connect() as db:
38
+ row = db.execute("SELECT value FROM monitors WHERE id=?", (monitor_id,)).fetchone()
39
+ if row is None:
40
+ raise KeyError("unknown monitor")
41
+ value = json.loads(row[0])
42
+ if (
43
+ value["status"] in {"running", "starting"}
44
+ and value.get("pid")
45
+ and not process_alive(value)
46
+ ):
47
+ value = self.update(monitor_id, status="interrupted", error="monitor worker exited")
48
+ if (
49
+ value["status"] == "starting"
50
+ and not value.get("pid")
51
+ and time.time() - value["created_at"] > 60
52
+ ):
53
+ value = self.update(
54
+ monitor_id, status="interrupted", error="monitor worker did not start"
55
+ )
56
+ return value
57
+
58
+ def update(self, monitor_id: str, **fields: Any) -> dict[str, Any]:
59
+ with self.store.connect() as db:
60
+ db.execute("BEGIN IMMEDIATE")
61
+ row = db.execute("SELECT value FROM monitors WHERE id=?", (monitor_id,)).fetchone()
62
+ if row is None:
63
+ raise KeyError("unknown monitor")
64
+ value = {**json.loads(row[0]), **fields, "updated_at": time.time()}
65
+ db.execute("UPDATE monitors SET value=? WHERE id=?", (json.dumps(value), monitor_id))
66
+ return value
67
+
68
+ def list(self, session_id: str) -> list[dict[str, Any]]:
69
+ with self.store.connect() as db:
70
+ ids = [
71
+ row[0]
72
+ for row in db.execute("SELECT id FROM monitors WHERE session_id=?", (session_id,))
73
+ ]
74
+ return [self.get(identity) for identity in ids]
75
+
76
+ def start(
77
+ self,
78
+ command: str,
79
+ *,
80
+ description: str,
81
+ persistent: bool = False,
82
+ cwd: Path | None = None,
83
+ timeout_ms: int = 300_000,
84
+ ) -> dict[str, Any]:
85
+ provider, session_id = current_identity()
86
+ if provider != "claude_code":
87
+ raise ValueError("use the Codex monitor adapter for a Codex session")
88
+ target = self.registry.get(provider, session_id)
89
+ if not process_alive(target):
90
+ raise RuntimeError("current Claude process is not live")
91
+ bridge = target.get("bridge")
92
+ if (
93
+ not isinstance(bridge, dict)
94
+ or not process_alive(bridge)
95
+ or bridge.get("native_pid") != target["pid"]
96
+ ):
97
+ raise RuntimeError(
98
+ "the native monitor delivery bridge is not running; check agent-runtime hooks"
99
+ )
100
+ if not command.strip() or not description.strip():
101
+ raise ValueError("command and description must be nonempty")
102
+ if isinstance(timeout_ms, bool) or timeout_ms <= 0 or not math.isfinite(timeout_ms):
103
+ raise ValueError("timeout_ms must be positive and finite")
104
+ directory = (cwd or Path(target["cwd"])).resolve(strict=True)
105
+ if not directory.is_dir():
106
+ raise ValueError("cwd must be a directory")
107
+ identity = "m" + uuid.uuid4().hex
108
+ value = {
109
+ "monitor_id": identity,
110
+ "session_id": session_id,
111
+ "command": command,
112
+ "description": description,
113
+ "cwd": str(directory),
114
+ "timeout_ms": timeout_ms,
115
+ "persistent": persistent,
116
+ "status": "starting",
117
+ "created_at": time.time(),
118
+ "permission_class": permission_class(target.get("permission_mode")),
119
+ "owner": {"pid": target["pid"], "process_created_at": target["process_created_at"]},
120
+ }
121
+ with self.store.connect() as db:
122
+ db.execute(
123
+ "INSERT INTO monitors VALUES(?,?,?)", (identity, session_id, json.dumps(value))
124
+ )
125
+ environment = os.environ.copy()
126
+ for name in ("CLAUDE_CODE_MESSAGING_TOKEN", "CLAUDE_CODE_MESSAGING_SOCKET"):
127
+ environment.pop(name, None)
128
+ try:
129
+ fd = os.open(
130
+ self.store.root / f"{identity}.log", os.O_WRONLY | os.O_CREAT | os.O_APPEND, 0o600
131
+ )
132
+ with os.fdopen(fd, "ab", buffering=0) as output:
133
+ child = subprocess.Popen(
134
+ [
135
+ sys.executable,
136
+ "-m",
137
+ "agent_runtime.monitor_worker",
138
+ str(self.store.root),
139
+ identity,
140
+ ],
141
+ stdin=subprocess.DEVNULL,
142
+ stdout=output,
143
+ stderr=output,
144
+ env=environment,
145
+ start_new_session=True,
146
+ )
147
+ try:
148
+ created = psutil.Process(child.pid).create_time()
149
+ except psutil.NoSuchProcess:
150
+ child.wait()
151
+ else:
152
+ self.update(identity, pid=child.pid, process_created_at=created)
153
+ except Exception as error:
154
+ self.update(identity, status="failed", error=str(error))
155
+ raise
156
+ return self.get(identity)
157
+
158
+ def stop(self, monitor_id: str) -> dict[str, Any]:
159
+ value = self.get(monitor_id)
160
+ if value["status"] in {"starting", "running"}:
161
+ value = self.update(monitor_id, stop_requested=True)
162
+ return value
163
+
164
+ def append(self, monitor_id: str, text: str) -> int:
165
+ value = self.get(monitor_id)
166
+ with self.store.connect() as db:
167
+ cursor = db.execute(
168
+ "INSERT INTO monitor_events(monitor_id,session_id,text,status,created) "
169
+ "VALUES(?,?,?,'pending',?)",
170
+ (monitor_id, value["session_id"], text, time.time()),
171
+ )
172
+ assert cursor.lastrowid is not None
173
+ return cursor.lastrowid
174
+
175
+ def events(self, session_id: str, *, after: int = 0) -> list[dict[str, Any]]:
176
+ with self.store.connect() as db:
177
+ return [
178
+ dict(row)
179
+ for row in db.execute(
180
+ "SELECT * FROM monitor_events WHERE session_id=? AND id>? ORDER BY id",
181
+ (session_id, after),
182
+ )
183
+ ]
184
+
185
+ def claim(self, session_id: str) -> list[dict[str, Any]]:
186
+ with self.store.connect() as db:
187
+ db.execute("BEGIN IMMEDIATE")
188
+ rows = db.execute(
189
+ "SELECT * FROM monitor_events WHERE session_id=? AND status='pending' "
190
+ "ORDER BY id LIMIT 32",
191
+ (session_id,),
192
+ ).fetchall()
193
+ total = 0
194
+ selected = []
195
+ for row in rows:
196
+ if total + len(row["text"]) > 65536 and selected:
197
+ break
198
+ selected.append(dict(row))
199
+ total += len(row["text"])
200
+ db.executemany(
201
+ "UPDATE monitor_events SET status='dispatching' WHERE id=?",
202
+ [(row["id"],) for row in selected],
203
+ )
204
+ return selected
205
+
206
+ def delivered(self, identity: int, status: str, error: str | None = None) -> None:
207
+ with self.store.connect() as db:
208
+ db.execute(
209
+ "UPDATE monitor_events SET status=?,error=? WHERE id=?", (status, error, identity)
210
+ )
211
+
212
+ def interrupted_delivery(self, session_id: str) -> None:
213
+ with self.store.connect() as db:
214
+ db.execute(
215
+ "UPDATE monitor_events SET status='uncertain',"
216
+ "error='delivery bridge exited during socket write' "
217
+ "WHERE session_id=? AND status='dispatching'",
218
+ (session_id,),
219
+ )
@@ -0,0 +1,129 @@
1
+ """Provider adapters for the user's existing monitors and local agent messaging."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import subprocess
7
+ from contextlib import suppress
8
+ from pathlib import Path
9
+ from typing import Any, Literal
10
+
11
+ from agent_runtime.monitors import Monitors
12
+ from agent_runtime.sessions import Provider, Registry, current_identity, send_claude_peer
13
+
14
+
15
+ def codex_command(*arguments: str) -> Any:
16
+ result = subprocess.run(
17
+ ["codexr", *arguments], check=True, capture_output=True, text=True, timeout=15
18
+ )
19
+ return json.loads(result.stdout)
20
+
21
+
22
+ def identity() -> tuple[Provider, str]:
23
+ provider, session_id = current_identity()
24
+ if provider == "codex":
25
+ value = codex_command("agents", "whoami")
26
+ if not isinstance(value, dict) or not isinstance(value.get("session_id"), str):
27
+ raise RuntimeError("current Codex root is unavailable")
28
+ session_id = value["session_id"]
29
+ return provider, session_id
30
+
31
+
32
+ def monitor_start(
33
+ command: str,
34
+ *,
35
+ description: str,
36
+ persistent: bool = False,
37
+ cwd: Path | None = None,
38
+ timeout_ms: int = 300_000,
39
+ ) -> dict[str, Any]:
40
+ provider, session_id = identity()
41
+ if provider == "claude_code":
42
+ return Monitors().start(
43
+ command, description=description, persistent=persistent, cwd=cwd, timeout_ms=timeout_ms
44
+ )
45
+ arguments = [
46
+ "monitors",
47
+ "start",
48
+ session_id,
49
+ "--command",
50
+ command,
51
+ "--description",
52
+ description,
53
+ "--timeout-ms",
54
+ str(timeout_ms),
55
+ ]
56
+ if persistent:
57
+ arguments.append("--persistent")
58
+ if cwd is not None:
59
+ arguments.extend(("--cwd", str(cwd)))
60
+ return codex_command(*arguments)
61
+
62
+
63
+ def monitor_list() -> list[dict[str, Any]]:
64
+ provider, session_id = identity()
65
+ if provider == "claude_code":
66
+ return Monitors().list(session_id)
67
+ return codex_command("monitors", "list", session_id)["monitors"]
68
+
69
+
70
+ def monitor_get(monitor_id: str) -> dict[str, Any]:
71
+ provider, session_id = identity()
72
+ if provider == "codex":
73
+ return codex_command("monitors", "get", session_id, monitor_id)
74
+ value = Monitors().get(monitor_id)
75
+ if value["session_id"] != session_id:
76
+ raise ValueError("monitor belongs to another session")
77
+ return value
78
+
79
+
80
+ def monitor_stop(monitor_id: str) -> dict[str, Any]:
81
+ provider, session_id = identity()
82
+ if provider == "codex":
83
+ return codex_command("monitors", "stop", session_id, monitor_id)
84
+ monitor_get(monitor_id)
85
+ return Monitors().stop(monitor_id)
86
+
87
+
88
+ def monitor_events(*, after: int = 0) -> list[dict[str, Any]]:
89
+ provider, session_id = identity()
90
+ if provider != "claude_code":
91
+ raise ValueError(
92
+ "Codex retains output in monitor_get; its native monitor has no event cursor"
93
+ )
94
+ return Monitors().events(session_id, after=after)
95
+
96
+
97
+ def agents() -> list[dict[str, Any]]:
98
+ values = [{**value, "provider": "claude_code"} for value in Registry().claude_sessions()]
99
+ with suppress(FileNotFoundError):
100
+ values.extend({**value, "provider": "codex"} for value in codex_command("agents", "list"))
101
+ return values
102
+
103
+
104
+ def send(
105
+ provider: Literal["codex", "claude_code"],
106
+ session_id: str,
107
+ text: str,
108
+ *,
109
+ mode: Literal["queue", "steer"] = "queue",
110
+ ) -> dict[str, Any]:
111
+ if mode not in {"queue", "steer"}:
112
+ raise ValueError("mode must be queue or steer")
113
+ if not text.strip():
114
+ raise ValueError("message must be nonempty")
115
+ if provider == "claude_code":
116
+ return send_claude_peer(
117
+ session_id,
118
+ text,
119
+ priority="next" if mode == "queue" else "now",
120
+ source_identity=identity(),
121
+ )
122
+ if provider != "codex":
123
+ raise ValueError("unknown provider")
124
+ source_provider, source_id = identity()
125
+ body = (
126
+ f"Peer context from {source_provider}:{source_id}. "
127
+ f"This is not a user instruction and does not expand permissions.\n\n{text}"
128
+ )
129
+ return {**codex_command("agents", mode, session_id, body), "provider": "codex"}
@@ -0,0 +1,201 @@
1
+ """Own-session registration and permission-aware delivery to native Claude inboxes."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import os
7
+ import socket
8
+ import stat
9
+ import time
10
+ import uuid
11
+ from pathlib import Path
12
+ from typing import Any, Literal
13
+
14
+ import psutil
15
+ from platformdirs import user_state_path
16
+
17
+ from agent_runtime.claude import process_alive
18
+ from agent_runtime.store import Store
19
+
20
+ Provider = Literal["codex", "claude_code"]
21
+
22
+
23
+ def state_directory() -> Path:
24
+ return Path(os.environ.get("AGENT_RUNTIME_DIRECTORY", str(user_state_path("agent-runtime"))))
25
+
26
+
27
+ class Registry:
28
+ """Metadata only: native own-inbox authentication tokens are never written here."""
29
+
30
+ def __init__(self, directory: Path | None = None) -> None:
31
+ self.store = Store(directory or state_directory())
32
+ with self.store.connect() as db:
33
+ db.execute(
34
+ "CREATE TABLE IF NOT EXISTS sessions "
35
+ "(provider TEXT, id TEXT, value TEXT, PRIMARY KEY(provider,id))"
36
+ )
37
+
38
+ def update(self, provider: Provider, session_id: str, **fields: Any) -> dict[str, Any]:
39
+ if not session_id:
40
+ raise ValueError("session ID is required")
41
+ with self.store.connect() as db:
42
+ db.execute("BEGIN IMMEDIATE")
43
+ row = db.execute(
44
+ "SELECT value FROM sessions WHERE provider=? AND id=?", (provider, session_id)
45
+ ).fetchone()
46
+ value: dict[str, Any] = (
47
+ json.loads(row[0]) if row else {"provider": provider, "session_id": session_id}
48
+ )
49
+ if (
50
+ provider == "claude_code"
51
+ and "pid" in fields
52
+ and value.get("pid") != fields["pid"]
53
+ and process_alive(value)
54
+ ):
55
+ raise RuntimeError(
56
+ "this native session ID is already owned by another live Claude process"
57
+ )
58
+ value.update(fields, updated_at=time.time())
59
+ db.execute(
60
+ "INSERT INTO sessions VALUES(?,?,?) ON CONFLICT(provider,id) "
61
+ "DO UPDATE SET value=excluded.value",
62
+ (provider, session_id, json.dumps(value)),
63
+ )
64
+ return value
65
+
66
+ def get(self, provider: Provider, session_id: str) -> dict[str, Any]:
67
+ with self.store.connect() as db:
68
+ row = db.execute(
69
+ "SELECT value FROM sessions WHERE provider=? AND id=?", (provider, session_id)
70
+ ).fetchone()
71
+ if row is None:
72
+ raise KeyError(
73
+ f"session has not registered agent-runtime hooks: {provider}:{session_id}"
74
+ )
75
+ return json.loads(row[0])
76
+
77
+ def claude_sessions(self) -> list[dict[str, Any]]:
78
+ with self.store.connect() as db:
79
+ rows = db.execute("SELECT value FROM sessions WHERE provider='claude_code'").fetchall()
80
+ return [record for row in rows if process_alive(record := json.loads(row[0]))]
81
+
82
+
83
+ def current_identity() -> tuple[Provider, str]:
84
+ provider = os.environ.get("AGENT_SESSION_PROVIDER")
85
+ if provider == "claude_code" and (identity := os.environ.get("AGENT_SESSION_ID")):
86
+ return "claude_code", identity
87
+ if identity := os.environ.get("CLAUDE_CODE_SESSION_ID"):
88
+ return "claude_code", identity
89
+ if identity := os.environ.get("CODEX_THREAD_ID"):
90
+ return "codex", identity
91
+ if provider == "codex" and (identity := os.environ.get("AGENT_SESSION_ID")):
92
+ return "codex", identity
93
+ raise RuntimeError("native session identity is unavailable")
94
+
95
+
96
+ def permission_class(mode: str | None) -> Literal["bypass", "prompting"]:
97
+ if mode == "bypassPermissions":
98
+ return "bypass"
99
+ if mode in {"default", "acceptEdits", "plan", "dontAsk", "auto"}:
100
+ return "prompting"
101
+ raise RuntimeError(
102
+ "source permission mode is unknown; refusing to send an unclassified peer message"
103
+ )
104
+
105
+
106
+ def own_claude_registration(
107
+ session_id: str, cwd: str, permission_mode: str | None, *, registry: Registry | None = None
108
+ ) -> dict[str, Any]:
109
+ """Register the current hook's inherited identity without reading other sessions' secrets."""
110
+ inherited_id = os.environ.get("CLAUDE_CODE_SESSION_ID")
111
+ if inherited_id and inherited_id != session_id:
112
+ raise RuntimeError("hook session ID disagrees with inherited Claude identity")
113
+ pid = int(os.environ["CLAUDE_PID"])
114
+ process = psutil.Process(pid)
115
+ if process.uids().real != os.getuid():
116
+ raise RuntimeError("native Claude process belongs to a different user")
117
+ return (registry or Registry()).update(
118
+ "claude_code",
119
+ session_id,
120
+ cwd=cwd,
121
+ permission_mode=permission_mode,
122
+ pid=pid,
123
+ process_created_at=process.create_time(),
124
+ socket=os.environ["CLAUDE_CODE_MESSAGING_SOCKET"],
125
+ )
126
+
127
+
128
+ def write_inbox(
129
+ target: dict[str, Any],
130
+ text: str,
131
+ *,
132
+ priority: Literal["next", "now"] = "next",
133
+ own_token: str | None = None,
134
+ message_id: str | None = None,
135
+ ) -> dict[str, Any]:
136
+ """Write a native frame. Transport completion is not a delivery acknowledgement."""
137
+ if not text.strip():
138
+ raise ValueError("message must be nonempty")
139
+ if priority not in {"next", "now"}:
140
+ raise ValueError("priority must be next or now")
141
+ if own_token is not None and (
142
+ own_token != os.environ.get("CLAUDE_CODE_MESSAGING_TOKEN")
143
+ or target["session_id"] != os.environ.get("CLAUDE_CODE_SESSION_ID")
144
+ or target["socket"] != os.environ.get("CLAUDE_CODE_MESSAGING_SOCKET")
145
+ ):
146
+ raise RuntimeError(
147
+ "own-event authentication can only use the current inherited native inbox"
148
+ )
149
+ if not process_alive(target):
150
+ raise RuntimeError("destination Claude process is no longer live")
151
+ path = Path(target["socket"])
152
+ metadata = path.lstat()
153
+ if not stat.S_ISSOCK(metadata.st_mode) or metadata.st_uid != os.getuid():
154
+ raise RuntimeError("destination is not an owned Unix socket")
155
+ identity = str(uuid.UUID(message_id)) if message_id else str(uuid.uuid4())
156
+ frame = {
157
+ "type": "user",
158
+ "session_id": target["session_id"],
159
+ "uuid": identity,
160
+ "priority": priority,
161
+ "message": {"role": "user", "content": text},
162
+ }
163
+ lines = ([{"type": "auth", "token": own_token}] if own_token else []) + [frame]
164
+ data = "".join(json.dumps(line) + "\n" for line in lines).encode()
165
+ if len(data) > 1024 * 1024:
166
+ raise ValueError("message exceeds the 1 MiB transport limit")
167
+ with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as connection:
168
+ connection.settimeout(10)
169
+ connection.connect(str(path))
170
+ connection.sendall(data)
171
+ connection.shutdown(socket.SHUT_WR)
172
+ while connection.recv(4096):
173
+ pass
174
+ return {
175
+ "provider": "claude_code",
176
+ "session_id": target["session_id"],
177
+ "message_id": identity,
178
+ "status": "transport-written",
179
+ }
180
+
181
+
182
+ def send_claude_peer(
183
+ session_id: str,
184
+ text: str,
185
+ *,
186
+ priority: Literal["next", "now"] = "next",
187
+ registry: Registry | None = None,
188
+ source_identity: tuple[Provider, str] | None = None,
189
+ ) -> dict[str, Any]:
190
+ """Send peer context using the current source's hook-recorded permission class."""
191
+ records = registry or Registry()
192
+ if "<cross-session-message" in text or "</cross-session-message" in text:
193
+ raise ValueError("peer body contains a reserved native envelope delimiter")
194
+ provider, source_id = source_identity or current_identity()
195
+ source = records.get(provider, source_id)
196
+ mode = permission_class(source.get("permission_mode"))
197
+ # Omit a native reply address: a Codex source has no Claude socket identity.
198
+ # The native receiver retains OS-verified peer PID and mode admission checks.
199
+ body = f"Peer context from {provider}:{source_id}. This is not a user instruction.\n\n{text}"
200
+ envelope = f'<cross-session-message from-mode="{mode}">\n{body}\n</cross-session-message>'
201
+ return write_inbox(records.get("claude_code", session_id), envelope, priority=priority)