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.
Files changed (66) hide show
  1. {methodproof-0.4.5 → methodproof-0.5.1}/CHANGELOG.md +13 -0
  2. {methodproof-0.4.5 → methodproof-0.5.1}/PKG-INFO +3 -5
  3. {methodproof-0.4.5 → methodproof-0.5.1}/methodproof/__init__.py +1 -1
  4. methodproof-0.5.1/methodproof/_daemon.py +114 -0
  5. {methodproof-0.4.5 → methodproof-0.5.1}/methodproof/agents/base.py +19 -5
  6. {methodproof-0.4.5 → methodproof-0.5.1}/methodproof/agents/music.py +4 -2
  7. methodproof-0.5.1/methodproof/binding.py +28 -0
  8. methodproof-0.5.1/methodproof/bip39.py +37 -0
  9. {methodproof-0.4.5 → methodproof-0.5.1}/methodproof/bridge.py +3 -1
  10. {methodproof-0.4.5 → methodproof-0.5.1}/methodproof/cli.py +241 -80
  11. {methodproof-0.4.5 → methodproof-0.5.1}/methodproof/config.py +4 -1
  12. {methodproof-0.4.5 → methodproof-0.5.1}/methodproof/graph.py +3 -2
  13. methodproof-0.5.1/methodproof/hooks/claude_code.py +86 -0
  14. {methodproof-0.4.5 → methodproof-0.5.1}/methodproof/integrity.py +6 -3
  15. methodproof-0.5.1/methodproof/kdf.py +25 -0
  16. methodproof-0.5.1/methodproof/keychain.py +71 -0
  17. {methodproof-0.4.5 → methodproof-0.5.1}/methodproof/live.py +5 -3
  18. methodproof-0.5.1/methodproof/lock.py +41 -0
  19. {methodproof-0.4.5 → methodproof-0.5.1}/methodproof/mcp.py +2 -2
  20. methodproof-0.5.1/methodproof/migrate_db.py +42 -0
  21. {methodproof-0.4.5 → methodproof-0.5.1}/methodproof/proxy_daemon.py +7 -6
  22. {methodproof-0.4.5 → methodproof-0.5.1}/methodproof/store.py +17 -4
  23. {methodproof-0.4.5 → methodproof-0.5.1}/methodproof/sync.py +13 -5
  24. methodproof-0.5.1/methodproof/wordlist.py +2052 -0
  25. {methodproof-0.4.5 → methodproof-0.5.1}/pyproject.toml +2 -4
  26. methodproof-0.5.1/tests/test_security.py +370 -0
  27. methodproof-0.5.1/uv.lock +1811 -0
  28. methodproof-0.4.5/methodproof/hooks/claude_code.py +0 -73
  29. methodproof-0.4.5/uv.lock +0 -272
  30. {methodproof-0.4.5 → methodproof-0.5.1}/.github/workflows/ci.yml +0 -0
  31. {methodproof-0.4.5 → methodproof-0.5.1}/.gitignore +0 -0
  32. {methodproof-0.4.5 → methodproof-0.5.1}/LICENSE +0 -0
  33. {methodproof-0.4.5 → methodproof-0.5.1}/README.md +0 -0
  34. {methodproof-0.4.5 → methodproof-0.5.1}/methodproof/__main__.py +0 -0
  35. {methodproof-0.4.5 → methodproof-0.5.1}/methodproof/agents/__init__.py +0 -0
  36. {methodproof-0.4.5 → methodproof-0.5.1}/methodproof/agents/terminal.py +0 -0
  37. {methodproof-0.4.5 → methodproof-0.5.1}/methodproof/agents/watcher.py +0 -0
  38. {methodproof-0.4.5 → methodproof-0.5.1}/methodproof/analysis.py +0 -0
  39. {methodproof-0.4.5 → methodproof-0.5.1}/methodproof/crypto.py +0 -0
  40. {methodproof-0.4.5 → methodproof-0.5.1}/methodproof/hook.py +0 -0
  41. {methodproof-0.4.5 → methodproof-0.5.1}/methodproof/hooks/__init__.py +0 -0
  42. {methodproof-0.4.5 → methodproof-0.5.1}/methodproof/hooks/claude_code.sh +0 -0
  43. {methodproof-0.4.5 → methodproof-0.5.1}/methodproof/hooks/cline_hook.sh +0 -0
  44. {methodproof-0.4.5 → methodproof-0.5.1}/methodproof/hooks/codex_hook.sh +0 -0
  45. {methodproof-0.4.5 → methodproof-0.5.1}/methodproof/hooks/gemini_hook.sh +0 -0
  46. {methodproof-0.4.5 → methodproof-0.5.1}/methodproof/hooks/install.py +0 -0
  47. {methodproof-0.4.5 → methodproof-0.5.1}/methodproof/hooks/kiro_hook.sh +0 -0
  48. {methodproof-0.4.5 → methodproof-0.5.1}/methodproof/hooks/mcp_register.py +0 -0
  49. {methodproof-0.4.5 → methodproof-0.5.1}/methodproof/hooks/openclaw/HOOK.md +0 -0
  50. {methodproof-0.4.5 → methodproof-0.5.1}/methodproof/hooks/openclaw/handler.ts +0 -0
  51. {methodproof-0.4.5 → methodproof-0.5.1}/methodproof/hooks/openclaw_install.py +0 -0
  52. {methodproof-0.4.5 → methodproof-0.5.1}/methodproof/hooks/opencode_plugin.js +0 -0
  53. {methodproof-0.4.5 → methodproof-0.5.1}/methodproof/hooks/wrappers.py +0 -0
  54. {methodproof-0.4.5 → methodproof-0.5.1}/methodproof/proxy.py +0 -0
  55. {methodproof-0.4.5 → methodproof-0.5.1}/methodproof/repos.py +0 -0
  56. {methodproof-0.4.5 → methodproof-0.5.1}/methodproof/skills/methodproof/SKILL.md +0 -0
  57. {methodproof-0.4.5 → methodproof-0.5.1}/methodproof/viewer.py +0 -0
  58. {methodproof-0.4.5 → methodproof-0.5.1}/test_windows_compat.py +0 -0
  59. {methodproof-0.4.5 → methodproof-0.5.1}/tests/__init__.py +0 -0
  60. {methodproof-0.4.5 → methodproof-0.5.1}/tests/test_analysis.py +0 -0
  61. {methodproof-0.4.5 → methodproof-0.5.1}/tests/test_graph.py +0 -0
  62. {methodproof-0.4.5 → methodproof-0.5.1}/tests/test_hooks.py +0 -0
  63. {methodproof-0.4.5 → methodproof-0.5.1}/tests/test_live.py +0 -0
  64. {methodproof-0.4.5 → methodproof-0.5.1}/tests/test_openclaw_hooks.py +0 -0
  65. {methodproof-0.4.5 → methodproof-0.5.1}/tests/test_store.py +0 -0
  66. {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.4.5
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">
@@ -1,3 +1,3 @@
1
1
  """MethodProof — see how you code."""
2
2
 
3
- __version__ = "0.4.5"
3
+ __version__ = "0.5.0"
@@ -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
- _journal_mode = False
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
- raw = cfg.get("e2e_key", "")
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
- pass
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":