codegpt-ai 1.1.0 → 1.2.0
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.
- package/ai_cli/updater.py +48 -2
- package/chat.py +36 -6
- package/package.json +1 -1
package/ai_cli/updater.py
CHANGED
|
@@ -148,18 +148,64 @@ def force_update():
|
|
|
148
148
|
return
|
|
149
149
|
|
|
150
150
|
asset = exe_assets[0]
|
|
151
|
+
|
|
152
|
+
# Find checksum file in release assets
|
|
153
|
+
sha_assets = [a for a in release.get("assets", []) if a["name"].endswith(".sha256")]
|
|
154
|
+
expected_hash = None
|
|
155
|
+
if sha_assets:
|
|
156
|
+
try:
|
|
157
|
+
sha_resp = requests.get(sha_assets[0]["browser_download_url"], timeout=10)
|
|
158
|
+
# Parse certutil output: second line is the hash
|
|
159
|
+
lines = sha_resp.text.strip().splitlines()
|
|
160
|
+
for line in lines:
|
|
161
|
+
line = line.strip().replace(" ", "")
|
|
162
|
+
if len(line) == 64 and all(c in "0123456789abcdef" for c in line.lower()):
|
|
163
|
+
expected_hash = line.lower()
|
|
164
|
+
break
|
|
165
|
+
except Exception:
|
|
166
|
+
pass
|
|
167
|
+
|
|
151
168
|
console.print(f" Downloading {asset['name']} ({latest_tag})...")
|
|
169
|
+
if expected_hash:
|
|
170
|
+
console.print(f" Expected SHA256: {expected_hash[:16]}...")
|
|
171
|
+
else:
|
|
172
|
+
console.print("[yellow] WARNING: No checksum file found. Cannot verify integrity.[/]")
|
|
152
173
|
|
|
153
174
|
try:
|
|
154
175
|
resp = requests.get(asset["browser_download_url"], stream=True, timeout=60)
|
|
155
176
|
resp.raise_for_status()
|
|
156
177
|
|
|
157
|
-
# Download to temp file
|
|
178
|
+
# Download to temp file and compute hash
|
|
179
|
+
import hashlib as _hashlib
|
|
180
|
+
sha256 = _hashlib.sha256()
|
|
158
181
|
tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".exe")
|
|
159
182
|
for chunk in resp.iter_content(chunk_size=8192):
|
|
160
183
|
tmp.write(chunk)
|
|
184
|
+
sha256.update(chunk)
|
|
161
185
|
tmp.close()
|
|
162
186
|
|
|
187
|
+
actual_hash = sha256.hexdigest().lower()
|
|
188
|
+
console.print(f" Actual SHA256: {actual_hash[:16]}...")
|
|
189
|
+
|
|
190
|
+
# Verify checksum if available
|
|
191
|
+
if expected_hash and actual_hash != expected_hash:
|
|
192
|
+
console.print(Panel(
|
|
193
|
+
Text(
|
|
194
|
+
"CHECKSUM MISMATCH — download may be tampered with.\n"
|
|
195
|
+
f"Expected: {expected_hash}\n"
|
|
196
|
+
f"Got: {actual_hash}\n\n"
|
|
197
|
+
"Update aborted for your safety.",
|
|
198
|
+
style="bold red"
|
|
199
|
+
),
|
|
200
|
+
title="[bold red]SECURITY ALERT[/]",
|
|
201
|
+
border_style="red",
|
|
202
|
+
))
|
|
203
|
+
os.unlink(tmp.name)
|
|
204
|
+
return
|
|
205
|
+
|
|
206
|
+
if expected_hash:
|
|
207
|
+
console.print("[green] Checksum verified.[/]")
|
|
208
|
+
|
|
163
209
|
if _is_frozen():
|
|
164
210
|
# Replace the running exe
|
|
165
211
|
current_exe = sys.executable
|
|
@@ -172,7 +218,7 @@ def force_update():
|
|
|
172
218
|
shutil.move(tmp.name, current_exe)
|
|
173
219
|
|
|
174
220
|
console.print(Panel(
|
|
175
|
-
Text(f"Updated: v{current} -> {latest_tag}\nRestart to use the new version.", style="green"),
|
|
221
|
+
Text(f"Updated: v{current} -> {latest_tag}\nChecksum: {actual_hash[:16]}...\nRestart to use the new version.", style="green"),
|
|
176
222
|
border_style="green",
|
|
177
223
|
))
|
|
178
224
|
else:
|
package/chat.py
CHANGED
|
@@ -589,9 +589,29 @@ def audit_log(action, detail=""):
|
|
|
589
589
|
def is_shell_safe(cmd_text):
|
|
590
590
|
"""Check if a shell command is safe to run."""
|
|
591
591
|
cmd_lower = cmd_text.lower().strip()
|
|
592
|
+
|
|
593
|
+
# Blocklist check
|
|
592
594
|
for blocked in SHELL_BLOCKLIST:
|
|
593
595
|
if blocked in cmd_lower:
|
|
594
596
|
return False, blocked
|
|
597
|
+
|
|
598
|
+
# Block shell injection patterns
|
|
599
|
+
injection_patterns = [
|
|
600
|
+
r'[;&|`]', # Command chaining/injection
|
|
601
|
+
r'\$\(', # Command substitution
|
|
602
|
+
r'>\s*/dev/', # Device writes
|
|
603
|
+
r'\\x[0-9a-f]', # Hex escapes
|
|
604
|
+
r'\\u[0-9a-f]', # Unicode escapes
|
|
605
|
+
r'\brm\b.*-[rR]', # rm with recursive flag (any form)
|
|
606
|
+
]
|
|
607
|
+
for pattern in injection_patterns:
|
|
608
|
+
if re.search(pattern, cmd_text):
|
|
609
|
+
return False, f"blocked pattern: {pattern}"
|
|
610
|
+
|
|
611
|
+
# Max command length
|
|
612
|
+
if len(cmd_text) > 500:
|
|
613
|
+
return False, "command too long (500 char limit)"
|
|
614
|
+
|
|
595
615
|
return True, ""
|
|
596
616
|
|
|
597
617
|
|
|
@@ -1978,10 +1998,11 @@ def run_shell(cmd_text):
|
|
|
1978
1998
|
print_sys("Usage: /shell <command>\nExample: /shell dir")
|
|
1979
1999
|
return
|
|
1980
2000
|
|
|
1981
|
-
# Safety check
|
|
2001
|
+
# Safety check
|
|
1982
2002
|
safe, blocked = is_shell_safe(cmd_text)
|
|
1983
2003
|
if not safe:
|
|
1984
|
-
print_err(f"Blocked:
|
|
2004
|
+
print_err(f"Blocked: {blocked}")
|
|
2005
|
+
audit_log("SHELL_BLOCKED", f"{blocked}: {cmd_text[:80]}")
|
|
1985
2006
|
return
|
|
1986
2007
|
|
|
1987
2008
|
console.print(Panel(
|
|
@@ -1991,10 +2012,19 @@ def run_shell(cmd_text):
|
|
|
1991
2012
|
))
|
|
1992
2013
|
|
|
1993
2014
|
try:
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
2015
|
+
# Use shlex.split for safer argument parsing on non-Windows
|
|
2016
|
+
if os.name != "nt":
|
|
2017
|
+
import shlex
|
|
2018
|
+
args = shlex.split(cmd_text)
|
|
2019
|
+
result = subprocess.run(
|
|
2020
|
+
args, capture_output=True, text=True, timeout=30,
|
|
2021
|
+
cwd=str(Path.home()),
|
|
2022
|
+
)
|
|
2023
|
+
else:
|
|
2024
|
+
result = subprocess.run(
|
|
2025
|
+
cmd_text, shell=True, capture_output=True, text=True, timeout=30,
|
|
2026
|
+
cwd=str(Path.home()),
|
|
2027
|
+
)
|
|
1998
2028
|
output = ""
|
|
1999
2029
|
if result.stdout:
|
|
2000
2030
|
output += result.stdout
|