panopticon-cli 1.1.2__tar.gz → 1.1.5__tar.gz

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.
Files changed (22) hide show
  1. {panopticon_cli-1.1.2/panopticon_cli.egg-info → panopticon_cli-1.1.5}/PKG-INFO +1 -1
  2. {panopticon_cli-1.1.2 → panopticon_cli-1.1.5}/panopticon/cli.py +161 -18
  3. {panopticon_cli-1.1.2 → panopticon_cli-1.1.5/panopticon_cli.egg-info}/PKG-INFO +1 -1
  4. {panopticon_cli-1.1.2 → panopticon_cli-1.1.5}/pyproject.toml +1 -1
  5. {panopticon_cli-1.1.2 → panopticon_cli-1.1.5}/tests/test_cli.py +7 -6
  6. {panopticon_cli-1.1.2 → panopticon_cli-1.1.5}/LICENSE +0 -0
  7. {panopticon_cli-1.1.2 → panopticon_cli-1.1.5}/README.md +0 -0
  8. {panopticon_cli-1.1.2 → panopticon_cli-1.1.5}/panopticon/__init__.py +0 -0
  9. {panopticon_cli-1.1.2 → panopticon_cli-1.1.5}/panopticon/memory.py +0 -0
  10. {panopticon_cli-1.1.2 → panopticon_cli-1.1.5}/panopticon/observer.py +0 -0
  11. {panopticon_cli-1.1.2 → panopticon_cli-1.1.5}/panopticon/policies.py +0 -0
  12. {panopticon_cli-1.1.2 → panopticon_cli-1.1.5}/panopticon/sentinel.py +0 -0
  13. {panopticon_cli-1.1.2 → panopticon_cli-1.1.5}/panopticon_cli.egg-info/SOURCES.txt +0 -0
  14. {panopticon_cli-1.1.2 → panopticon_cli-1.1.5}/panopticon_cli.egg-info/dependency_links.txt +0 -0
  15. {panopticon_cli-1.1.2 → panopticon_cli-1.1.5}/panopticon_cli.egg-info/entry_points.txt +0 -0
  16. {panopticon_cli-1.1.2 → panopticon_cli-1.1.5}/panopticon_cli.egg-info/requires.txt +0 -0
  17. {panopticon_cli-1.1.2 → panopticon_cli-1.1.5}/panopticon_cli.egg-info/top_level.txt +0 -0
  18. {panopticon_cli-1.1.2 → panopticon_cli-1.1.5}/setup.cfg +0 -0
  19. {panopticon_cli-1.1.2 → panopticon_cli-1.1.5}/tests/test_memory.py +0 -0
  20. {panopticon_cli-1.1.2 → panopticon_cli-1.1.5}/tests/test_observer.py +0 -0
  21. {panopticon_cli-1.1.2 → panopticon_cli-1.1.5}/tests/test_policies.py +0 -0
  22. {panopticon_cli-1.1.2 → panopticon_cli-1.1.5}/tests/test_sentinel.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: panopticon-cli
3
- Version: 1.1.2
3
+ Version: 1.1.5
4
4
  Summary: The Cognitive Immune System for Autonomous CLI Agents.
5
5
  Author: Ak
6
6
  License: MIT
@@ -32,11 +32,12 @@ class CLIWrapper:
32
32
  )
33
33
 
34
34
  self.process = None
35
+ self.master_fd = None
35
36
  self.buffer = []
36
37
  self.running = False
37
38
  self.lock = threading.Lock()
38
39
  self.threads = []
39
- self.stdin_reader = None
40
+ self._stop_event = threading.Event()
40
41
 
41
42
  def run(self):
42
43
  self.running = True
@@ -51,15 +52,46 @@ class CLIWrapper:
51
52
  f"AdversarialLogic"
52
53
  )
53
54
 
54
- # Flaw 2 Fix: Removing text=True to read raw binary bytes
55
- self.process = subprocess.Popen(
56
- self.command,
57
- stdout=subprocess.PIPE,
58
- stderr=subprocess.STDOUT,
59
- stdin=subprocess.PIPE,
60
- bufsize=0,
61
- env=env,
55
+ use_pty = (
56
+ sys.platform != "win32"
57
+ and sys.stdin is not None
58
+ and sys.stdin.isatty()
59
+ and sys.stdout.isatty()
62
60
  )
61
+ self.master_fd = None
62
+
63
+ if use_pty:
64
+ import pty
65
+
66
+ master_fd, slave_fd = pty.openpty()
67
+ self.master_fd = master_fd
68
+ self.process = subprocess.Popen(
69
+ self.command,
70
+ stdout=slave_fd,
71
+ stderr=slave_fd,
72
+ stdin=slave_fd,
73
+ bufsize=0,
74
+ env=env,
75
+ close_fds=True,
76
+ )
77
+ os.close(slave_fd)
78
+ use_stdin_thread = True
79
+ else:
80
+ stdin_target = subprocess.PIPE
81
+ use_stdin_thread = True
82
+
83
+ if sys.platform != "win32" and sys.stdin is not None and sys.stdin.isatty():
84
+ stdin_target = sys.stdin
85
+ use_stdin_thread = False
86
+
87
+ self.process = subprocess.Popen(
88
+ self.command,
89
+ stdout=subprocess.PIPE,
90
+ stderr=subprocess.STDOUT,
91
+ stdin=stdin_target,
92
+ bufsize=0,
93
+ env=env,
94
+ )
63
95
 
64
96
  t_out = threading.Thread(target=self._read_stdout, daemon=True)
65
97
  t_out.start()
@@ -69,9 +101,10 @@ class CLIWrapper:
69
101
  t_eval.start()
70
102
  self.threads.append(t_eval)
71
103
 
72
- t_in = threading.Thread(target=self._read_stdin, daemon=True)
73
- t_in.start()
74
- self.threads.append(t_in)
104
+ if use_stdin_thread:
105
+ t_in = threading.Thread(target=self._read_stdin, daemon=True)
106
+ t_in.start()
107
+ self.threads.append(t_in)
75
108
 
76
109
  # Register cleanup on exit
77
110
  atexit.register(self._cleanup)
@@ -87,6 +120,14 @@ class CLIWrapper:
87
120
  def _cleanup(self):
88
121
  """Cleanly shutdown all threads and resources."""
89
122
  self.running = False
123
+ self._stop_event.set()
124
+
125
+ if getattr(self, "master_fd", None) is not None:
126
+ try:
127
+ os.close(self.master_fd)
128
+ except Exception:
129
+ pass
130
+ self.master_fd = None
90
131
 
91
132
  # Close subprocess pipes FIRST before thread cleanup
92
133
  if self.process:
@@ -123,7 +164,10 @@ class CLIWrapper:
123
164
  try:
124
165
  while self.running and self.process and self.process.poll() is None:
125
166
  try:
126
- byte_chunk = self.process.stdout.read(1)
167
+ if getattr(self, "master_fd", None) is not None:
168
+ byte_chunk = os.read(self.master_fd, 1)
169
+ else:
170
+ byte_chunk = self.process.stdout.read(1)
127
171
  if not byte_chunk:
128
172
  break
129
173
 
@@ -142,7 +186,8 @@ class CLIWrapper:
142
186
  finally:
143
187
  try:
144
188
  if (
145
- self.process
189
+ getattr(self, "master_fd", None) is None
190
+ and self.process
146
191
  and self.process.stdout
147
192
  and (not self.process.stdout.closed)
148
193
  ):
@@ -150,13 +195,109 @@ class CLIWrapper:
150
195
  except Exception:
151
196
  pass
152
197
 
198
+ def _stdin_ready(self) -> bool:
199
+ if sys.platform == "win32":
200
+ if (
201
+ sys.stdin is not None
202
+ and getattr(sys.stdin, "isatty", lambda: False)()
203
+ and hasattr(sys.stdin, "fileno")
204
+ ):
205
+ try:
206
+ import msvcrt
207
+
208
+ return msvcrt.kbhit()
209
+ except ImportError:
210
+ return False
211
+ return True
212
+ else:
213
+ import select
214
+
215
+ try:
216
+ rlist, _, _ = select.select([sys.stdin], [], [], 0)
217
+ return bool(rlist)
218
+ except Exception:
219
+ return False
220
+
153
221
  def _read_stdin(self):
222
+ if sys.platform == "win32":
223
+ if (
224
+ sys.stdin is not None
225
+ and getattr(sys.stdin, "isatty", lambda: False)()
226
+ and hasattr(sys.stdin, "fileno")
227
+ and sys.stdin.fileno() == 0
228
+ ):
229
+ self._read_stdin_windows()
230
+ return
231
+
232
+ stdin_buffer = getattr(sys.stdin, "buffer", None)
233
+ if stdin_buffer is None:
234
+ return
235
+
154
236
  try:
155
- while self.running and self.process and self.process.poll() is None:
237
+ while (
238
+ self.running
239
+ and self.process
240
+ and self.process.poll() is None
241
+ and not self._stop_event.is_set()
242
+ ):
243
+ if not self._stdin_ready():
244
+ time.sleep(0.1)
245
+ continue
246
+
156
247
  try:
157
- data = sys.stdin.buffer.read1(1024)
248
+ read_func = getattr(stdin_buffer, "read1", stdin_buffer.read)
249
+ data = read_func(1024)
158
250
  if not data:
159
251
  break
252
+ if getattr(self, "master_fd", None) is not None:
253
+ os.write(self.master_fd, data)
254
+ elif self.process.stdin and not self.process.stdin.closed:
255
+ self.process.stdin.write(data)
256
+ self.process.stdin.flush()
257
+ except (OSError, ValueError, BrokenPipeError):
258
+ break
259
+ except Exception:
260
+ pass
261
+ finally:
262
+ try:
263
+ if (
264
+ getattr(self, "master_fd", None) is None
265
+ and self.process
266
+ and self.process.stdin
267
+ and (not self.process.stdin.closed)
268
+ ):
269
+ self.process.stdin.close()
270
+ except Exception:
271
+ pass
272
+
273
+ def _read_stdin_windows(self):
274
+ try:
275
+ import msvcrt
276
+ except ImportError:
277
+ return
278
+
279
+ try:
280
+ while (
281
+ self.running
282
+ and self.process
283
+ and self.process.poll() is None
284
+ and not self._stop_event.is_set()
285
+ ):
286
+ if not msvcrt.kbhit():
287
+ time.sleep(0.1)
288
+ continue
289
+
290
+ try:
291
+ char = msvcrt.getwch()
292
+ if char == "\x03":
293
+ if self.process.poll() is None:
294
+ try:
295
+ self.process.send_signal(signal.CTRL_C_EVENT)
296
+ except Exception:
297
+ pass
298
+ continue
299
+
300
+ data = char.encode("utf-8")
160
301
  if self.process.stdin and not self.process.stdin.closed:
161
302
  self.process.stdin.write(data)
162
303
  self.process.stdin.flush()
@@ -216,8 +357,10 @@ class CLIWrapper:
216
357
 
217
358
  try:
218
359
  # Write the injection payload as binary
219
- if self.process.stdin and not self.process.stdin.closed:
220
- payload = (e.course_correction + "\n").encode("utf-8")
360
+ payload = (e.course_correction + "\n").encode("utf-8")
361
+ if getattr(self, "master_fd", None) is not None:
362
+ os.write(self.master_fd, payload)
363
+ elif self.process.stdin and not self.process.stdin.closed:
221
364
  self.process.stdin.write(payload)
222
365
  self.process.stdin.flush()
223
366
  except Exception as ex:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: panopticon-cli
3
- Version: 1.1.2
3
+ Version: 1.1.5
4
4
  Summary: The Cognitive Immune System for Autonomous CLI Agents.
5
5
  Author: Ak
6
6
  License: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "panopticon-cli"
7
- version = "1.1.2"
7
+ version = "1.1.5"
8
8
  description = "The Cognitive Immune System for Autonomous CLI Agents."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -1,5 +1,6 @@
1
1
  import io
2
2
  import sys
3
+ from unittest.mock import patch
3
4
 
4
5
  from panopticon.cli import CLIWrapper, strip_ansi
5
6
 
@@ -52,12 +53,12 @@ def test_read_stdin_forwards_input():
52
53
  wrapper.running = True
53
54
  wrapper.process = DummyProcess()
54
55
 
55
- original_stdin = sys.stdin
56
- try:
57
- sys.stdin = DummyStdinStream(b"hello world\n")
58
- wrapper._read_stdin()
59
- finally:
60
- sys.stdin = original_stdin
56
+ dummy_stdin = DummyStdinStream(b"hello world\n")
57
+ dummy_stdin.isatty = lambda: True
58
+
59
+ with patch("panopticon.cli.sys.stdin", dummy_stdin):
60
+ with patch.object(CLIWrapper, "_stdin_ready", return_value=True):
61
+ wrapper._read_stdin()
61
62
 
62
63
  assert wrapper.process.stdin.written == b"hello world\n"
63
64
  assert wrapper.process.stdin.closed is True
File without changes
File without changes
File without changes