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.
- {draft_protocol-1.1.1 → draft_protocol-1.3.0}/PKG-INFO +1 -1
- {draft_protocol-1.1.1 → draft_protocol-1.3.0}/pyproject.toml +1 -1
- {draft_protocol-1.1.1 → draft_protocol-1.3.0}/src/draft_protocol/__init__.py +1 -1
- {draft_protocol-1.1.1 → draft_protocol-1.3.0}/src/draft_protocol/engine.py +26 -3
- draft_protocol-1.3.0/src/draft_protocol/hmac_utils.py +131 -0
- {draft_protocol-1.1.1 → draft_protocol-1.3.0}/src/draft_protocol/storage.py +15 -0
- {draft_protocol-1.1.1 → draft_protocol-1.3.0}/.dockerignore +0 -0
- {draft_protocol-1.1.1 → draft_protocol-1.3.0}/.editorconfig +0 -0
- {draft_protocol-1.1.1 → draft_protocol-1.3.0}/.github/CODEOWNERS +0 -0
- {draft_protocol-1.1.1 → draft_protocol-1.3.0}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
- {draft_protocol-1.1.1 → draft_protocol-1.3.0}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
- {draft_protocol-1.1.1 → draft_protocol-1.3.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {draft_protocol-1.1.1 → draft_protocol-1.3.0}/.github/dependabot.yml +0 -0
- {draft_protocol-1.1.1 → draft_protocol-1.3.0}/.github/workflows/ci.yml +0 -0
- {draft_protocol-1.1.1 → draft_protocol-1.3.0}/.github/workflows/release.yml +0 -0
- {draft_protocol-1.1.1 → draft_protocol-1.3.0}/.gitignore +0 -0
- {draft_protocol-1.1.1 → draft_protocol-1.3.0}/.pre-commit-config.yaml +0 -0
- {draft_protocol-1.1.1 → draft_protocol-1.3.0}/AGENTS.md +0 -0
- {draft_protocol-1.1.1 → draft_protocol-1.3.0}/BENCHMARKS.md +0 -0
- {draft_protocol-1.1.1 → draft_protocol-1.3.0}/CHANGELOG.md +0 -0
- {draft_protocol-1.1.1 → draft_protocol-1.3.0}/CODE_OF_CONDUCT.md +0 -0
- {draft_protocol-1.1.1 → draft_protocol-1.3.0}/CONFORMANCE.md +0 -0
- {draft_protocol-1.1.1 → draft_protocol-1.3.0}/CONTRIBUTING.md +0 -0
- {draft_protocol-1.1.1 → draft_protocol-1.3.0}/Dockerfile +0 -0
- {draft_protocol-1.1.1 → draft_protocol-1.3.0}/INTEGRATIONS.md +0 -0
- {draft_protocol-1.1.1 → draft_protocol-1.3.0}/LICENSE +0 -0
- {draft_protocol-1.1.1 → draft_protocol-1.3.0}/METHODOLOGY.md +0 -0
- {draft_protocol-1.1.1 → draft_protocol-1.3.0}/Makefile +0 -0
- {draft_protocol-1.1.1 → draft_protocol-1.3.0}/README.md +0 -0
- {draft_protocol-1.1.1 → draft_protocol-1.3.0}/RELEASING.md +0 -0
- {draft_protocol-1.1.1 → draft_protocol-1.3.0}/ROADMAP.md +0 -0
- {draft_protocol-1.1.1 → draft_protocol-1.3.0}/RULES.md +0 -0
- {draft_protocol-1.1.1 → draft_protocol-1.3.0}/SECURITY.md +0 -0
- {draft_protocol-1.1.1 → draft_protocol-1.3.0}/STRUCTURE.md +0 -0
- {draft_protocol-1.1.1 → draft_protocol-1.3.0}/THREAT_MODEL.md +0 -0
- {draft_protocol-1.1.1 → draft_protocol-1.3.0}/docker-compose.example.yml +0 -0
- {draft_protocol-1.1.1 → draft_protocol-1.3.0}/docs/README.md +0 -0
- {draft_protocol-1.1.1 → draft_protocol-1.3.0}/docs/api.md +0 -0
- {draft_protocol-1.1.1 → draft_protocol-1.3.0}/docs/architecture.md +0 -0
- {draft_protocol-1.1.1 → draft_protocol-1.3.0}/examples/README.md +0 -0
- {draft_protocol-1.1.1 → draft_protocol-1.3.0}/examples/basic_usage.py +0 -0
- {draft_protocol-1.1.1 → draft_protocol-1.3.0}/extension/background.js +0 -0
- {draft_protocol-1.1.1 → draft_protocol-1.3.0}/extension/content.css +0 -0
- {draft_protocol-1.1.1 → draft_protocol-1.3.0}/extension/content.js +0 -0
- {draft_protocol-1.1.1 → draft_protocol-1.3.0}/extension/icons/icon128.png +0 -0
- {draft_protocol-1.1.1 → draft_protocol-1.3.0}/extension/icons/icon16.png +0 -0
- {draft_protocol-1.1.1 → draft_protocol-1.3.0}/extension/icons/icon48.png +0 -0
- {draft_protocol-1.1.1 → draft_protocol-1.3.0}/extension/manifest.json +0 -0
- {draft_protocol-1.1.1 → draft_protocol-1.3.0}/extension/popup.html +0 -0
- {draft_protocol-1.1.1 → draft_protocol-1.3.0}/extension/popup.js +0 -0
- {draft_protocol-1.1.1 → draft_protocol-1.3.0}/extension/sidepanel.html +0 -0
- {draft_protocol-1.1.1 → draft_protocol-1.3.0}/extension/sidepanel.js +0 -0
- {draft_protocol-1.1.1 → draft_protocol-1.3.0}/scripts/tmp/run_lint.ps1 +0 -0
- {draft_protocol-1.1.1 → draft_protocol-1.3.0}/scripts/tmp/run_new_tests.ps1 +0 -0
- {draft_protocol-1.1.1 → draft_protocol-1.3.0}/scripts/tmp/run_tests.ps1 +0 -0
- {draft_protocol-1.1.1 → draft_protocol-1.3.0}/src/draft_protocol/__main__.py +0 -0
- {draft_protocol-1.1.1 → draft_protocol-1.3.0}/src/draft_protocol/config.py +0 -0
- {draft_protocol-1.1.1 → draft_protocol-1.3.0}/src/draft_protocol/extension_points.py +0 -0
- {draft_protocol-1.1.1 → draft_protocol-1.3.0}/src/draft_protocol/providers.py +0 -0
- {draft_protocol-1.1.1 → draft_protocol-1.3.0}/src/draft_protocol/py.typed +0 -0
- {draft_protocol-1.1.1 → draft_protocol-1.3.0}/src/draft_protocol/rest.py +0 -0
- {draft_protocol-1.1.1 → draft_protocol-1.3.0}/src/draft_protocol/server.py +0 -0
- {draft_protocol-1.1.1 → draft_protocol-1.3.0}/tests/conftest.py +0 -0
- {draft_protocol-1.1.1 → draft_protocol-1.3.0}/tests/test_draft_protocol.py +0 -0
- {draft_protocol-1.1.1 → draft_protocol-1.3.0}/tests/test_rest.py +0 -0
- {draft_protocol-1.1.1 → draft_protocol-1.3.0}/tests/test_security.py +0 -0
- {draft_protocol-1.1.1 → draft_protocol-1.3.0}/tests/test_v1_1_features.py +0 -0
- {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.
|
|
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.
|
|
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"
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|