draft-protocol 1.1.1__tar.gz → 1.3.0__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 (68) hide show
  1. {draft_protocol-1.1.1 → draft_protocol-1.3.0}/PKG-INFO +1 -1
  2. {draft_protocol-1.1.1 → draft_protocol-1.3.0}/pyproject.toml +1 -1
  3. {draft_protocol-1.1.1 → draft_protocol-1.3.0}/src/draft_protocol/__init__.py +1 -1
  4. {draft_protocol-1.1.1 → draft_protocol-1.3.0}/src/draft_protocol/engine.py +26 -3
  5. draft_protocol-1.3.0/src/draft_protocol/hmac_utils.py +131 -0
  6. {draft_protocol-1.1.1 → draft_protocol-1.3.0}/src/draft_protocol/storage.py +15 -0
  7. {draft_protocol-1.1.1 → draft_protocol-1.3.0}/.dockerignore +0 -0
  8. {draft_protocol-1.1.1 → draft_protocol-1.3.0}/.editorconfig +0 -0
  9. {draft_protocol-1.1.1 → draft_protocol-1.3.0}/.github/CODEOWNERS +0 -0
  10. {draft_protocol-1.1.1 → draft_protocol-1.3.0}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  11. {draft_protocol-1.1.1 → draft_protocol-1.3.0}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
  12. {draft_protocol-1.1.1 → draft_protocol-1.3.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  13. {draft_protocol-1.1.1 → draft_protocol-1.3.0}/.github/dependabot.yml +0 -0
  14. {draft_protocol-1.1.1 → draft_protocol-1.3.0}/.github/workflows/ci.yml +0 -0
  15. {draft_protocol-1.1.1 → draft_protocol-1.3.0}/.github/workflows/release.yml +0 -0
  16. {draft_protocol-1.1.1 → draft_protocol-1.3.0}/.gitignore +0 -0
  17. {draft_protocol-1.1.1 → draft_protocol-1.3.0}/.pre-commit-config.yaml +0 -0
  18. {draft_protocol-1.1.1 → draft_protocol-1.3.0}/AGENTS.md +0 -0
  19. {draft_protocol-1.1.1 → draft_protocol-1.3.0}/BENCHMARKS.md +0 -0
  20. {draft_protocol-1.1.1 → draft_protocol-1.3.0}/CHANGELOG.md +0 -0
  21. {draft_protocol-1.1.1 → draft_protocol-1.3.0}/CODE_OF_CONDUCT.md +0 -0
  22. {draft_protocol-1.1.1 → draft_protocol-1.3.0}/CONFORMANCE.md +0 -0
  23. {draft_protocol-1.1.1 → draft_protocol-1.3.0}/CONTRIBUTING.md +0 -0
  24. {draft_protocol-1.1.1 → draft_protocol-1.3.0}/Dockerfile +0 -0
  25. {draft_protocol-1.1.1 → draft_protocol-1.3.0}/INTEGRATIONS.md +0 -0
  26. {draft_protocol-1.1.1 → draft_protocol-1.3.0}/LICENSE +0 -0
  27. {draft_protocol-1.1.1 → draft_protocol-1.3.0}/METHODOLOGY.md +0 -0
  28. {draft_protocol-1.1.1 → draft_protocol-1.3.0}/Makefile +0 -0
  29. {draft_protocol-1.1.1 → draft_protocol-1.3.0}/README.md +0 -0
  30. {draft_protocol-1.1.1 → draft_protocol-1.3.0}/RELEASING.md +0 -0
  31. {draft_protocol-1.1.1 → draft_protocol-1.3.0}/ROADMAP.md +0 -0
  32. {draft_protocol-1.1.1 → draft_protocol-1.3.0}/RULES.md +0 -0
  33. {draft_protocol-1.1.1 → draft_protocol-1.3.0}/SECURITY.md +0 -0
  34. {draft_protocol-1.1.1 → draft_protocol-1.3.0}/STRUCTURE.md +0 -0
  35. {draft_protocol-1.1.1 → draft_protocol-1.3.0}/THREAT_MODEL.md +0 -0
  36. {draft_protocol-1.1.1 → draft_protocol-1.3.0}/docker-compose.example.yml +0 -0
  37. {draft_protocol-1.1.1 → draft_protocol-1.3.0}/docs/README.md +0 -0
  38. {draft_protocol-1.1.1 → draft_protocol-1.3.0}/docs/api.md +0 -0
  39. {draft_protocol-1.1.1 → draft_protocol-1.3.0}/docs/architecture.md +0 -0
  40. {draft_protocol-1.1.1 → draft_protocol-1.3.0}/examples/README.md +0 -0
  41. {draft_protocol-1.1.1 → draft_protocol-1.3.0}/examples/basic_usage.py +0 -0
  42. {draft_protocol-1.1.1 → draft_protocol-1.3.0}/extension/background.js +0 -0
  43. {draft_protocol-1.1.1 → draft_protocol-1.3.0}/extension/content.css +0 -0
  44. {draft_protocol-1.1.1 → draft_protocol-1.3.0}/extension/content.js +0 -0
  45. {draft_protocol-1.1.1 → draft_protocol-1.3.0}/extension/icons/icon128.png +0 -0
  46. {draft_protocol-1.1.1 → draft_protocol-1.3.0}/extension/icons/icon16.png +0 -0
  47. {draft_protocol-1.1.1 → draft_protocol-1.3.0}/extension/icons/icon48.png +0 -0
  48. {draft_protocol-1.1.1 → draft_protocol-1.3.0}/extension/manifest.json +0 -0
  49. {draft_protocol-1.1.1 → draft_protocol-1.3.0}/extension/popup.html +0 -0
  50. {draft_protocol-1.1.1 → draft_protocol-1.3.0}/extension/popup.js +0 -0
  51. {draft_protocol-1.1.1 → draft_protocol-1.3.0}/extension/sidepanel.html +0 -0
  52. {draft_protocol-1.1.1 → draft_protocol-1.3.0}/extension/sidepanel.js +0 -0
  53. {draft_protocol-1.1.1 → draft_protocol-1.3.0}/scripts/tmp/run_lint.ps1 +0 -0
  54. {draft_protocol-1.1.1 → draft_protocol-1.3.0}/scripts/tmp/run_new_tests.ps1 +0 -0
  55. {draft_protocol-1.1.1 → draft_protocol-1.3.0}/scripts/tmp/run_tests.ps1 +0 -0
  56. {draft_protocol-1.1.1 → draft_protocol-1.3.0}/src/draft_protocol/__main__.py +0 -0
  57. {draft_protocol-1.1.1 → draft_protocol-1.3.0}/src/draft_protocol/config.py +0 -0
  58. {draft_protocol-1.1.1 → draft_protocol-1.3.0}/src/draft_protocol/extension_points.py +0 -0
  59. {draft_protocol-1.1.1 → draft_protocol-1.3.0}/src/draft_protocol/providers.py +0 -0
  60. {draft_protocol-1.1.1 → draft_protocol-1.3.0}/src/draft_protocol/py.typed +0 -0
  61. {draft_protocol-1.1.1 → draft_protocol-1.3.0}/src/draft_protocol/rest.py +0 -0
  62. {draft_protocol-1.1.1 → draft_protocol-1.3.0}/src/draft_protocol/server.py +0 -0
  63. {draft_protocol-1.1.1 → draft_protocol-1.3.0}/tests/conftest.py +0 -0
  64. {draft_protocol-1.1.1 → draft_protocol-1.3.0}/tests/test_draft_protocol.py +0 -0
  65. {draft_protocol-1.1.1 → draft_protocol-1.3.0}/tests/test_rest.py +0 -0
  66. {draft_protocol-1.1.1 → draft_protocol-1.3.0}/tests/test_security.py +0 -0
  67. {draft_protocol-1.1.1 → draft_protocol-1.3.0}/tests/test_v1_1_features.py +0 -0
  68. {draft_protocol-1.1.1 → draft_protocol-1.3.0}/tests/test_v1_2_features.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: draft-protocol
3
- Version: 1.1.1
3
+ Version: 1.3.0
4
4
  Summary: DRAFT Protocol — Intake governance for AI tool calls. Ensures AI understands human intent before execution begins.
5
5
  Project-URL: Homepage, https://github.com/manifold-vectors/draft-protocol
6
6
  Project-URL: Documentation, https://github.com/manifold-vectors/draft-protocol#readme
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "draft-protocol"
7
- version = "1.1.1"
7
+ version = "1.3.0"
8
8
  description = "DRAFT Protocol — Intake governance for AI tool calls. Ensures AI understands human intent before execution begins."
9
9
  readme = "README.md"
10
10
  license = "Apache-2.0"
@@ -13,7 +13,7 @@ Usage:
13
13
  from draft_protocol.providers import llm_available, embed_available
14
14
  """
15
15
 
16
- __version__ = "1.1.1"
16
+ __version__ = "1.3.0"
17
17
 
18
18
  # Public API — importable from `draft_protocol` directly
19
19
  from draft_protocol.engine import (
@@ -36,6 +36,7 @@ from draft_protocol.config import (
36
36
  TRIVIAL_PATTERNS,
37
37
  )
38
38
  from draft_protocol.extension_points import get_classify_hook, get_post_gate_hook
39
+ from draft_protocol.hmac_utils import sign_assertion, sign_gate_pass
39
40
 
40
41
  # ── M1.3: Closed Session Guard ───────────────────────────
41
42
 
@@ -1026,7 +1027,8 @@ def check_gate(session_id: str) -> dict:
1026
1027
 
1027
1028
  passed = len(blockers) == 0
1028
1029
  if passed:
1029
- storage.update_session(session_id, gate_passed=1)
1030
+ gate_sig = sign_gate_pass(session_id)
1031
+ storage.update_session(session_id, gate_passed=1, gate_hmac=gate_sig)
1030
1032
 
1031
1033
  storage.log_audit(session_id, "draft_gate", "gate_check", f"{'PASS' if passed else 'FAIL'}: {confirmed}/{total}")
1032
1034
 
@@ -1038,6 +1040,15 @@ def check_gate(session_id: str) -> dict:
1038
1040
  "summary": f"{'[PASS]' if passed else '[BLOCKED]'}: {confirmed}/{total}",
1039
1041
  }
1040
1042
 
1043
+ # Phase A: emit signed assertion for cross-gate consumption
1044
+ if passed:
1045
+ result["assertion"] = sign_assertion("draft_gate_passed", {
1046
+ "session_id": session_id,
1047
+ "tier": session.get("tier", "STANDARD") if session else "STANDARD",
1048
+ "confirmed_fields": confirmed,
1049
+ "total_fields": total,
1050
+ })
1051
+
1041
1052
  if perfunctory_warnings:
1042
1053
  result["warnings"] = perfunctory_warnings
1043
1054
 
@@ -1197,11 +1208,23 @@ def override_gate(session_id: str, reason: str) -> dict:
1197
1208
  if gate.get("passed"):
1198
1209
  return {"note": "Already passed.", "gate": gate}
1199
1210
 
1200
- storage.update_session(session_id, gate_passed=1)
1211
+ gate_sig = sign_gate_pass(session_id)
1212
+ storage.update_session(session_id, gate_passed=1, gate_hmac=gate_sig)
1201
1213
  storage.log_audit(
1202
1214
  session_id, "override_gate", "OVERRIDDEN", f"AUTHORIZED: {reason.strip()}. Blockers: {gate.get('blockers', [])}"
1203
1215
  )
1204
- return {"status": "OVERRIDDEN", "reason": reason.strip(), "blockers": gate.get("blockers", [])}
1216
+ override_assertion = sign_assertion("draft_gate_passed", {
1217
+ "session_id": session_id,
1218
+ "tier": session.get("tier", "STANDARD"),
1219
+ "override": True,
1220
+ "reason": reason.strip(),
1221
+ })
1222
+ return {
1223
+ "status": "OVERRIDDEN",
1224
+ "reason": reason.strip(),
1225
+ "blockers": gate.get("blockers", []),
1226
+ "assertion": override_assertion,
1227
+ }
1205
1228
 
1206
1229
 
1207
1230
  def verify_assumption(session_id: str, index: int, verified: bool, note: str = "") -> dict:
@@ -0,0 +1,131 @@
1
+ """HMAC signing for inter-gate assertions.
2
+
3
+ Provides integrity guarantees for all cross-gate communication.
4
+ Each assertion is signed with HMAC-SHA256, includes a timestamp
5
+ and a monotonic nonce for replay protection.
6
+
7
+ Secret comes from GATE_HMAC_SECRET environment variable.
8
+ """
9
+
10
+ import hashlib
11
+ import hmac
12
+ import json
13
+ import os
14
+ import time
15
+ from typing import Any
16
+
17
+ # Monotonic nonce counter (per-process, resets on restart)
18
+ _nonce_counter: int = 0
19
+
20
+
21
+ def _get_secret() -> bytes:
22
+ """Get HMAC secret from environment. Falls back to a default for dev."""
23
+ secret = os.environ.get("GATE_HMAC_SECRET", "")
24
+ if not secret:
25
+ secret = "vector-gate-dev-secret-change-me"
26
+ return secret.encode()
27
+
28
+
29
+ def _next_nonce() -> int:
30
+ """Return next monotonic nonce."""
31
+ global _nonce_counter
32
+ _nonce_counter += 1
33
+ return _nonce_counter
34
+
35
+
36
+ def sign_assertion(assertion_type: str, payload: dict[str, Any]) -> dict:
37
+ """Sign an arbitrary inter-gate assertion.
38
+
39
+ Returns a complete signed assertion dict ready for transport:
40
+ {
41
+ "type": "draft_gate_passed",
42
+ "payload": {...},
43
+ "timestamp": "1773474000",
44
+ "nonce": 1,
45
+ "hmac": "hex..."
46
+ }
47
+ """
48
+ ts = str(int(time.time()))
49
+ nonce = _next_nonce()
50
+ # Canonical form: type|timestamp|nonce|sorted-json-payload
51
+ canonical = f"{assertion_type}|{ts}|{nonce}|{json.dumps(payload, sort_keys=True)}"
52
+ sig = hmac.new(_get_secret(), canonical.encode(), hashlib.sha256).hexdigest()
53
+ return {
54
+ "type": assertion_type,
55
+ "payload": payload,
56
+ "timestamp": ts,
57
+ "nonce": nonce,
58
+ "hmac": sig,
59
+ }
60
+
61
+
62
+ def verify_assertion(assertion: dict, max_age_seconds: int = 300) -> dict:
63
+ """Verify a signed assertion.
64
+
65
+ Checks HMAC integrity, timestamp freshness, and nonce monotonicity.
66
+
67
+ Returns:
68
+ {"valid": True, "type": "...", "payload": {...}}
69
+ {"valid": False, "reason": "..."}
70
+ """
71
+ required = {"type", "payload", "timestamp", "nonce", "hmac"}
72
+ if not isinstance(assertion, dict) or not required.issubset(assertion.keys()):
73
+ return {"valid": False, "reason": f"Missing fields: {required - set(assertion.keys())}"}
74
+
75
+ a_type = assertion["type"]
76
+ payload = assertion["payload"]
77
+ ts = assertion["timestamp"]
78
+ nonce = assertion["nonce"]
79
+ sig = assertion["hmac"]
80
+
81
+ # Reconstruct canonical form and verify HMAC
82
+ canonical = f"{a_type}|{ts}|{nonce}|{json.dumps(payload, sort_keys=True)}"
83
+ expected = hmac.new(_get_secret(), canonical.encode(), hashlib.sha256).hexdigest()
84
+ if not hmac.compare_digest(sig, expected):
85
+ return {"valid": False, "reason": "HMAC mismatch — possible tampering"}
86
+
87
+ # Timestamp freshness
88
+ try:
89
+ age = abs(int(time.time()) - int(ts))
90
+ if age > max_age_seconds:
91
+ return {"valid": False, "reason": f"Assertion stale ({age}s > {max_age_seconds}s)"}
92
+ except (ValueError, TypeError):
93
+ return {"valid": False, "reason": "Invalid timestamp"}
94
+
95
+ return {"valid": True, "type": a_type, "payload": payload}
96
+
97
+
98
+ # ── Legacy compat: gate_passed signing (wraps new generalized API) ──
99
+
100
+ def sign_gate_pass(session_id: str) -> str:
101
+ """Compute HMAC for a gate pass event (legacy format: ts:hex).
102
+
103
+ Kept for backward compatibility with existing cross_gate.py code.
104
+ New code should use sign_assertion() directly.
105
+ """
106
+ ts = str(int(time.time()))
107
+ payload = f"{session_id}|1|{ts}".encode()
108
+ sig = hmac.new(_get_secret(), payload, hashlib.sha256).hexdigest()
109
+ return f"{ts}:{sig}"
110
+
111
+
112
+ def verify_gate_pass(session_id: str, gate_hmac: str | None) -> bool:
113
+ """Verify legacy gate_passed HMAC (ts:hex format)."""
114
+ if not gate_hmac:
115
+ return False
116
+ parts = gate_hmac.split(":", 1)
117
+ if len(parts) != 2:
118
+ return False
119
+ ts, sig = parts
120
+ payload = f"{session_id}|1|{ts}".encode()
121
+ expected = hmac.new(_get_secret(), payload, hashlib.sha256).hexdigest()
122
+ return hmac.compare_digest(sig, expected)
123
+
124
+
125
+ def verify_or_warn(session_id: str, gate_hmac: str | None) -> dict:
126
+ """Verify legacy gate HMAC and return structured result."""
127
+ if gate_hmac is None:
128
+ return {"valid": False, "reason": "gate_hmac missing (legacy session or unsigned)"}
129
+ if verify_gate_pass(session_id, gate_hmac):
130
+ return {"valid": True}
131
+ return {"valid": False, "reason": "gate_hmac signature mismatch — possible tampering"}
@@ -32,6 +32,7 @@ def init_db():
32
32
  dimensions JSON NOT NULL DEFAULT '{}',
33
33
  assumptions JSON NOT NULL DEFAULT '[]',
34
34
  gate_passed INTEGER NOT NULL DEFAULT 0,
35
+ gate_hmac TEXT,
35
36
  review_done INTEGER NOT NULL DEFAULT 0,
36
37
  review_notes TEXT,
37
38
  created_at TEXT NOT NULL,
@@ -55,6 +56,20 @@ def init_db():
55
56
  init_db()
56
57
 
57
58
 
59
+ def _migrate_gate_hmac():
60
+ """Add gate_hmac column if missing (migration for existing DBs)."""
61
+ conn = get_db()
62
+ try:
63
+ conn.execute("SELECT gate_hmac FROM sessions LIMIT 1")
64
+ except sqlite3.OperationalError:
65
+ conn.execute("ALTER TABLE sessions ADD COLUMN gate_hmac TEXT")
66
+ conn.commit()
67
+ conn.close()
68
+
69
+
70
+ _migrate_gate_hmac()
71
+
72
+
58
73
  def _now() -> str:
59
74
  return datetime.now(timezone.utc).isoformat()
60
75
 
File without changes
File without changes
File without changes
File without changes
File without changes