fix-cli 0.6.1__tar.gz → 0.6.4__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.
- {fix_cli-0.6.1/fix_cli.egg-info → fix_cli-0.6.4}/PKG-INFO +3 -2
- {fix_cli-0.6.1 → fix_cli-0.6.4}/agent.py +6 -10
- {fix_cli-0.6.1 → fix_cli-0.6.4}/contract.py +1 -3
- {fix_cli-0.6.1 → fix_cli-0.6.4}/crypto.py +70 -21
- {fix_cli-0.6.1 → fix_cli-0.6.4}/fix.py +21 -30
- {fix_cli-0.6.1 → fix_cli-0.6.4/fix_cli.egg-info}/PKG-INFO +3 -2
- {fix_cli-0.6.1 → fix_cli-0.6.4}/fix_cli.egg-info/SOURCES.txt +1 -0
- {fix_cli-0.6.1 → fix_cli-0.6.4}/protocol.py +5 -4
- {fix_cli-0.6.1 → fix_cli-0.6.4}/pyproject.toml +1 -1
- fix_cli-0.6.4/scrubber.py +790 -0
- {fix_cli-0.6.1 → fix_cli-0.6.4}/server/app.py +167 -64
- fix_cli-0.6.4/server/escrow.py +702 -0
- {fix_cli-0.6.1 → fix_cli-0.6.4}/server/judge.py +98 -35
- {fix_cli-0.6.1 → fix_cli-0.6.4}/server/nano.py +71 -309
- {fix_cli-0.6.1 → fix_cli-0.6.4}/server/store.py +30 -26
- {fix_cli-0.6.1 → fix_cli-0.6.4}/tests/test_app.py +324 -31
- {fix_cli-0.6.1 → fix_cli-0.6.4}/tests/test_crypto.py +175 -0
- fix_cli-0.6.4/tests/test_escrow.py +406 -0
- {fix_cli-0.6.1 → fix_cli-0.6.4}/tests/test_judge.py +3 -3
- fix_cli-0.6.4/tests/test_nano.py +423 -0
- {fix_cli-0.6.1 → fix_cli-0.6.4}/tests/test_nano_integration.py +1 -0
- fix_cli-0.6.4/tests/test_security.py +153 -0
- {fix_cli-0.6.1 → fix_cli-0.6.4}/tests/test_server.py +9 -11
- fix_cli-0.6.1/scrubber.py +0 -358
- fix_cli-0.6.1/server/escrow.py +0 -555
- fix_cli-0.6.1/tests/test_escrow.py +0 -429
- fix_cli-0.6.1/tests/test_nano.py +0 -572
- {fix_cli-0.6.1 → fix_cli-0.6.4}/LICENSE +0 -0
- {fix_cli-0.6.1 → fix_cli-0.6.4}/README.md +0 -0
- {fix_cli-0.6.1 → fix_cli-0.6.4}/client.py +0 -0
- {fix_cli-0.6.1 → fix_cli-0.6.4}/fix_cli.egg-info/dependency_links.txt +0 -0
- {fix_cli-0.6.1 → fix_cli-0.6.4}/fix_cli.egg-info/entry_points.txt +0 -0
- {fix_cli-0.6.1 → fix_cli-0.6.4}/fix_cli.egg-info/requires.txt +0 -0
- {fix_cli-0.6.1 → fix_cli-0.6.4}/fix_cli.egg-info/top_level.txt +0 -0
- {fix_cli-0.6.1 → fix_cli-0.6.4}/server/__init__.py +0 -0
- {fix_cli-0.6.1 → fix_cli-0.6.4}/setup.cfg +0 -0
- {fix_cli-0.6.1 → fix_cli-0.6.4}/tests/test_agent.py +0 -0
- {fix_cli-0.6.1 → fix_cli-0.6.4}/tests/test_cli.py +0 -0
- {fix_cli-0.6.1 → fix_cli-0.6.4}/tests/test_client.py +0 -0
- {fix_cli-0.6.1 → fix_cli-0.6.4}/tests/test_contract.py +0 -0
- {fix_cli-0.6.1 → fix_cli-0.6.4}/tests/test_scrubber.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: fix-cli
|
|
3
|
-
Version: 0.6.
|
|
3
|
+
Version: 0.6.4
|
|
4
4
|
Summary: AI-powered command fixer with contract-based dispute resolution
|
|
5
5
|
Author-email: Karan Sharma <karans4@protonmail.com>
|
|
6
6
|
License: AGPL-3.0-or-later
|
|
@@ -15,6 +15,7 @@ Requires-Dist: uvicorn>=0.20; extra == "server"
|
|
|
15
15
|
Requires-Dist: starlette>=0.27; extra == "server"
|
|
16
16
|
Requires-Dist: ed25519-blake2b>=1.4; extra == "server"
|
|
17
17
|
Requires-Dist: requests>=2.28; extra == "server"
|
|
18
|
+
Dynamic: license-file
|
|
18
19
|
|
|
19
20
|
# fix
|
|
20
21
|
|
|
@@ -53,7 +53,9 @@ def build_agent_prompt(contract: dict, investigation_results: list[dict],
|
|
|
53
53
|
parts.append("You are a fix agent. Your job is to diagnose and fix the problem described in this contract.")
|
|
54
54
|
parts.append("You may decline (\"accepted\": false) before or after investigation, as long as you haven't accepted yet.\n")
|
|
55
55
|
parts.append("## Contract\n")
|
|
56
|
+
parts.append('<user-content type="contract">')
|
|
56
57
|
parts.append(json.dumps(contract, indent=2))
|
|
58
|
+
parts.append('</user-content>')
|
|
57
59
|
parts.append("")
|
|
58
60
|
|
|
59
61
|
if agent_memory:
|
|
@@ -75,7 +77,9 @@ def build_agent_prompt(contract: dict, investigation_results: list[dict],
|
|
|
75
77
|
parts.append("## Investigation Results So Far\n")
|
|
76
78
|
for i, r in enumerate(investigation_results, 1):
|
|
77
79
|
parts.append(f"### Round {i}: `{r['command']}`")
|
|
80
|
+
parts.append(f'<user-content type="investigation_result">')
|
|
78
81
|
parts.append(f"```\n{r['output']}\n```")
|
|
82
|
+
parts.append('</user-content>')
|
|
79
83
|
parts.append("")
|
|
80
84
|
|
|
81
85
|
parts.append("## Instructions\n")
|
|
@@ -406,12 +410,6 @@ class FixAgent:
|
|
|
406
410
|
return False, f"bounty {bounty} below minimum {self.min_bounty}"
|
|
407
411
|
if bounty > self.max_bounty:
|
|
408
412
|
return False, f"bounty {bounty} above maximum {self.max_bounty}"
|
|
409
|
-
# Check min_bond requirement against our capabilities
|
|
410
|
-
terms = contract.get("terms", {})
|
|
411
|
-
min_bond = terms.get("min_bond", "0")
|
|
412
|
-
if min_bond and Decimal(min_bond) > Decimal("0"):
|
|
413
|
-
pass # Agent can check their own balance here if needed
|
|
414
|
-
|
|
415
413
|
match, reason = capabilities_match(self.capabilities, contract)
|
|
416
414
|
if not match:
|
|
417
415
|
return False, reason
|
|
@@ -492,13 +490,11 @@ async def _main():
|
|
|
492
490
|
with open(key_path, "rb") as f:
|
|
493
491
|
privkey = f.read(32)
|
|
494
492
|
else:
|
|
495
|
-
from crypto import generate_ed25519_keypair, pubkey_to_fix_id
|
|
493
|
+
from crypto import generate_ed25519_keypair, pubkey_to_fix_id, save_ed25519_key
|
|
496
494
|
privkey, pubkey = generate_ed25519_keypair()
|
|
497
495
|
os.makedirs(os.path.dirname(key_path), exist_ok=True)
|
|
498
|
-
|
|
499
|
-
f.write(privkey)
|
|
496
|
+
save_ed25519_key(key_path, privkey)
|
|
500
497
|
print(f"Generated new agent identity: {pubkey_to_fix_id(pubkey)}")
|
|
501
|
-
print(f"Key saved to {key_path}")
|
|
502
498
|
|
|
503
499
|
from crypto import ed25519_privkey_to_pubkey, pubkey_to_fix_id
|
|
504
500
|
pubkey = ed25519_privkey_to_pubkey(privkey)
|
|
@@ -169,7 +169,7 @@ def build_contract(command, stderr, env_info, **kwargs):
|
|
|
169
169
|
|
|
170
170
|
# Market mode: add escrow + terms
|
|
171
171
|
if market:
|
|
172
|
-
bounty_str = str(bounty) if bounty else "0.
|
|
172
|
+
bounty_str = str(bounty) if bounty else "0.19"
|
|
173
173
|
contract["escrow"] = {
|
|
174
174
|
"bounty": bounty_str,
|
|
175
175
|
"currency": "XNO",
|
|
@@ -178,8 +178,6 @@ def build_contract(command, stderr, env_info, **kwargs):
|
|
|
178
178
|
}
|
|
179
179
|
contract["terms"] = {
|
|
180
180
|
"cancellation": {
|
|
181
|
-
"agent_fee": "0.002",
|
|
182
|
-
"principal_fee": "0.002",
|
|
183
181
|
"grace_period": 30,
|
|
184
182
|
},
|
|
185
183
|
"abandonment": {
|
|
@@ -14,6 +14,7 @@ import hmac as hmac_mod
|
|
|
14
14
|
import json
|
|
15
15
|
import math
|
|
16
16
|
import os
|
|
17
|
+
import sqlite3
|
|
17
18
|
import time as _time
|
|
18
19
|
|
|
19
20
|
from cryptography.hazmat.primitives.asymmetric.ed25519 import (
|
|
@@ -96,8 +97,17 @@ def load_ed25519_key(path: str) -> bytes:
|
|
|
96
97
|
|
|
97
98
|
|
|
98
99
|
def save_ed25519_key(path: str, key: bytes) -> None:
|
|
99
|
-
"""Save a 32-byte raw Ed25519 private key to file (mode 0600).
|
|
100
|
-
|
|
100
|
+
"""Save a 32-byte raw Ed25519 private key to file (mode 0600).
|
|
101
|
+
Uses O_EXCL for new files to prevent symlink attacks.
|
|
102
|
+
Falls back to O_TRUNC for overwrites (existing file must be a regular file)."""
|
|
103
|
+
if os.path.exists(path):
|
|
104
|
+
# Overwrite: verify it's a regular file, not a symlink
|
|
105
|
+
if os.path.islink(path):
|
|
106
|
+
raise ValueError(f"Refusing to overwrite symlink: {path}")
|
|
107
|
+
fd = os.open(path, os.O_WRONLY | os.O_TRUNC, 0o600)
|
|
108
|
+
else:
|
|
109
|
+
# New file: O_EXCL prevents race conditions
|
|
110
|
+
fd = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
|
|
101
111
|
try:
|
|
102
112
|
os.write(fd, key)
|
|
103
113
|
finally:
|
|
@@ -283,30 +293,69 @@ REQUEST_MAX_AGE = 300 # 5 minutes
|
|
|
283
293
|
|
|
284
294
|
|
|
285
295
|
class ReplayGuard:
|
|
286
|
-
"""Track seen signatures to prevent replay attacks.
|
|
296
|
+
"""Track seen signatures to prevent replay attacks.
|
|
287
297
|
|
|
288
|
-
|
|
289
|
-
|
|
298
|
+
Uses SQLite for persistence across restarts and multi-process safety.
|
|
299
|
+
Falls back to in-memory dict if no db_path provided.
|
|
300
|
+
"""
|
|
301
|
+
|
|
302
|
+
def __init__(self, ttl: int = REQUEST_MAX_AGE + 30, db_path: str = ""):
|
|
290
303
|
self._ttl = ttl
|
|
304
|
+
self._db = None
|
|
305
|
+
if db_path:
|
|
306
|
+
import sqlite3
|
|
307
|
+
self._db = sqlite3.connect(db_path, check_same_thread=False)
|
|
308
|
+
self._db.execute("PRAGMA journal_mode=WAL")
|
|
309
|
+
self._db.execute(
|
|
310
|
+
"CREATE TABLE IF NOT EXISTS replay_guard "
|
|
311
|
+
"(sig TEXT PRIMARY KEY, expires_at REAL)"
|
|
312
|
+
)
|
|
313
|
+
self._db.commit()
|
|
314
|
+
self._seen: dict[str, float] = {}
|
|
291
315
|
self._check_count = 0
|
|
292
316
|
|
|
293
317
|
def check_and_record(self, sig_hex: str) -> bool:
|
|
294
318
|
"""Return False if sig was already seen, True if new (and record it)."""
|
|
295
|
-
self._check_count += 1
|
|
296
|
-
if self._check_count % 100 == 0:
|
|
297
|
-
self._prune()
|
|
298
|
-
|
|
299
|
-
now = _time.time()
|
|
300
|
-
if sig_hex in self._seen:
|
|
301
|
-
if now < self._seen[sig_hex]:
|
|
302
|
-
return False # still valid, replay detected
|
|
303
|
-
# expired entry, treat as new
|
|
304
|
-
self._seen[sig_hex] = now + self._ttl
|
|
305
|
-
return True
|
|
306
|
-
|
|
307
|
-
def _prune(self):
|
|
308
319
|
now = _time.time()
|
|
309
|
-
|
|
320
|
+
expires = now + self._ttl
|
|
321
|
+
|
|
322
|
+
if self._db:
|
|
323
|
+
# Periodic prune
|
|
324
|
+
self._check_count += 1
|
|
325
|
+
if self._check_count % 100 == 0:
|
|
326
|
+
self._db.execute("DELETE FROM replay_guard WHERE expires_at <= ?", (now,))
|
|
327
|
+
self._db.commit()
|
|
328
|
+
# Atomic check-and-insert
|
|
329
|
+
try:
|
|
330
|
+
self._db.execute(
|
|
331
|
+
"INSERT INTO replay_guard (sig, expires_at) VALUES (?, ?)",
|
|
332
|
+
(sig_hex, expires),
|
|
333
|
+
)
|
|
334
|
+
self._db.commit()
|
|
335
|
+
return True # new signature
|
|
336
|
+
except sqlite3.IntegrityError:
|
|
337
|
+
# Unique constraint violation = already seen, or expired
|
|
338
|
+
row = self._db.execute(
|
|
339
|
+
"SELECT expires_at FROM replay_guard WHERE sig = ?", (sig_hex,)
|
|
340
|
+
).fetchone()
|
|
341
|
+
if row and row[0] > now:
|
|
342
|
+
return False # still valid, replay
|
|
343
|
+
# Expired, update
|
|
344
|
+
self._db.execute(
|
|
345
|
+
"UPDATE replay_guard SET expires_at = ? WHERE sig = ?",
|
|
346
|
+
(expires, sig_hex),
|
|
347
|
+
)
|
|
348
|
+
self._db.commit()
|
|
349
|
+
return True
|
|
350
|
+
else:
|
|
351
|
+
# In-memory fallback
|
|
352
|
+
self._check_count += 1
|
|
353
|
+
if self._check_count % 100 == 0:
|
|
354
|
+
self._seen = {k: v for k, v in self._seen.items() if v > now}
|
|
355
|
+
if sig_hex in self._seen and now < self._seen[sig_hex]:
|
|
356
|
+
return False
|
|
357
|
+
self._seen[sig_hex] = expires
|
|
358
|
+
return True
|
|
310
359
|
|
|
311
360
|
|
|
312
361
|
def sign_request_ed25519(
|
|
@@ -352,9 +401,9 @@ def verify_request_ed25519(
|
|
|
352
401
|
now = _time.time()
|
|
353
402
|
age = now - ts
|
|
354
403
|
if age < -30: # allow 30s clock skew for future timestamps
|
|
355
|
-
return False,
|
|
404
|
+
return False, "request timestamp is in the future"
|
|
356
405
|
if age > REQUEST_MAX_AGE:
|
|
357
|
-
return False,
|
|
406
|
+
return False, "request expired"
|
|
358
407
|
|
|
359
408
|
# Decode pubkey
|
|
360
409
|
try:
|
|
@@ -85,21 +85,20 @@ INVESTIGATE_WHITELIST = {
|
|
|
85
85
|
# Directory listing
|
|
86
86
|
"ls", "find", "tree", "du",
|
|
87
87
|
# Search
|
|
88
|
-
"grep", "rg", "ag",
|
|
88
|
+
"grep", "rg", "ag",
|
|
89
89
|
# Versions/info
|
|
90
90
|
"which", "whereis", "type", "command", "uname", "arch", "lsb_release", "hostnamectl",
|
|
91
91
|
# Package queries
|
|
92
92
|
"dpkg", "apt", "apt-cache", "apt-file", "apt-list", "rpm", "pacman",
|
|
93
93
|
"pip", "pip3", "npm", "gem", "cargo", "rustc",
|
|
94
94
|
# Runtime versions
|
|
95
|
-
"
|
|
95
|
+
"gcc", "g++", "make", "cmake",
|
|
96
96
|
"clang", "clang++", "ld", "as", "nasm",
|
|
97
97
|
# Environment
|
|
98
|
-
"
|
|
98
|
+
"echo", "id", "whoami", "pwd", "hostname",
|
|
99
99
|
# System info
|
|
100
100
|
"lsmod", "lscpu", "free", "df", "mount", "ip", "ss", "ps",
|
|
101
|
-
#
|
|
102
|
-
"journalctl", "dmesg",
|
|
101
|
+
# (logs removed — system info leakage)
|
|
103
102
|
# Misc
|
|
104
103
|
"readlink", "realpath", "basename", "dirname", "diff", "cmp",
|
|
105
104
|
"strings", "nm", "ldd", "objdump", "pkg-config", "test", "timeout",
|
|
@@ -134,6 +133,13 @@ def validate_investigate_command(cmd, root=None):
|
|
|
134
133
|
if not cmd:
|
|
135
134
|
return False, "empty command"
|
|
136
135
|
|
|
136
|
+
# Block ALL shell metacharacters — eliminates pipe injection, redirects,
|
|
137
|
+
# command chaining, and code execution via subshells in one check
|
|
138
|
+
SHELL_METACHARS = set('|;&$`()')
|
|
139
|
+
for ch in cmd:
|
|
140
|
+
if ch in SHELL_METACHARS:
|
|
141
|
+
return False, f"blocked: contains shell metacharacter '{ch}'"
|
|
142
|
+
|
|
137
143
|
# Block command substitution: $(...), `...`, <(...)
|
|
138
144
|
if re.search(r'\$\(', cmd):
|
|
139
145
|
return False, "blocked: contains $() command substitution"
|
|
@@ -169,7 +175,7 @@ def validate_investigate_command(cmd, root=None):
|
|
|
169
175
|
if first_word not in INVESTIGATE_WHITELIST:
|
|
170
176
|
return False, f"'{first_word}' not in investigation whitelist"
|
|
171
177
|
# Commands that run other commands — check their argument too
|
|
172
|
-
if first_word in ("timeout", "time", "nice", "ionice"
|
|
178
|
+
if first_word in ("timeout", "time", "nice", "ionice"):
|
|
173
179
|
parts = subcmd.split()
|
|
174
180
|
# Skip flags and the timeout value to find the actual command
|
|
175
181
|
i = 1
|
|
@@ -180,34 +186,18 @@ def validate_investigate_command(cmd, root=None):
|
|
|
180
186
|
if actual not in INVESTIGATE_WHITELIST:
|
|
181
187
|
return False, f"'{actual}' (via {first_word}) not in investigation whitelist"
|
|
182
188
|
|
|
183
|
-
# Block dangerous argument patterns for
|
|
184
|
-
EXEC_CAPABLE = {"python3", "python", "node", "ruby", "java", "go"}
|
|
185
|
-
EXEC_FLAGS = {"-c", "-e", "--eval", "-exec", "--exec"}
|
|
189
|
+
# Block dangerous argument patterns for remaining whitelisted commands
|
|
186
190
|
for subcmd in subcmds:
|
|
187
191
|
subcmd = subcmd.strip()
|
|
188
192
|
if not subcmd:
|
|
189
193
|
continue
|
|
190
194
|
parts = subcmd.split()
|
|
191
195
|
first = os.path.basename(parts[0])
|
|
192
|
-
# Block interpreter execution flags
|
|
193
|
-
if first in EXEC_CAPABLE:
|
|
194
|
-
for p in parts[1:]:
|
|
195
|
-
if p in EXEC_FLAGS:
|
|
196
|
-
return False, f"blocked: '{first} {p}' can execute arbitrary code"
|
|
197
196
|
# Block find -exec
|
|
198
197
|
if first == "find":
|
|
199
198
|
for p in parts[1:]:
|
|
200
199
|
if p in ("-exec", "-execdir", "-ok", "-okdir", "-delete"):
|
|
201
200
|
return False, f"blocked: 'find {p}' can modify files"
|
|
202
|
-
# Block awk/sed system() calls
|
|
203
|
-
if first in ("awk", "gawk", "mawk", "nawk"):
|
|
204
|
-
cmd_lower = subcmd.lower()
|
|
205
|
-
if "system(" in cmd_lower or "getline" in cmd_lower:
|
|
206
|
-
return False, f"blocked: '{first}' with system()/getline can execute code"
|
|
207
|
-
if first == "sed":
|
|
208
|
-
for p in parts[1:]:
|
|
209
|
-
if p in ("-i", "--in-place"):
|
|
210
|
-
return False, f"blocked: 'sed {p}' can modify files"
|
|
211
201
|
|
|
212
202
|
# Root jail: check that all path arguments resolve inside root
|
|
213
203
|
if root:
|
|
@@ -913,9 +903,11 @@ def _first_run_setup():
|
|
|
913
903
|
backend_line = 'backend = "auto"'
|
|
914
904
|
print(f" {C_DIM}Unrecognized key prefix, saved as custom.{C_RESET}")
|
|
915
905
|
print(f" {C_DIM}Set FIX_API_URL and FIX_MODEL env vars for custom endpoints.{C_RESET}")
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
906
|
+
fd = os.open(keyfile, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
|
|
907
|
+
try:
|
|
908
|
+
os.write(fd, key.encode())
|
|
909
|
+
finally:
|
|
910
|
+
os.close(fd)
|
|
919
911
|
with open(config_path, "w") as f:
|
|
920
912
|
f.write(f'# fix global config -- {provider} API\n')
|
|
921
913
|
f.write(f'{backend_line}\n')
|
|
@@ -2654,23 +2646,22 @@ def apply_fix(fix_cmd, original_cmd, verify_spec, safe_mode, cfg, contract=None)
|
|
|
2654
2646
|
msg += " System unchanged."
|
|
2655
2647
|
status(f"{C_YELLOW}\u2718{C_RESET}", msg)
|
|
2656
2648
|
if contract and contract.get("escrow"):
|
|
2657
|
-
cancel_fee = contract.get("terms", {}).get("cancellation", {}).get("agent_fee", "0.002")
|
|
2658
2649
|
bounty = contract["escrow"]["bounty"]
|
|
2659
2650
|
both_evil = "evil_agent" in judge_flags and "evil_principal" in judge_flags
|
|
2660
2651
|
agent_evil = "evil_agent" in judge_flags
|
|
2661
2652
|
principal_evil = "evil_principal" in judge_flags
|
|
2662
2653
|
if both_evil:
|
|
2663
2654
|
status(f"{C_RED}${C_RESET}",
|
|
2664
|
-
f"Escrow: {bounty} +
|
|
2655
|
+
f"Escrow: {bounty} + bonds donated to charity (both parties flagged)")
|
|
2665
2656
|
elif agent_evil:
|
|
2666
2657
|
status(f"{C_RED}${C_RESET}",
|
|
2667
|
-
f"Escrow: {bounty} returned to principal (agent flagged,
|
|
2658
|
+
f"Escrow: {bounty} returned to principal (agent flagged, bond to charity)")
|
|
2668
2659
|
elif principal_evil:
|
|
2669
2660
|
status(f"{C_RED}${C_RESET}",
|
|
2670
2661
|
f"Escrow: {bounty} donated to charity (principal flagged)")
|
|
2671
2662
|
else:
|
|
2672
2663
|
status(f"{C_DIM}${C_RESET}",
|
|
2673
|
-
f"Escrow: {bounty} returned to principal (
|
|
2664
|
+
f"Escrow: {bounty} returned to principal (minus platform fee)")
|
|
2674
2665
|
|
|
2675
2666
|
return 0 if success else 1
|
|
2676
2667
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: fix-cli
|
|
3
|
-
Version: 0.6.
|
|
3
|
+
Version: 0.6.4
|
|
4
4
|
Summary: AI-powered command fixer with contract-based dispute resolution
|
|
5
5
|
Author-email: Karan Sharma <karans4@protonmail.com>
|
|
6
6
|
License: AGPL-3.0-or-later
|
|
@@ -15,6 +15,7 @@ Requires-Dist: uvicorn>=0.20; extra == "server"
|
|
|
15
15
|
Requires-Dist: starlette>=0.27; extra == "server"
|
|
16
16
|
Requires-Dist: ed25519-blake2b>=1.4; extra == "server"
|
|
17
17
|
Requires-Dist: requests>=2.28; extra == "server"
|
|
18
|
+
Dynamic: license-file
|
|
18
19
|
|
|
19
20
|
# fix
|
|
20
21
|
|
|
@@ -11,11 +11,10 @@ from enum import Enum
|
|
|
11
11
|
|
|
12
12
|
PROTOCOL_VERSION = 2
|
|
13
13
|
|
|
14
|
-
DEFAULT_BOUNTY = "0.
|
|
14
|
+
DEFAULT_BOUNTY = "0.19"
|
|
15
15
|
DEFAULT_CURRENCY = "XNO"
|
|
16
16
|
DEFAULT_CHAIN = "nano"
|
|
17
|
-
|
|
18
|
-
MINIMUM_BOUNTY = "0.05"
|
|
17
|
+
MINIMUM_BOUNTY = "0.19"
|
|
19
18
|
GRACE_PERIOD_SECONDS = 30
|
|
20
19
|
ABANDONMENT_TIMEOUT = 120
|
|
21
20
|
MAX_INVESTIGATION_ROUNDS = 5
|
|
@@ -32,7 +31,9 @@ DEFAULT_REVIEW_WINDOW = 7200 # 2 hours
|
|
|
32
31
|
# Judge defaults
|
|
33
32
|
DEFAULT_JUDGE_FEE = "0.17" # XNO -- each side stakes this as dispute bond
|
|
34
33
|
DEFAULT_RULING_TIMEOUT = 60 # seconds judge has to rule
|
|
35
|
-
|
|
34
|
+
# Inclusive bond: bounty + judge_fee. Both sides pay the same.
|
|
35
|
+
MIN_BOUNTY_EXCESS = Decimal("0.02") # Minimum bounty above judge_fee
|
|
36
|
+
CANCEL_FEE_RATE = Decimal("0.20") # 20% of excess bond (bounty) on cancellation
|
|
36
37
|
|
|
37
38
|
# Tiered court system: escalating models and fees
|
|
38
39
|
COURT_TIERS = [
|