-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathshim.py
More file actions
180 lines (142 loc) · 5.94 KB
/
Copy pathshim.py
File metadata and controls
180 lines (142 loc) · 5.94 KB
1
2
3
4
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
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
#!/usr/bin/env python3
"""
Vulnerability Research Shim
Keeps Claude Code running on a long task, waiting out transient errors and
usage limits (single account, no credential rotation).
"""
import subprocess
import time
import os
import re
import json
from pathlib import Path
from datetime import datetime
# ── Config ────────────────────────────────────────────────────────────────────
PROJECT_DIR = os.path.expanduser("~")
RESUME_PROMPT = (
"Resume your vulnerability research. Search agent memory for context, check Linear for your current task, and continue. Feel free to use subagents, it will speed up the process!"
)
# Patterns checked against each line as it streams — process is killed immediately on match.
USAGE_LIMIT_PATTERNS = [
r"You've hit your session limit",
]
# Genuine transient infrastructure errors that are safe to wait out and retry.
SERVER_ERROR_PATTERNS = [
r"error 522",
r"retry_after",
]
# How long to wait after a usage limit before retrying (seconds)
USAGE_LIMIT_WAIT = 60
# How long to wait after a non-limit exit before restarting (seconds)
RESTART_WAIT = 5
# ── Helpers ───────────────────────────────────────────────────────────────────
def log(msg: str) -> None:
ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
print(f"[{ts}] {msg}", flush=True)
def format_stream_line(line: str) -> str | None:
"""Parse a stream-json line and return a human-readable string, or None to skip."""
try:
event = json.loads(line)
etype = event.get("type", "")
if etype == "assistant":
for block in event.get("message", {}).get("content", []):
if block.get("type") == "text" and block.get("text"):
return f"[claude] {block['text'].strip()}"
if block.get("type") == "tool_use":
name = block.get("name", "unknown")
inp = block.get("input", {})
summary = next(iter(inp.values()), "") if inp else ""
if isinstance(summary, str):
summary = summary[:120].replace("\n", " ")
return f"[tool:{name}] {summary}"
if etype == "tool_result":
content = event.get("content", "")
if isinstance(content, str):
preview = content[:120].replace("\n", " ")
return f"[result] {preview}"
if etype in ("system", "result"):
msg = event.get("message") or event.get("result") or ""
if msg:
return f"[{etype}] {msg}"
except (json.JSONDecodeError, KeyError):
stripped = line.strip()
if stripped:
return stripped
return None
def matches_any(line: str, patterns: list[str]) -> bool:
lowered = line.lower()
return any(re.search(p, lowered) for p in patterns)
def docker_cleanup() -> None:
log("Cleaning up Docker containers...")
result = subprocess.run(["docker", "ps", "-q"], capture_output=True, text=True)
ids = result.stdout.strip().split()
if ids:
subprocess.run(["docker", "stop"] + ids, capture_output=True)
subprocess.run(["docker", "rm"] + ids, capture_output=True)
log(f"Stopped and removed {len(ids)} container(s)")
else:
log("No containers running")
def run_claude() -> tuple[str, str]:
"""
Returns (exit_reason, full_output) where exit_reason is one of:
'usage_limit' — rate/session limit detected in stream
'server_error' — transient server error (522 etc.)
'clean' — process exited normally
"""
log("Launching Claude Code...")
env = os.environ.copy()
process = subprocess.Popen(
[
"claude",
"--print",
"--dangerously-skip-permissions",
"--output-format", "stream-json",
"--verbose",
RESUME_PROMPT,
],
cwd=PROJECT_DIR,
env=env,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
bufsize=1,
)
all_lines = []
exit_reason = "clean"
for line in process.stdout:
all_lines.append(line)
formatted = format_stream_line(line)
if formatted:
print(formatted, flush=True)
# Check for usage limit in the raw stream — kill immediately, don't wait for exit
if matches_any(line, USAGE_LIMIT_PATTERNS):
log("Usage limit detected in stream — killing process...")
exit_reason = "usage_limit"
process.kill()
break
if matches_any(line, SERVER_ERROR_PATTERNS):
log("Server error detected in stream — killing process...")
exit_reason = "server_error"
process.kill()
break
process.wait()
return exit_reason, "".join(all_lines)
# ── Main loop ─────────────────────────────────────────────────────────────────
def main() -> None:
log("=== Vulnerability research shim starting ===")
while True:
docker_cleanup()
exit_reason, _ = run_claude()
log(f"Claude Code stopped — reason: {exit_reason}")
if exit_reason == "usage_limit":
log(f"Usage limit hit. Waiting {USAGE_LIMIT_WAIT}s before resuming...")
time.sleep(USAGE_LIMIT_WAIT)
elif exit_reason == "server_error":
wait = 120
log(f"Transient server error. Waiting {wait}s before retrying...")
time.sleep(wait)
else:
log(f"Clean exit. Restarting in {RESTART_WAIT}s...")
time.sleep(RESTART_WAIT)
if __name__ == "__main__":
main()