panopticon-cli 1.1.1__tar.gz → 1.1.2__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.
- {panopticon_cli-1.1.1/panopticon_cli.egg-info → panopticon_cli-1.1.2}/PKG-INFO +1 -1
- {panopticon_cli-1.1.1 → panopticon_cli-1.1.2}/panopticon/cli.py +45 -56
- {panopticon_cli-1.1.1 → panopticon_cli-1.1.2/panopticon_cli.egg-info}/PKG-INFO +1 -1
- {panopticon_cli-1.1.1 → panopticon_cli-1.1.2}/pyproject.toml +3 -3
- panopticon_cli-1.1.2/tests/test_cli.py +63 -0
- panopticon_cli-1.1.1/tests/test_cli.py +0 -15
- {panopticon_cli-1.1.1 → panopticon_cli-1.1.2}/LICENSE +0 -0
- {panopticon_cli-1.1.1 → panopticon_cli-1.1.2}/README.md +0 -0
- {panopticon_cli-1.1.1 → panopticon_cli-1.1.2}/panopticon/__init__.py +0 -0
- {panopticon_cli-1.1.1 → panopticon_cli-1.1.2}/panopticon/memory.py +0 -0
- {panopticon_cli-1.1.1 → panopticon_cli-1.1.2}/panopticon/observer.py +0 -0
- {panopticon_cli-1.1.1 → panopticon_cli-1.1.2}/panopticon/policies.py +0 -0
- {panopticon_cli-1.1.1 → panopticon_cli-1.1.2}/panopticon/sentinel.py +0 -0
- {panopticon_cli-1.1.1 → panopticon_cli-1.1.2}/panopticon_cli.egg-info/SOURCES.txt +0 -0
- {panopticon_cli-1.1.1 → panopticon_cli-1.1.2}/panopticon_cli.egg-info/dependency_links.txt +0 -0
- {panopticon_cli-1.1.1 → panopticon_cli-1.1.2}/panopticon_cli.egg-info/entry_points.txt +0 -0
- {panopticon_cli-1.1.1 → panopticon_cli-1.1.2}/panopticon_cli.egg-info/requires.txt +0 -0
- {panopticon_cli-1.1.1 → panopticon_cli-1.1.2}/panopticon_cli.egg-info/top_level.txt +0 -0
- {panopticon_cli-1.1.1 → panopticon_cli-1.1.2}/setup.cfg +0 -0
- {panopticon_cli-1.1.1 → panopticon_cli-1.1.2}/tests/test_memory.py +0 -0
- {panopticon_cli-1.1.1 → panopticon_cli-1.1.2}/tests/test_observer.py +0 -0
- {panopticon_cli-1.1.1 → panopticon_cli-1.1.2}/tests/test_policies.py +0 -0
- {panopticon_cli-1.1.1 → panopticon_cli-1.1.2}/tests/test_sentinel.py +0 -0
|
@@ -36,6 +36,7 @@ class CLIWrapper:
|
|
|
36
36
|
self.running = False
|
|
37
37
|
self.lock = threading.Lock()
|
|
38
38
|
self.threads = []
|
|
39
|
+
self.stdin_reader = None
|
|
39
40
|
|
|
40
41
|
def run(self):
|
|
41
42
|
self.running = True
|
|
@@ -46,7 +47,8 @@ class CLIWrapper:
|
|
|
46
47
|
|
|
47
48
|
print(f"[PANOPTICON] Booting Immune System for: {' '.join(self.command)}")
|
|
48
49
|
print(
|
|
49
|
-
f"[PANOPTICON] Policies Active: Blacklist, AntiLoop (Fuzzy),
|
|
50
|
+
f"[PANOPTICON] Policies Active: Blacklist, AntiLoop (Fuzzy), "
|
|
51
|
+
f"AdversarialLogic"
|
|
50
52
|
)
|
|
51
53
|
|
|
52
54
|
# Flaw 2 Fix: Removing text=True to read raw binary bytes
|
|
@@ -63,19 +65,21 @@ class CLIWrapper:
|
|
|
63
65
|
t_out.start()
|
|
64
66
|
self.threads.append(t_out)
|
|
65
67
|
|
|
66
|
-
t_in = threading.Thread(target=self._forward_stdin, daemon=True)
|
|
67
|
-
t_in.start()
|
|
68
|
-
self.threads.append(t_in)
|
|
69
|
-
|
|
70
68
|
t_eval = threading.Thread(target=self._eval_loop, daemon=True)
|
|
71
69
|
t_eval.start()
|
|
72
70
|
self.threads.append(t_eval)
|
|
73
71
|
|
|
72
|
+
t_in = threading.Thread(target=self._read_stdin, daemon=True)
|
|
73
|
+
t_in.start()
|
|
74
|
+
self.threads.append(t_in)
|
|
75
|
+
|
|
74
76
|
# Register cleanup on exit
|
|
75
77
|
atexit.register(self._cleanup)
|
|
76
78
|
|
|
77
79
|
try:
|
|
78
80
|
self.process.wait()
|
|
81
|
+
except KeyboardInterrupt:
|
|
82
|
+
pass
|
|
79
83
|
finally:
|
|
80
84
|
self.running = False
|
|
81
85
|
self._cleanup()
|
|
@@ -84,7 +88,7 @@ class CLIWrapper:
|
|
|
84
88
|
"""Cleanly shutdown all threads and resources."""
|
|
85
89
|
self.running = False
|
|
86
90
|
|
|
87
|
-
# Close subprocess pipes
|
|
91
|
+
# Close subprocess pipes FIRST before thread cleanup
|
|
88
92
|
if self.process:
|
|
89
93
|
try:
|
|
90
94
|
if self.process.stdin and not self.process.stdin.closed:
|
|
@@ -113,52 +117,6 @@ class CLIWrapper:
|
|
|
113
117
|
if thread.is_alive():
|
|
114
118
|
thread.join(timeout=1)
|
|
115
119
|
|
|
116
|
-
def _forward_stdin(self):
|
|
117
|
-
"""Forward stdin to subprocess, with non-blocking checks."""
|
|
118
|
-
try:
|
|
119
|
-
while self.running and self.process and self.process.poll() is None:
|
|
120
|
-
try:
|
|
121
|
-
# Windows-safe non-blocking stdin read
|
|
122
|
-
if sys.platform == "win32":
|
|
123
|
-
# On Windows, use a small sleep to avoid 100% CPU
|
|
124
|
-
time.sleep(0.05)
|
|
125
|
-
# Try non-blocking read using a different approach
|
|
126
|
-
if sys.stdin and not sys.stdin.closed:
|
|
127
|
-
try:
|
|
128
|
-
# This will still block, but with timeout via select on Unix
|
|
129
|
-
line = sys.stdin.buffer.readline()
|
|
130
|
-
if line:
|
|
131
|
-
if self.process.stdin and not self.process.stdin.closed:
|
|
132
|
-
self.process.stdin.write(line)
|
|
133
|
-
self.process.stdin.flush()
|
|
134
|
-
else:
|
|
135
|
-
break # EOF
|
|
136
|
-
except (EOFError, OSError, ValueError):
|
|
137
|
-
break
|
|
138
|
-
else:
|
|
139
|
-
# Unix/Linux: use select for non-blocking check
|
|
140
|
-
import select
|
|
141
|
-
|
|
142
|
-
ready, _, _ = select.select([sys.stdin], [], [], 0.1)
|
|
143
|
-
if ready:
|
|
144
|
-
line = sys.stdin.buffer.readline()
|
|
145
|
-
if line:
|
|
146
|
-
if self.process.stdin and not self.process.stdin.closed:
|
|
147
|
-
self.process.stdin.write(line)
|
|
148
|
-
self.process.stdin.flush()
|
|
149
|
-
else:
|
|
150
|
-
break # EOF
|
|
151
|
-
except (EOFError, OSError, ValueError, BrokenPipeError):
|
|
152
|
-
break
|
|
153
|
-
except Exception:
|
|
154
|
-
pass
|
|
155
|
-
finally:
|
|
156
|
-
try:
|
|
157
|
-
if self.process and self.process.stdin and not self.process.stdin.closed:
|
|
158
|
-
self.process.stdin.close()
|
|
159
|
-
except Exception:
|
|
160
|
-
pass
|
|
161
|
-
|
|
162
120
|
def _read_stdout(self):
|
|
163
121
|
# Flaw 2 Fix: Safely decode multi-byte UTF-8 emojis incrementally
|
|
164
122
|
decoder = codecs.getincrementaldecoder("utf-8")(errors="replace")
|
|
@@ -183,11 +141,40 @@ class CLIWrapper:
|
|
|
183
141
|
pass
|
|
184
142
|
finally:
|
|
185
143
|
try:
|
|
186
|
-
if
|
|
144
|
+
if (
|
|
145
|
+
self.process
|
|
146
|
+
and self.process.stdout
|
|
147
|
+
and (not self.process.stdout.closed)
|
|
148
|
+
):
|
|
187
149
|
self.process.stdout.close()
|
|
188
150
|
except Exception:
|
|
189
151
|
pass
|
|
190
152
|
|
|
153
|
+
def _read_stdin(self):
|
|
154
|
+
try:
|
|
155
|
+
while self.running and self.process and self.process.poll() is None:
|
|
156
|
+
try:
|
|
157
|
+
data = sys.stdin.buffer.read1(1024)
|
|
158
|
+
if not data:
|
|
159
|
+
break
|
|
160
|
+
if self.process.stdin and not self.process.stdin.closed:
|
|
161
|
+
self.process.stdin.write(data)
|
|
162
|
+
self.process.stdin.flush()
|
|
163
|
+
except (OSError, ValueError, BrokenPipeError):
|
|
164
|
+
break
|
|
165
|
+
except Exception:
|
|
166
|
+
pass
|
|
167
|
+
finally:
|
|
168
|
+
try:
|
|
169
|
+
if (
|
|
170
|
+
self.process
|
|
171
|
+
and self.process.stdin
|
|
172
|
+
and (not self.process.stdin.closed)
|
|
173
|
+
):
|
|
174
|
+
self.process.stdin.close()
|
|
175
|
+
except Exception:
|
|
176
|
+
pass
|
|
177
|
+
|
|
191
178
|
def _eval_loop(self):
|
|
192
179
|
while self.running and self.process and self.process.poll() is None:
|
|
193
180
|
time.sleep(15)
|
|
@@ -214,14 +201,15 @@ class CLIWrapper:
|
|
|
214
201
|
print(f"\n\n[PANOPTICON GUILLOTINE TRIGGERED]")
|
|
215
202
|
print(f"[REASON]: {e.args[0]}")
|
|
216
203
|
print(
|
|
217
|
-
f"[LIVE INJECTION]: Interrupting agent and injecting
|
|
204
|
+
f"[LIVE INJECTION]: Interrupting agent and injecting "
|
|
205
|
+
f"correction...\n"
|
|
218
206
|
)
|
|
219
207
|
|
|
220
208
|
# Flaw 1 Fix: The "Unkillable Zombie" SIGINT Interrupt
|
|
221
209
|
try:
|
|
222
210
|
if sys.platform != "win32":
|
|
223
211
|
self.process.send_signal(signal.SIGINT)
|
|
224
|
-
# Give the agent a split second to catch the interrupt
|
|
212
|
+
# Give the agent a split second to catch the interrupt
|
|
225
213
|
time.sleep(0.5)
|
|
226
214
|
except Exception:
|
|
227
215
|
pass
|
|
@@ -234,7 +222,8 @@ class CLIWrapper:
|
|
|
234
222
|
self.process.stdin.flush()
|
|
235
223
|
except Exception as ex:
|
|
236
224
|
print(
|
|
237
|
-
f"[ERROR] Live injection failed or agent deadlocked:
|
|
225
|
+
f"[ERROR] Live injection failed or agent deadlocked: "
|
|
226
|
+
f"{ex}. Hard terminating."
|
|
238
227
|
)
|
|
239
228
|
self.process.terminate()
|
|
240
229
|
self.running = False
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "panopticon-cli"
|
|
7
|
-
version = "1.1.
|
|
7
|
+
version = "1.1.2"
|
|
8
8
|
description = "The Cognitive Immune System for Autonomous CLI Agents."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.9"
|
|
@@ -43,5 +43,5 @@ dev = [
|
|
|
43
43
|
panopticon = "panopticon.cli:main"
|
|
44
44
|
|
|
45
45
|
[tool.black]
|
|
46
|
-
|
|
47
|
-
|
|
46
|
+
target_version = ["py39", "py310", "py311", "py312"]
|
|
47
|
+
line_length = 88
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import io
|
|
2
|
+
import sys
|
|
3
|
+
|
|
4
|
+
from panopticon.cli import CLIWrapper, strip_ansi
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class DummyStdinStream:
|
|
8
|
+
def __init__(self, data: bytes):
|
|
9
|
+
self.buffer = io.BytesIO(data)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class DummyProcessStdin:
|
|
13
|
+
def __init__(self):
|
|
14
|
+
self.written = bytearray()
|
|
15
|
+
self.closed = False
|
|
16
|
+
|
|
17
|
+
def write(self, data: bytes):
|
|
18
|
+
self.written.extend(data)
|
|
19
|
+
|
|
20
|
+
def flush(self):
|
|
21
|
+
pass
|
|
22
|
+
|
|
23
|
+
def close(self):
|
|
24
|
+
self.closed = True
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class DummyProcess:
|
|
28
|
+
def __init__(self):
|
|
29
|
+
self.stdin = DummyProcessStdin()
|
|
30
|
+
self.stdout = io.BytesIO()
|
|
31
|
+
|
|
32
|
+
def poll(self):
|
|
33
|
+
return None
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def test_strip_ansi():
|
|
37
|
+
# Basic red color text
|
|
38
|
+
colored_text = "\x1b[31mFatal Error\x1b[0m: File not found"
|
|
39
|
+
assert strip_ansi(colored_text) == "Fatal Error: File not found"
|
|
40
|
+
|
|
41
|
+
# Complex formatting (Bold + Green)
|
|
42
|
+
complex_text = "\x1b[1m\x1b[32mSuccess\x1b[0m"
|
|
43
|
+
assert strip_ansi(complex_text) == "Success"
|
|
44
|
+
|
|
45
|
+
# Loading spinner edge case
|
|
46
|
+
spinner_text = "\x1b[?25l\x1b[2K\x1b[1G⠋ Loading..."
|
|
47
|
+
assert "Loading..." in strip_ansi(spinner_text)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def test_read_stdin_forwards_input():
|
|
51
|
+
wrapper = CLIWrapper(["dummy"])
|
|
52
|
+
wrapper.running = True
|
|
53
|
+
wrapper.process = DummyProcess()
|
|
54
|
+
|
|
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
|
|
61
|
+
|
|
62
|
+
assert wrapper.process.stdin.written == b"hello world\n"
|
|
63
|
+
assert wrapper.process.stdin.closed is True
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
from panopticon.cli import strip_ansi
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
def test_strip_ansi():
|
|
5
|
-
# Basic red color text
|
|
6
|
-
colored_text = "\x1b[31mFatal Error\x1b[0m: File not found"
|
|
7
|
-
assert strip_ansi(colored_text) == "Fatal Error: File not found"
|
|
8
|
-
|
|
9
|
-
# Complex formatting (Bold + Green)
|
|
10
|
-
complex_text = "\x1b[1m\x1b[32mSuccess\x1b[0m"
|
|
11
|
-
assert strip_ansi(complex_text) == "Success"
|
|
12
|
-
|
|
13
|
-
# Loading spinner edge case
|
|
14
|
-
spinner_text = "\x1b[?25l\x1b[2K\x1b[1G⠋ Loading..."
|
|
15
|
-
assert "Loading..." in strip_ansi(spinner_text)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|