methodproof 0.4.5__tar.gz → 0.5.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.
- {methodproof-0.4.5 → methodproof-0.5.1}/CHANGELOG.md +13 -0
- {methodproof-0.4.5 → methodproof-0.5.1}/PKG-INFO +3 -5
- {methodproof-0.4.5 → methodproof-0.5.1}/methodproof/__init__.py +1 -1
- methodproof-0.5.1/methodproof/_daemon.py +114 -0
- {methodproof-0.4.5 → methodproof-0.5.1}/methodproof/agents/base.py +19 -5
- {methodproof-0.4.5 → methodproof-0.5.1}/methodproof/agents/music.py +4 -2
- methodproof-0.5.1/methodproof/binding.py +28 -0
- methodproof-0.5.1/methodproof/bip39.py +37 -0
- {methodproof-0.4.5 → methodproof-0.5.1}/methodproof/bridge.py +3 -1
- {methodproof-0.4.5 → methodproof-0.5.1}/methodproof/cli.py +241 -80
- {methodproof-0.4.5 → methodproof-0.5.1}/methodproof/config.py +4 -1
- {methodproof-0.4.5 → methodproof-0.5.1}/methodproof/graph.py +3 -2
- methodproof-0.5.1/methodproof/hooks/claude_code.py +86 -0
- {methodproof-0.4.5 → methodproof-0.5.1}/methodproof/integrity.py +6 -3
- methodproof-0.5.1/methodproof/kdf.py +25 -0
- methodproof-0.5.1/methodproof/keychain.py +71 -0
- {methodproof-0.4.5 → methodproof-0.5.1}/methodproof/live.py +5 -3
- methodproof-0.5.1/methodproof/lock.py +41 -0
- {methodproof-0.4.5 → methodproof-0.5.1}/methodproof/mcp.py +2 -2
- methodproof-0.5.1/methodproof/migrate_db.py +42 -0
- {methodproof-0.4.5 → methodproof-0.5.1}/methodproof/proxy_daemon.py +7 -6
- {methodproof-0.4.5 → methodproof-0.5.1}/methodproof/store.py +17 -4
- {methodproof-0.4.5 → methodproof-0.5.1}/methodproof/sync.py +13 -5
- methodproof-0.5.1/methodproof/wordlist.py +2052 -0
- {methodproof-0.4.5 → methodproof-0.5.1}/pyproject.toml +2 -4
- methodproof-0.5.1/tests/test_security.py +370 -0
- methodproof-0.5.1/uv.lock +1811 -0
- methodproof-0.4.5/methodproof/hooks/claude_code.py +0 -73
- methodproof-0.4.5/uv.lock +0 -272
- {methodproof-0.4.5 → methodproof-0.5.1}/.github/workflows/ci.yml +0 -0
- {methodproof-0.4.5 → methodproof-0.5.1}/.gitignore +0 -0
- {methodproof-0.4.5 → methodproof-0.5.1}/LICENSE +0 -0
- {methodproof-0.4.5 → methodproof-0.5.1}/README.md +0 -0
- {methodproof-0.4.5 → methodproof-0.5.1}/methodproof/__main__.py +0 -0
- {methodproof-0.4.5 → methodproof-0.5.1}/methodproof/agents/__init__.py +0 -0
- {methodproof-0.4.5 → methodproof-0.5.1}/methodproof/agents/terminal.py +0 -0
- {methodproof-0.4.5 → methodproof-0.5.1}/methodproof/agents/watcher.py +0 -0
- {methodproof-0.4.5 → methodproof-0.5.1}/methodproof/analysis.py +0 -0
- {methodproof-0.4.5 → methodproof-0.5.1}/methodproof/crypto.py +0 -0
- {methodproof-0.4.5 → methodproof-0.5.1}/methodproof/hook.py +0 -0
- {methodproof-0.4.5 → methodproof-0.5.1}/methodproof/hooks/__init__.py +0 -0
- {methodproof-0.4.5 → methodproof-0.5.1}/methodproof/hooks/claude_code.sh +0 -0
- {methodproof-0.4.5 → methodproof-0.5.1}/methodproof/hooks/cline_hook.sh +0 -0
- {methodproof-0.4.5 → methodproof-0.5.1}/methodproof/hooks/codex_hook.sh +0 -0
- {methodproof-0.4.5 → methodproof-0.5.1}/methodproof/hooks/gemini_hook.sh +0 -0
- {methodproof-0.4.5 → methodproof-0.5.1}/methodproof/hooks/install.py +0 -0
- {methodproof-0.4.5 → methodproof-0.5.1}/methodproof/hooks/kiro_hook.sh +0 -0
- {methodproof-0.4.5 → methodproof-0.5.1}/methodproof/hooks/mcp_register.py +0 -0
- {methodproof-0.4.5 → methodproof-0.5.1}/methodproof/hooks/openclaw/HOOK.md +0 -0
- {methodproof-0.4.5 → methodproof-0.5.1}/methodproof/hooks/openclaw/handler.ts +0 -0
- {methodproof-0.4.5 → methodproof-0.5.1}/methodproof/hooks/openclaw_install.py +0 -0
- {methodproof-0.4.5 → methodproof-0.5.1}/methodproof/hooks/opencode_plugin.js +0 -0
- {methodproof-0.4.5 → methodproof-0.5.1}/methodproof/hooks/wrappers.py +0 -0
- {methodproof-0.4.5 → methodproof-0.5.1}/methodproof/proxy.py +0 -0
- {methodproof-0.4.5 → methodproof-0.5.1}/methodproof/repos.py +0 -0
- {methodproof-0.4.5 → methodproof-0.5.1}/methodproof/skills/methodproof/SKILL.md +0 -0
- {methodproof-0.4.5 → methodproof-0.5.1}/methodproof/viewer.py +0 -0
- {methodproof-0.4.5 → methodproof-0.5.1}/test_windows_compat.py +0 -0
- {methodproof-0.4.5 → methodproof-0.5.1}/tests/__init__.py +0 -0
- {methodproof-0.4.5 → methodproof-0.5.1}/tests/test_analysis.py +0 -0
- {methodproof-0.4.5 → methodproof-0.5.1}/tests/test_graph.py +0 -0
- {methodproof-0.4.5 → methodproof-0.5.1}/tests/test_hooks.py +0 -0
- {methodproof-0.4.5 → methodproof-0.5.1}/tests/test_live.py +0 -0
- {methodproof-0.4.5 → methodproof-0.5.1}/tests/test_openclaw_hooks.py +0 -0
- {methodproof-0.4.5 → methodproof-0.5.1}/tests/test_store.py +0 -0
- {methodproof-0.4.5 → methodproof-0.5.1}/tests/test_wrappers.py +0 -0
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.5.1] — 2026-04-06
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
- macOS daemon segfault: replaced `os.fork()` with `subprocess.Popen()` — CoreFoundation crash after fork killed all capture agents, producing 0 events
|
|
7
|
+
- Hook type mismatches: `task_created`→`task_start`, `task_completed`→`task_end`, added required `tool` field to all hook metadata
|
|
8
|
+
- Unmapped hook events dropped instead of sent as invalid `claude_code_event` type (rejected entire batch)
|
|
9
|
+
- Daemon output logged to `~/.methodproof/daemon.log` instead of `/dev/null`
|
|
10
|
+
- ~20 silent `except Exception: pass` blocks replaced with structured logging across bridge, hooks, live streaming, sync, keychain, MCP, and proxy
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
- Daemon health check on startup — immediate error if daemon exits
|
|
14
|
+
- Extension status check shows actual error on failure
|
|
15
|
+
|
|
3
16
|
## [0.3.4] — 2026-04-05
|
|
4
17
|
|
|
5
18
|
### Added
|
|
@@ -1,18 +1,16 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: methodproof
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.1
|
|
4
4
|
Summary: See how you code. Capture and visualize your engineering process.
|
|
5
5
|
License-Expression: Apache-2.0
|
|
6
6
|
License-File: LICENSE
|
|
7
7
|
Requires-Python: >=3.11
|
|
8
|
+
Requires-Dist: cryptography>=43.0
|
|
9
|
+
Requires-Dist: keyring>=25.0
|
|
8
10
|
Requires-Dist: watchdog>=4.0
|
|
9
11
|
Requires-Dist: websocket-client>=1.7
|
|
10
|
-
Provides-Extra: e2e
|
|
11
|
-
Requires-Dist: cryptography>=43.0; extra == 'e2e'
|
|
12
12
|
Provides-Extra: proxy
|
|
13
13
|
Requires-Dist: mitmproxy>=10.0; extra == 'proxy'
|
|
14
|
-
Provides-Extra: signing
|
|
15
|
-
Requires-Dist: cryptography>=43.0; extra == 'signing'
|
|
16
14
|
Description-Content-Type: text/markdown
|
|
17
15
|
|
|
18
16
|
<p align="center">
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"""Daemon process for methodproof capture — spawned by cmd_start via subprocess.
|
|
2
|
+
|
|
3
|
+
Uses subprocess.Popen (fork+exec) instead of os.fork() to avoid macOS
|
|
4
|
+
CoreFoundation segfaults when forking a multi-threaded Python 3.12+ process.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import signal
|
|
8
|
+
import sys
|
|
9
|
+
import threading
|
|
10
|
+
import time
|
|
11
|
+
|
|
12
|
+
from methodproof import config, store, graph
|
|
13
|
+
from methodproof.agents import base
|
|
14
|
+
|
|
15
|
+
PIDFILE = config.DIR / "methodproof.pid"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def main() -> None:
|
|
19
|
+
sid = sys.argv[1]
|
|
20
|
+
watch_dir = sys.argv[2]
|
|
21
|
+
|
|
22
|
+
cfg = config.load()
|
|
23
|
+
capture = cfg.get("capture", {})
|
|
24
|
+
live_url = cfg.get("_live_url", "")
|
|
25
|
+
|
|
26
|
+
# Clean up transient key
|
|
27
|
+
if "_live_url" in cfg:
|
|
28
|
+
del cfg["_live_url"]
|
|
29
|
+
config.save(cfg)
|
|
30
|
+
|
|
31
|
+
base.init(sid, live=bool(live_url))
|
|
32
|
+
|
|
33
|
+
# Environment profile (was done pre-fork in old code, now done in daemon)
|
|
34
|
+
if capture.get("environment_analysis", True):
|
|
35
|
+
try:
|
|
36
|
+
from methodproof.analysis import scan_environment
|
|
37
|
+
env_profile = scan_environment(watch_dir)
|
|
38
|
+
base.emit("environment_profile", env_profile)
|
|
39
|
+
except Exception as exc:
|
|
40
|
+
base.log("warning", "environment_scan.failed", error=str(exc))
|
|
41
|
+
|
|
42
|
+
stop_event = threading.Event()
|
|
43
|
+
threads: list[threading.Thread] = []
|
|
44
|
+
|
|
45
|
+
files_enabled = (
|
|
46
|
+
capture.get("file_changes", True)
|
|
47
|
+
or capture.get("git_diffs", True)
|
|
48
|
+
or capture.get("git_commits", True)
|
|
49
|
+
)
|
|
50
|
+
if files_enabled:
|
|
51
|
+
from methodproof.agents import watcher
|
|
52
|
+
threads.append(threading.Thread(
|
|
53
|
+
target=watcher.start, args=(watch_dir, stop_event), daemon=True,
|
|
54
|
+
))
|
|
55
|
+
|
|
56
|
+
if capture.get("terminal_commands", True) or capture.get("test_results", True):
|
|
57
|
+
from methodproof.agents import terminal
|
|
58
|
+
threads.append(threading.Thread(
|
|
59
|
+
target=terminal.start, args=(stop_event,), daemon=True,
|
|
60
|
+
))
|
|
61
|
+
|
|
62
|
+
if capture.get("browser", True):
|
|
63
|
+
from methodproof import bridge
|
|
64
|
+
threads.append(threading.Thread(
|
|
65
|
+
target=bridge.start,
|
|
66
|
+
args=(sid, stop_event, 9877,
|
|
67
|
+
cfg.get("token", ""), cfg.get("api_url", ""), cfg.get("e2e_key", "")),
|
|
68
|
+
daemon=True,
|
|
69
|
+
))
|
|
70
|
+
|
|
71
|
+
if capture.get("music", True):
|
|
72
|
+
from methodproof.agents import music
|
|
73
|
+
threads.append(threading.Thread(
|
|
74
|
+
target=music.start, args=(stop_event,), daemon=True,
|
|
75
|
+
))
|
|
76
|
+
|
|
77
|
+
def _shutdown(sig_num: int, frame: object) -> None:
|
|
78
|
+
stop_event.set()
|
|
79
|
+
try:
|
|
80
|
+
if live_url:
|
|
81
|
+
from methodproof import live as live_mod
|
|
82
|
+
live_mod.stop()
|
|
83
|
+
base.flush()
|
|
84
|
+
store.complete_session(sid)
|
|
85
|
+
graph.build(sid)
|
|
86
|
+
except Exception as exc:
|
|
87
|
+
base.log("error", "daemon.shutdown_cleanup_failed", error=str(exc))
|
|
88
|
+
try:
|
|
89
|
+
cfg_now = config.load()
|
|
90
|
+
cfg_now["active_session"] = None
|
|
91
|
+
config.save(cfg_now)
|
|
92
|
+
except Exception as exc:
|
|
93
|
+
base.log("error", "daemon.config_cleanup_failed", error=str(exc))
|
|
94
|
+
PIDFILE.unlink(missing_ok=True)
|
|
95
|
+
sys.exit(0)
|
|
96
|
+
|
|
97
|
+
for t in threads:
|
|
98
|
+
t.start()
|
|
99
|
+
signal.signal(signal.SIGINT, _shutdown)
|
|
100
|
+
signal.signal(signal.SIGTERM, _shutdown)
|
|
101
|
+
|
|
102
|
+
base.log("info", "daemon.started", session_id=sid, watch_dir=watch_dir, agents=len(threads))
|
|
103
|
+
|
|
104
|
+
try:
|
|
105
|
+
while not stop_event.is_set():
|
|
106
|
+
time.sleep(5)
|
|
107
|
+
base.flush()
|
|
108
|
+
except Exception as exc:
|
|
109
|
+
base.log("error", "daemon.loop_crashed", error=str(exc))
|
|
110
|
+
_shutdown(0, None)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
if __name__ == "__main__":
|
|
114
|
+
main()
|
|
@@ -19,6 +19,8 @@ _buffer: list[dict[str, Any]] = []
|
|
|
19
19
|
_FLUSH_SIZE = 50
|
|
20
20
|
_MAX_RETRIES = 3
|
|
21
21
|
_prev_hash = "genesis"
|
|
22
|
+
_account_id = ""
|
|
23
|
+
_journal_mode = False
|
|
22
24
|
|
|
23
25
|
# Maps event types to the capture category that gates them
|
|
24
26
|
_EVENT_GATES: dict[str, str] = {
|
|
@@ -74,21 +76,33 @@ _FIELD_GATES: dict[str, list[tuple[str, str]]] = {
|
|
|
74
76
|
"code_capture": [("file_edit", "diff"), ("git_commit", "diff")],
|
|
75
77
|
}
|
|
76
78
|
|
|
77
|
-
|
|
79
|
+
|
|
80
|
+
def _load_encryption_key(cfg: dict) -> bytes | None:
|
|
81
|
+
"""Load db_key from keychain (preferred) or legacy e2e_key config."""
|
|
82
|
+
account_id = cfg.get("account_id", "")
|
|
83
|
+
if account_id and cfg.get("master_key_fingerprint"):
|
|
84
|
+
from methodproof.keychain import load_secret
|
|
85
|
+
from methodproof.kdf import derive_master, derive_db_key
|
|
86
|
+
master_entropy = load_secret(account_id)
|
|
87
|
+
if master_entropy:
|
|
88
|
+
master = derive_master(master_entropy)
|
|
89
|
+
return derive_db_key(master, account_id)
|
|
90
|
+
raw = cfg.get("e2e_key", "")
|
|
91
|
+
return bytes.fromhex(raw) if raw else None
|
|
78
92
|
|
|
79
93
|
|
|
80
94
|
def init(session_id: str, live: bool = False) -> None:
|
|
81
|
-
global _session_id, _initialized, _e2e_key, _capture, _live_mode, _prev_hash, _journal_mode
|
|
95
|
+
global _session_id, _initialized, _e2e_key, _capture, _live_mode, _prev_hash, _journal_mode, _account_id
|
|
82
96
|
_session_id = session_id
|
|
83
97
|
_initialized = True
|
|
84
98
|
_live_mode = live
|
|
85
99
|
_prev_hash = "genesis"
|
|
86
100
|
from methodproof import config
|
|
87
101
|
cfg = config.load()
|
|
88
|
-
|
|
89
|
-
_e2e_key = bytes.fromhex(raw) if raw else None
|
|
102
|
+
_e2e_key = _load_encryption_key(cfg)
|
|
90
103
|
_capture = cfg.get("capture", {})
|
|
91
104
|
_journal_mode = cfg.get("journal_mode", False)
|
|
105
|
+
_account_id = cfg.get("account_id", "")
|
|
92
106
|
|
|
93
107
|
|
|
94
108
|
def log(level: str, event: str, **kw: object) -> None:
|
|
@@ -133,7 +147,7 @@ def emit(event_type: str, metadata: dict[str, Any]) -> None:
|
|
|
133
147
|
entry["metadata"] = encrypt_metadata(dict(entry["metadata"]), _e2e_key)
|
|
134
148
|
global _prev_hash
|
|
135
149
|
from methodproof.integrity import compute_event_hash
|
|
136
|
-
entry["_chain_hash"] = compute_event_hash(entry, _prev_hash)
|
|
150
|
+
entry["_chain_hash"] = compute_event_hash(entry, _prev_hash, _account_id)
|
|
137
151
|
_prev_hash = entry["_chain_hash"]
|
|
138
152
|
if _live_mode:
|
|
139
153
|
from methodproof import live as live_mod
|
|
@@ -39,7 +39,8 @@ def _get_now_playing_macos() -> dict[str, str] | None:
|
|
|
39
39
|
if len(parts) != 3 or not parts[0]:
|
|
40
40
|
return None
|
|
41
41
|
return {"track": parts[0], "artist": parts[1], "player": parts[2]}
|
|
42
|
-
except Exception:
|
|
42
|
+
except Exception as exc:
|
|
43
|
+
base.log("debug", "music.macos_poll_failed", error=str(exc))
|
|
43
44
|
return None
|
|
44
45
|
|
|
45
46
|
|
|
@@ -58,7 +59,8 @@ def _get_now_playing_linux() -> dict[str, str] | None:
|
|
|
58
59
|
return {"track": parts[1], "artist": parts[0], "player": player_map.get(parts[2].lower(), "unknown")}
|
|
59
60
|
except FileNotFoundError:
|
|
60
61
|
return None
|
|
61
|
-
except Exception:
|
|
62
|
+
except Exception as exc:
|
|
63
|
+
base.log("debug", "music.linux_poll_failed", error=str(exc))
|
|
62
64
|
return None
|
|
63
65
|
|
|
64
66
|
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""Session binding — HMAC ties sessions to account + device."""
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
import hmac
|
|
5
|
+
import os
|
|
6
|
+
import platform
|
|
7
|
+
import sys
|
|
8
|
+
import time as _time
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def compute_binding(bind_key: bytes, session_id: str, account_id: str,
|
|
12
|
+
device_id: str, created_at: float) -> str:
|
|
13
|
+
"""HMAC-SHA256 binding signature for a session."""
|
|
14
|
+
msg = f"{session_id}:{account_id}:{device_id}:{created_at}".encode()
|
|
15
|
+
return hmac.new(bind_key, msg, hashlib.sha256).hexdigest()
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def compute_device_id() -> str:
|
|
19
|
+
"""Deterministic device fingerprint — hash of stable machine attributes."""
|
|
20
|
+
parts = [
|
|
21
|
+
platform.node(),
|
|
22
|
+
platform.system(),
|
|
23
|
+
platform.machine(),
|
|
24
|
+
platform.python_version(),
|
|
25
|
+
_time.tzname[0],
|
|
26
|
+
str(os.cpu_count()),
|
|
27
|
+
]
|
|
28
|
+
return hashlib.sha256(":".join(parts).encode()).hexdigest()[:16]
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""BIP39 mnemonic encoding — 128-bit entropy to 12-word recovery phrase."""
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def entropy_to_phrase(entropy: bytes) -> str:
|
|
7
|
+
"""Encode 16 bytes (128 bits) as 12 BIP39 words with checksum."""
|
|
8
|
+
from methodproof.wordlist import WORDS
|
|
9
|
+
if len(entropy) != 16:
|
|
10
|
+
raise ValueError("Expected 16 bytes of entropy")
|
|
11
|
+
checksum = hashlib.sha256(entropy).digest()[0] >> 4 # 4 bits
|
|
12
|
+
bits = int.from_bytes(entropy) << 4 | checksum # 132 bits
|
|
13
|
+
words = []
|
|
14
|
+
for _ in range(12):
|
|
15
|
+
words.append(WORDS[bits & 0x7FF])
|
|
16
|
+
bits >>= 11
|
|
17
|
+
return " ".join(reversed(words))
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def phrase_to_entropy(phrase: str) -> bytes:
|
|
21
|
+
"""Decode 12 BIP39 words back to 16 bytes of entropy."""
|
|
22
|
+
from methodproof.wordlist import WORDS
|
|
23
|
+
word_list = phrase.strip().lower().split()
|
|
24
|
+
if len(word_list) != 12:
|
|
25
|
+
raise ValueError("Expected 12 words")
|
|
26
|
+
index = {w: i for i, w in enumerate(WORDS)}
|
|
27
|
+
bits = 0
|
|
28
|
+
for word in word_list:
|
|
29
|
+
if word not in index:
|
|
30
|
+
raise ValueError(f"Unknown word: {word}")
|
|
31
|
+
bits = (bits << 11) | index[word]
|
|
32
|
+
checksum = bits & 0xF
|
|
33
|
+
entropy = (bits >> 4).to_bytes(16)
|
|
34
|
+
expected = hashlib.sha256(entropy).digest()[0] >> 4
|
|
35
|
+
if checksum != expected:
|
|
36
|
+
raise ValueError("Invalid checksum")
|
|
37
|
+
return entropy
|
|
@@ -73,7 +73,9 @@ def generate_pair_token(session_id: str, api_token: str, api_base: str, e2e_key:
|
|
|
73
73
|
|
|
74
74
|
class _Handler(BaseHTTPRequestHandler):
|
|
75
75
|
def log_message(self, fmt: str, *args: Any) -> None:
|
|
76
|
-
|
|
76
|
+
# Log errors (4xx/5xx) to structured log; suppress normal request chatter
|
|
77
|
+
if args and str(args[0]).startswith(("4", "5")):
|
|
78
|
+
base.log("warning", "bridge.http_error", status=args[0], path=self.path)
|
|
77
79
|
|
|
78
80
|
def do_GET(self) -> None:
|
|
79
81
|
if self.path == "/session":
|