panopticon-cli 1.0.0__tar.gz → 1.1.1__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.0.0 → panopticon_cli-1.1.1}/LICENSE +21 -21
- {panopticon_cli-1.0.0 → panopticon_cli-1.1.1}/PKG-INFO +2 -3
- {panopticon_cli-1.0.0 → panopticon_cli-1.1.1}/README.md +43 -44
- {panopticon_cli-1.0.0 → panopticon_cli-1.1.1}/panopticon/__init__.py +11 -11
- panopticon_cli-1.1.1/panopticon/cli.py +278 -0
- {panopticon_cli-1.0.0 → panopticon_cli-1.1.1}/panopticon/memory.py +60 -56
- {panopticon_cli-1.0.0 → panopticon_cli-1.1.1}/panopticon/observer.py +74 -64
- {panopticon_cli-1.0.0 → panopticon_cli-1.1.1}/panopticon/policies.py +68 -53
- {panopticon_cli-1.0.0 → panopticon_cli-1.1.1}/panopticon/sentinel.py +143 -122
- {panopticon_cli-1.0.0 → panopticon_cli-1.1.1}/panopticon_cli.egg-info/PKG-INFO +2 -3
- {panopticon_cli-1.0.0 → panopticon_cli-1.1.1}/pyproject.toml +47 -43
- {panopticon_cli-1.0.0 → panopticon_cli-1.1.1}/tests/test_cli.py +15 -14
- {panopticon_cli-1.0.0 → panopticon_cli-1.1.1}/tests/test_memory.py +38 -29
- {panopticon_cli-1.0.0 → panopticon_cli-1.1.1}/tests/test_observer.py +37 -34
- {panopticon_cli-1.0.0 → panopticon_cli-1.1.1}/tests/test_policies.py +47 -35
- {panopticon_cli-1.0.0 → panopticon_cli-1.1.1}/tests/test_sentinel.py +33 -32
- panopticon_cli-1.0.0/panopticon/cli.py +0 -164
- {panopticon_cli-1.0.0 → panopticon_cli-1.1.1}/panopticon_cli.egg-info/SOURCES.txt +0 -0
- {panopticon_cli-1.0.0 → panopticon_cli-1.1.1}/panopticon_cli.egg-info/dependency_links.txt +0 -0
- {panopticon_cli-1.0.0 → panopticon_cli-1.1.1}/panopticon_cli.egg-info/entry_points.txt +0 -0
- {panopticon_cli-1.0.0 → panopticon_cli-1.1.1}/panopticon_cli.egg-info/requires.txt +0 -0
- {panopticon_cli-1.0.0 → panopticon_cli-1.1.1}/panopticon_cli.egg-info/top_level.txt +0 -0
- {panopticon_cli-1.0.0 → panopticon_cli-1.1.1}/setup.cfg +0 -0
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2026 Ak
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ak
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: panopticon-cli
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.1.1
|
|
4
4
|
Summary: The Cognitive Immune System for Autonomous CLI Agents.
|
|
5
5
|
Author: Ak
|
|
6
6
|
License: MIT
|
|
@@ -33,10 +33,9 @@ Dynamic: license-file
|
|
|
33
33
|
> **The Cognitive Immune System (and glorified babysitter) for Autonomous CLI Agents.**
|
|
34
34
|
|
|
35
35
|
[](https://pypi.org/project/panopticon-cli/)
|
|
36
|
-
[](https://pypi.org/project/panopticon-cli/)
|
|
37
36
|
[](https://opensource.org/licenses/MIT)
|
|
37
|
+
[](https://github.com/ak495867/Panopticon/actions/workflows/ci.yml)
|
|
38
38
|
[](https://www.python.org/downloads/)
|
|
39
|
-
[](https://github.com/ak/panopticon/actions)
|
|
40
39
|
|
|
41
40
|
Autonomous CLI agents (like Claude Code, AutoGPT, and Antigravity) are amazing... right until they hallucinate, burn through $50 of your API credits, or confidently try to `rm -rf /` your hard drive.
|
|
42
41
|
|
|
@@ -1,44 +1,43 @@
|
|
|
1
|
-
# PANOPTICON 👁️
|
|
2
|
-
|
|
3
|
-
> **The Cognitive Immune System (and glorified babysitter) for Autonomous CLI Agents.**
|
|
4
|
-
|
|
5
|
-
[](https://pypi.org/project/panopticon-cli/)
|
|
6
|
-
[](https://www.python.org/downloads/)
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
- **
|
|
20
|
-
- **
|
|
21
|
-
- **Level
|
|
22
|
-
- **Level
|
|
23
|
-
|
|
24
|
-
- **
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
export
|
|
35
|
-
# OR export
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
See [USAGE.md](USAGE.md) for deeper configuration so you can start dropping the Guillotine.
|
|
1
|
+
# PANOPTICON 👁️
|
|
2
|
+
|
|
3
|
+
> **The Cognitive Immune System (and glorified babysitter) for Autonomous CLI Agents.**
|
|
4
|
+
|
|
5
|
+
[](https://pypi.org/project/panopticon-cli/)
|
|
6
|
+
[](https://opensource.org/licenses/MIT)
|
|
7
|
+
[](https://github.com/ak495867/Panopticon/actions/workflows/ci.yml)
|
|
8
|
+
[](https://www.python.org/downloads/)
|
|
9
|
+
|
|
10
|
+
Autonomous CLI agents (like Claude Code, AutoGPT, and Antigravity) are amazing... right until they hallucinate, burn through $50 of your API credits, or confidently try to `rm -rf /` your hard drive.
|
|
11
|
+
|
|
12
|
+
**Panopticon** is a zero-latency, non-blocking wrapper that silently watches your agent's terminal output. When the AI inevitably tries to do something incredibly stupid, Panopticon's **State Guillotine** drops.
|
|
13
|
+
|
|
14
|
+
It intercepts the rogue process, uses a Meta-Agent to figure out why your AI is crying, and **forcefully injects the correction directly into the agent's live `stdin` stream** like a disappointed senior developer taking over the keyboard. Oh, and it saves that failure to a SQLite database so the agent never makes the exact same mistake twice.
|
|
15
|
+
|
|
16
|
+
## Why you need this
|
|
17
|
+
|
|
18
|
+
- **Native TTY Wrapping:** Slap it in front of any CLI. Loading spinners, ANSI colors, and interactive prompts still work perfectly. We just spy on them natively.
|
|
19
|
+
- **The Policy Cascade (Iron Dome):**
|
|
20
|
+
- **Level 1 (Blacklist):** Instant, zero-cost kills for destructive actions. Because your AI *will* try to drop your production database eventually.
|
|
21
|
+
- **Level 2 (Fuzzy Heuristics):** Zero-cost math thresholds that catch repetitive loops. Stops the agent from running `cat missing_file.py` 300 times in a row.
|
|
22
|
+
- **Level 3 (Universal Semantic Logic):** Streams a sliding-window of the terminal to a Meta-Agent (Claude, GPT, or Gemini) to judge your sub-agent's poor life choices.
|
|
23
|
+
- **True Live Injection:** We don't just kill the agent and leave you hanging. Panopticon literally types the course-correction prompt into the interactive terminal for you, saving the session.
|
|
24
|
+
- **Persistent Semantic Memory:** AI agents have goldfish memory. We use SQLite and Jaccard keyword routing to permanently scar them with their past failures so they actually learn.
|
|
25
|
+
|
|
26
|
+
## Quick Start
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
# Install globally from PyPI
|
|
30
|
+
pip install panopticon-cli
|
|
31
|
+
|
|
32
|
+
# Export your preferred API Key (Panopticon dynamically routes to whatever you actually pay for)
|
|
33
|
+
export OPENAI_API_KEY="sk-..."
|
|
34
|
+
# OR export ANTHROPIC_API_KEY="..."
|
|
35
|
+
# OR export GEMINI_API_KEY="..."
|
|
36
|
+
|
|
37
|
+
# Slap it in front of your agent!
|
|
38
|
+
panopticon claude
|
|
39
|
+
# OR
|
|
40
|
+
panopticon agy
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
See [USAGE.md](USAGE.md) for deeper configuration so you can start dropping the Guillotine.
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
from .observer import PanopticonObserver
|
|
2
|
-
from .policies import AdversarialLogicCheck, AntiLoopPolicy, BlacklistPolicy
|
|
3
|
-
from .memory import PersistentMemory
|
|
4
|
-
|
|
5
|
-
__all__ = [
|
|
6
|
-
"PanopticonObserver",
|
|
7
|
-
"AdversarialLogicCheck",
|
|
8
|
-
"AntiLoopPolicy",
|
|
9
|
-
"BlacklistPolicy",
|
|
10
|
-
"PersistentMemory"
|
|
11
|
-
]
|
|
1
|
+
from .observer import PanopticonObserver
|
|
2
|
+
from .policies import AdversarialLogicCheck, AntiLoopPolicy, BlacklistPolicy
|
|
3
|
+
from .memory import PersistentMemory
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
"PanopticonObserver",
|
|
7
|
+
"AdversarialLogicCheck",
|
|
8
|
+
"AntiLoopPolicy",
|
|
9
|
+
"BlacklistPolicy",
|
|
10
|
+
"PersistentMemory",
|
|
11
|
+
]
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
import subprocess
|
|
2
|
+
import threading
|
|
3
|
+
import sys
|
|
4
|
+
import time
|
|
5
|
+
import argparse
|
|
6
|
+
import re
|
|
7
|
+
import os
|
|
8
|
+
import signal
|
|
9
|
+
import codecs
|
|
10
|
+
import atexit
|
|
11
|
+
from typing import List
|
|
12
|
+
from .observer import PanopticonObserver, InterventionException
|
|
13
|
+
from .policies import AdversarialLogicCheck, AntiLoopPolicy, BlacklistPolicy
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def strip_ansi(text: str) -> str:
|
|
17
|
+
ansi_escape = re.compile(r"\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])")
|
|
18
|
+
return ansi_escape.sub("", text)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class CLIWrapper:
|
|
22
|
+
def __init__(self, command: List[str]):
|
|
23
|
+
self.command = command
|
|
24
|
+
target_agent = self.command[0] if self.command else "unknown"
|
|
25
|
+
|
|
26
|
+
self.observer = PanopticonObserver(
|
|
27
|
+
policies=[
|
|
28
|
+
BlacklistPolicy(forbidden_patterns=["rm -rf /", "DROP TABLE"]),
|
|
29
|
+
AntiLoopPolicy(window=3, threshold=0.85),
|
|
30
|
+
AdversarialLogicCheck(target_agent=target_agent),
|
|
31
|
+
]
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
self.process = None
|
|
35
|
+
self.buffer = []
|
|
36
|
+
self.running = False
|
|
37
|
+
self.lock = threading.Lock()
|
|
38
|
+
self.threads = []
|
|
39
|
+
|
|
40
|
+
def run(self):
|
|
41
|
+
self.running = True
|
|
42
|
+
env = os.environ.copy()
|
|
43
|
+
env["PYTHONUNBUFFERED"] = "1"
|
|
44
|
+
env["FORCE_COLOR"] = "1"
|
|
45
|
+
env["CLICOLOR_FORCE"] = "1"
|
|
46
|
+
|
|
47
|
+
print(f"[PANOPTICON] Booting Immune System for: {' '.join(self.command)}")
|
|
48
|
+
print(
|
|
49
|
+
f"[PANOPTICON] Policies Active: Blacklist, AntiLoop (Fuzzy), AdversarialLogic"
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
# Flaw 2 Fix: Removing text=True to read raw binary bytes
|
|
53
|
+
self.process = subprocess.Popen(
|
|
54
|
+
self.command,
|
|
55
|
+
stdout=subprocess.PIPE,
|
|
56
|
+
stderr=subprocess.STDOUT,
|
|
57
|
+
stdin=subprocess.PIPE,
|
|
58
|
+
bufsize=0,
|
|
59
|
+
env=env,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
t_out = threading.Thread(target=self._read_stdout, daemon=True)
|
|
63
|
+
t_out.start()
|
|
64
|
+
self.threads.append(t_out)
|
|
65
|
+
|
|
66
|
+
t_in = threading.Thread(target=self._forward_stdin, daemon=True)
|
|
67
|
+
t_in.start()
|
|
68
|
+
self.threads.append(t_in)
|
|
69
|
+
|
|
70
|
+
t_eval = threading.Thread(target=self._eval_loop, daemon=True)
|
|
71
|
+
t_eval.start()
|
|
72
|
+
self.threads.append(t_eval)
|
|
73
|
+
|
|
74
|
+
# Register cleanup on exit
|
|
75
|
+
atexit.register(self._cleanup)
|
|
76
|
+
|
|
77
|
+
try:
|
|
78
|
+
self.process.wait()
|
|
79
|
+
finally:
|
|
80
|
+
self.running = False
|
|
81
|
+
self._cleanup()
|
|
82
|
+
|
|
83
|
+
def _cleanup(self):
|
|
84
|
+
"""Cleanly shutdown all threads and resources."""
|
|
85
|
+
self.running = False
|
|
86
|
+
|
|
87
|
+
# Close subprocess pipes
|
|
88
|
+
if self.process:
|
|
89
|
+
try:
|
|
90
|
+
if self.process.stdin and not self.process.stdin.closed:
|
|
91
|
+
self.process.stdin.close()
|
|
92
|
+
except Exception:
|
|
93
|
+
pass
|
|
94
|
+
|
|
95
|
+
try:
|
|
96
|
+
if self.process.stdout and not self.process.stdout.closed:
|
|
97
|
+
self.process.stdout.close()
|
|
98
|
+
except Exception:
|
|
99
|
+
pass
|
|
100
|
+
|
|
101
|
+
try:
|
|
102
|
+
if self.process.poll() is None:
|
|
103
|
+
self.process.terminate()
|
|
104
|
+
try:
|
|
105
|
+
self.process.wait(timeout=2)
|
|
106
|
+
except subprocess.TimeoutExpired:
|
|
107
|
+
self.process.kill()
|
|
108
|
+
except Exception:
|
|
109
|
+
pass
|
|
110
|
+
|
|
111
|
+
# Wait for threads with timeout
|
|
112
|
+
for thread in self.threads:
|
|
113
|
+
if thread.is_alive():
|
|
114
|
+
thread.join(timeout=1)
|
|
115
|
+
|
|
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
|
+
def _read_stdout(self):
|
|
163
|
+
# Flaw 2 Fix: Safely decode multi-byte UTF-8 emojis incrementally
|
|
164
|
+
decoder = codecs.getincrementaldecoder("utf-8")(errors="replace")
|
|
165
|
+
try:
|
|
166
|
+
while self.running and self.process and self.process.poll() is None:
|
|
167
|
+
try:
|
|
168
|
+
byte_chunk = self.process.stdout.read(1)
|
|
169
|
+
if not byte_chunk:
|
|
170
|
+
break
|
|
171
|
+
|
|
172
|
+
# Mirror raw bytes to actual terminal seamlessly
|
|
173
|
+
sys.stdout.buffer.write(byte_chunk)
|
|
174
|
+
sys.stdout.buffer.flush()
|
|
175
|
+
|
|
176
|
+
char_str = decoder.decode(byte_chunk)
|
|
177
|
+
if char_str:
|
|
178
|
+
with self.lock:
|
|
179
|
+
self.buffer.append(char_str)
|
|
180
|
+
except (OSError, ValueError, BrokenPipeError):
|
|
181
|
+
break
|
|
182
|
+
except Exception:
|
|
183
|
+
pass
|
|
184
|
+
finally:
|
|
185
|
+
try:
|
|
186
|
+
if self.process and self.process.stdout and not self.process.stdout.closed:
|
|
187
|
+
self.process.stdout.close()
|
|
188
|
+
except Exception:
|
|
189
|
+
pass
|
|
190
|
+
|
|
191
|
+
def _eval_loop(self):
|
|
192
|
+
while self.running and self.process and self.process.poll() is None:
|
|
193
|
+
time.sleep(15)
|
|
194
|
+
|
|
195
|
+
with self.lock:
|
|
196
|
+
if not self.buffer:
|
|
197
|
+
continue
|
|
198
|
+
full_text = "".join(self.buffer)
|
|
199
|
+
|
|
200
|
+
# Flaw 1 Fix: Sliding window overlap to prevent bridging blindness
|
|
201
|
+
overlap = full_text[-100:] if len(full_text) > 100 else ""
|
|
202
|
+
self.buffer.clear()
|
|
203
|
+
if overlap:
|
|
204
|
+
self.buffer.append(overlap)
|
|
205
|
+
|
|
206
|
+
clean_text = strip_ansi(full_text)
|
|
207
|
+
self.observer.log_action(
|
|
208
|
+
"CLI_Agent", clean_text, "cli_execution", len(clean_text) // 4
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
try:
|
|
212
|
+
self.observer._evaluate_state("CLI_Agent")
|
|
213
|
+
except InterventionException as e:
|
|
214
|
+
print(f"\n\n[PANOPTICON GUILLOTINE TRIGGERED]")
|
|
215
|
+
print(f"[REASON]: {e.args[0]}")
|
|
216
|
+
print(
|
|
217
|
+
f"[LIVE INJECTION]: Interrupting agent and injecting correction...\n"
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
# Flaw 1 Fix: The "Unkillable Zombie" SIGINT Interrupt
|
|
221
|
+
try:
|
|
222
|
+
if sys.platform != "win32":
|
|
223
|
+
self.process.send_signal(signal.SIGINT)
|
|
224
|
+
# Give the agent a split second to catch the interrupt before writing to stdin
|
|
225
|
+
time.sleep(0.5)
|
|
226
|
+
except Exception:
|
|
227
|
+
pass
|
|
228
|
+
|
|
229
|
+
try:
|
|
230
|
+
# Write the injection payload as binary
|
|
231
|
+
if self.process.stdin and not self.process.stdin.closed:
|
|
232
|
+
payload = (e.course_correction + "\n").encode("utf-8")
|
|
233
|
+
self.process.stdin.write(payload)
|
|
234
|
+
self.process.stdin.flush()
|
|
235
|
+
except Exception as ex:
|
|
236
|
+
print(
|
|
237
|
+
f"[ERROR] Live injection failed or agent deadlocked: {ex}. Hard terminating."
|
|
238
|
+
)
|
|
239
|
+
self.process.terminate()
|
|
240
|
+
self.running = False
|
|
241
|
+
break
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def main():
|
|
245
|
+
try:
|
|
246
|
+
from dotenv import load_dotenv
|
|
247
|
+
|
|
248
|
+
load_dotenv()
|
|
249
|
+
except ImportError:
|
|
250
|
+
pass
|
|
251
|
+
|
|
252
|
+
parser = argparse.ArgumentParser(
|
|
253
|
+
description="Panopticon: Production-Grade Immune System for AI CLIs"
|
|
254
|
+
)
|
|
255
|
+
parser.add_argument(
|
|
256
|
+
"command", nargs=argparse.REMAINDER, help="The command to run, e.g., 'claude'"
|
|
257
|
+
)
|
|
258
|
+
args = parser.parse_args()
|
|
259
|
+
|
|
260
|
+
if not args.command:
|
|
261
|
+
print("Usage: panopticon [command]")
|
|
262
|
+
sys.exit(1)
|
|
263
|
+
|
|
264
|
+
wrapper = CLIWrapper(args.command)
|
|
265
|
+
try:
|
|
266
|
+
wrapper.run()
|
|
267
|
+
except KeyboardInterrupt:
|
|
268
|
+
print("\n[PANOPTICON] Interrupted by user. Cleaning up...")
|
|
269
|
+
wrapper._cleanup()
|
|
270
|
+
sys.exit(0)
|
|
271
|
+
except Exception as e:
|
|
272
|
+
print(f"[PANOPTICON ERROR] {e}")
|
|
273
|
+
wrapper._cleanup()
|
|
274
|
+
sys.exit(1)
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
if __name__ == "__main__":
|
|
278
|
+
main()
|
|
@@ -1,56 +1,60 @@
|
|
|
1
|
-
import sqlite3
|
|
2
|
-
import os
|
|
3
|
-
import re
|
|
4
|
-
from typing import List, Dict
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
1
|
+
import sqlite3
|
|
2
|
+
import os
|
|
3
|
+
import re
|
|
4
|
+
from typing import List, Dict
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class PersistentMemory:
|
|
8
|
+
"""Production-grade procedural memory using keyword similarity routing."""
|
|
9
|
+
|
|
10
|
+
def __init__(self, db_path="panopticon_memory.db"):
|
|
11
|
+
self.db_path = db_path
|
|
12
|
+
self._init_db()
|
|
13
|
+
|
|
14
|
+
def _init_db(self):
|
|
15
|
+
# Flaw 3 Fix: Enable WAL and connection timeout for multi-process concurrency
|
|
16
|
+
with sqlite3.connect(self.db_path, timeout=10.0) as conn:
|
|
17
|
+
conn.execute("PRAGMA journal_mode=WAL;")
|
|
18
|
+
conn.execute("""
|
|
19
|
+
CREATE TABLE IF NOT EXISTS failures (
|
|
20
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
21
|
+
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
22
|
+
reason TEXT,
|
|
23
|
+
correction TEXT
|
|
24
|
+
)
|
|
25
|
+
""")
|
|
26
|
+
|
|
27
|
+
def record_failure(self, reason: str, correction: str):
|
|
28
|
+
with sqlite3.connect(self.db_path, timeout=10.0) as conn:
|
|
29
|
+
conn.execute(
|
|
30
|
+
"INSERT INTO failures (reason, correction) VALUES (?, ?)",
|
|
31
|
+
(reason, correction),
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
def _extract_keywords(self, text: str) -> set:
|
|
35
|
+
words = re.findall(r"\b[a-zA-Z]{4,}\b", text.lower())
|
|
36
|
+
return set(words)
|
|
37
|
+
|
|
38
|
+
def get_relevant_failures(self, current_context: str, limit=3) -> List[Dict]:
|
|
39
|
+
"""Retrieves past failures that have the highest word overlap with the current context."""
|
|
40
|
+
current_keywords = self._extract_keywords(current_context)
|
|
41
|
+
if not current_keywords:
|
|
42
|
+
return []
|
|
43
|
+
|
|
44
|
+
with sqlite3.connect(self.db_path, timeout=10.0) as conn:
|
|
45
|
+
cursor = conn.execute("SELECT reason, correction FROM failures")
|
|
46
|
+
all_failures = cursor.fetchall()
|
|
47
|
+
|
|
48
|
+
scored_failures = []
|
|
49
|
+
for reason, correction in all_failures:
|
|
50
|
+
reason_keywords = self._extract_keywords(reason)
|
|
51
|
+
# Calculate Jaccard-like overlap score
|
|
52
|
+
overlap = len(current_keywords.intersection(reason_keywords))
|
|
53
|
+
if overlap > 0:
|
|
54
|
+
scored_failures.append(
|
|
55
|
+
(overlap, {"reason": reason, "correction": correction})
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
# Sort by highest overlap, then take top N
|
|
59
|
+
scored_failures.sort(key=lambda x: x[0], reverse=True)
|
|
60
|
+
return [f[1] for f in scored_failures[:limit]]
|