vitnify 0.2.2__tar.gz → 0.2.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.
- {vitnify-0.2.2/src/vitnify.egg-info → vitnify-0.2.4}/PKG-INFO +1 -1
- {vitnify-0.2.2 → vitnify-0.2.4}/pyproject.toml +1 -1
- {vitnify-0.2.2 → vitnify-0.2.4}/src/vitnify/certificate.py +20 -5
- {vitnify-0.2.2 → vitnify-0.2.4/src/vitnify.egg-info}/PKG-INFO +1 -1
- {vitnify-0.2.2 → vitnify-0.2.4}/tests/test_verify_hardening.py +46 -0
- {vitnify-0.2.2 → vitnify-0.2.4}/LICENSE +0 -0
- {vitnify-0.2.2 → vitnify-0.2.4}/NOTICE +0 -0
- {vitnify-0.2.2 → vitnify-0.2.4}/README.md +0 -0
- {vitnify-0.2.2 → vitnify-0.2.4}/setup.cfg +0 -0
- {vitnify-0.2.2 → vitnify-0.2.4}/src/vitnify/__init__.py +0 -0
- {vitnify-0.2.2 → vitnify-0.2.4}/src/vitnify/_vendor/__init__.py +0 -0
- {vitnify-0.2.2 → vitnify-0.2.4}/src/vitnify/_vendor/pck/__init__.py +0 -0
- {vitnify-0.2.2 → vitnify-0.2.4}/src/vitnify/_vendor/pck/cas.py +0 -0
- {vitnify-0.2.2 → vitnify-0.2.4}/src/vitnify/capability.py +0 -0
- {vitnify-0.2.2 → vitnify-0.2.4}/src/vitnify/deterministic_llm.py +0 -0
- {vitnify-0.2.2 → vitnify-0.2.4}/src/vitnify/engine.py +0 -0
- {vitnify-0.2.2 → vitnify-0.2.4}/src/vitnify/events.py +0 -0
- {vitnify-0.2.2 → vitnify-0.2.4}/src/vitnify/langgraph_adapter.py +0 -0
- {vitnify-0.2.2 → vitnify-0.2.4}/src/vitnify/mcp_adapter.py +0 -0
- {vitnify-0.2.2 → vitnify-0.2.4}/src/vitnify/react.py +0 -0
- {vitnify-0.2.2 → vitnify-0.2.4}/src/vitnify/replayer.py +0 -0
- {vitnify-0.2.2 → vitnify-0.2.4}/src/vitnify/viewer.py +0 -0
- {vitnify-0.2.2 → vitnify-0.2.4}/src/vitnify.egg-info/SOURCES.txt +0 -0
- {vitnify-0.2.2 → vitnify-0.2.4}/src/vitnify.egg-info/dependency_links.txt +0 -0
- {vitnify-0.2.2 → vitnify-0.2.4}/src/vitnify.egg-info/requires.txt +0 -0
- {vitnify-0.2.2 → vitnify-0.2.4}/src/vitnify.egg-info/top_level.txt +0 -0
- {vitnify-0.2.2 → vitnify-0.2.4}/tests/test_adapters.py +0 -0
- {vitnify-0.2.2 → vitnify-0.2.4}/tests/test_attack_matrix.py +0 -0
- {vitnify-0.2.2 → vitnify-0.2.4}/tests/test_capability.py +0 -0
- {vitnify-0.2.2 → vitnify-0.2.4}/tests/test_certificate.py +0 -0
- {vitnify-0.2.2 → vitnify-0.2.4}/tests/test_ed25519.py +0 -0
- {vitnify-0.2.2 → vitnify-0.2.4}/tests/test_events.py +0 -0
- {vitnify-0.2.2 → vitnify-0.2.4}/tests/test_l2_recompute.py +0 -0
- {vitnify-0.2.2 → vitnify-0.2.4}/tests/test_receipt_v2.py +0 -0
- {vitnify-0.2.2 → vitnify-0.2.4}/tests/test_verification.py +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "vitnify"
|
|
7
|
-
version = "0.2.
|
|
7
|
+
version = "0.2.4"
|
|
8
8
|
description = "Execution receipts for AI agents: contain, deterministically reconstruct, and cryptographically certify a run."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -158,8 +158,15 @@ def verify_certificate(cert: ExecutionCertificate, log: EventLog, key: bytes | N
|
|
|
158
158
|
needs the model weights.)
|
|
159
159
|
"""
|
|
160
160
|
cas = MerkleCAS(log.chunks())
|
|
161
|
+
# Every event kind must be recognised. The semantic checks below filter events
|
|
162
|
+
# by an EXACT kind match, so an unknown kind ("TOOL_CALL", "toolcall", ...) would
|
|
163
|
+
# slip past them silently -- the same self-declared-label class as an unknown
|
|
164
|
+
# sig_alg or an unknown decision string. Fail closed here so the whole class is
|
|
165
|
+
# gone: an unrecognised kind (or decision, or algorithm, or format) never verifies.
|
|
166
|
+
known_kinds = {k.value for k in Kind}
|
|
161
167
|
checks = {
|
|
162
168
|
"format": cert.v in SUPPORTED_FORMATS,
|
|
169
|
+
"kinds_known": all(e.kind in known_kinds for e in log.events),
|
|
163
170
|
"root_matches": cas.root == cert.event_root,
|
|
164
171
|
"head_matches": log.head() == cert.head_hash,
|
|
165
172
|
"count_matches": len(log) == cert.n_events,
|
|
@@ -184,15 +191,23 @@ def verify_certificate(cert: ExecutionCertificate, log: EventLog, key: bytes | N
|
|
|
184
191
|
cert.sig, hmac.new(key, cert.digest().encode(), hashlib.blake2b).hexdigest())
|
|
185
192
|
checks["sig_valid"] = sig_valid
|
|
186
193
|
|
|
187
|
-
# Capability consistency -- the receipt must PROVE
|
|
188
|
-
# carry a capability list
|
|
189
|
-
#
|
|
194
|
+
# Capability consistency -- the receipt must PROVE no ungranted tool executed,
|
|
195
|
+
# not just carry a capability list. Every tool call must be within the declared,
|
|
196
|
+
# signed-over capability set OR a CLEAN DENIAL (decision exactly "deny",
|
|
197
|
+
# case/space-insensitive, with no result). Keying only off decision=="allow"
|
|
198
|
+
# let a forged log slip an ungranted, result-bearing call through by relabelling
|
|
199
|
+
# the decision ("PERMIT", " allow", omitted...) -- F11. Failing closed on
|
|
200
|
+
# anything that is not a clean denial removes that, whatever string it carries.
|
|
190
201
|
granted = set(cert.capabilities)
|
|
202
|
+
|
|
203
|
+
def _clean_denial(p: dict) -> bool:
|
|
204
|
+
return (str(p.get("decision", "")).strip().lower() == "deny"
|
|
205
|
+
and "result" not in p and "result_hash" not in p)
|
|
206
|
+
|
|
191
207
|
checks["caps_consistent"] = all(
|
|
192
|
-
e.payload.get("tool") in granted
|
|
208
|
+
e.payload.get("tool") in granted or _clean_denial(e.payload)
|
|
193
209
|
for e in log.events
|
|
194
210
|
if e.kind == Kind.TOOL_CALL.value
|
|
195
|
-
and str(e.payload.get("decision", "")).lower() == "allow"
|
|
196
211
|
)
|
|
197
212
|
|
|
198
213
|
# A verified receipt must carry NO data field its version does not sign. The v1
|
|
@@ -74,3 +74,49 @@ def test_f5_signer_pinning():
|
|
|
74
74
|
assert verify_certificate(cert, log, pinned_pubkeys=[pub])["ok"] is True
|
|
75
75
|
# unpinned verification (no allow-list) still succeeds on integrity + self-sig
|
|
76
76
|
assert verify_certificate(cert, log)["ok"] is True
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def test_f11_containment_not_evaded_by_decision_string():
|
|
80
|
+
# F11: an ungranted, result-bearing tool call must be rejected no matter what
|
|
81
|
+
# string its decision carries -- containment is proven, not keyed off "allow".
|
|
82
|
+
priv, _ = gen_ed25519()
|
|
83
|
+
for decision in ("PERMIT", " allow", "allowed", "Allow", None):
|
|
84
|
+
payload = {"tool": "wire_transfer", "result": "SENT"}
|
|
85
|
+
if decision is not None:
|
|
86
|
+
payload["decision"] = decision
|
|
87
|
+
log = EventLog(); log.append(Kind.TOOL_CALL, payload)
|
|
88
|
+
cert, _ = issue_certificate("prog", ["read_docs"], log, priv=priv) # wire_transfer ungranted
|
|
89
|
+
assert verify_certificate(cert, log)["ok"] is False, repr(decision)
|
|
90
|
+
# an ungranted tool mislabelled DENY but still carrying a result is also rejected
|
|
91
|
+
log = EventLog(); log.append(Kind.TOOL_CALL, {"tool": "wire_transfer", "decision": "DENY", "result": "SENT"})
|
|
92
|
+
cert, _ = issue_certificate("prog", ["read_docs"], log, priv=priv)
|
|
93
|
+
assert verify_certificate(cert, log)["ok"] is False
|
|
94
|
+
# ...but a clean denial of an ungranted tool still verifies (containment working)
|
|
95
|
+
log = EventLog(); log.append(Kind.TOOL_CALL, {"tool": "send_email", "decision": "DENY"})
|
|
96
|
+
cert, _ = issue_certificate("prog", ["read_docs"], log, priv=priv)
|
|
97
|
+
assert verify_certificate(cert, log)["ok"] is True
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def test_f12_unknown_event_kind_is_rejected():
|
|
101
|
+
# F12: the semantic checks filter events by an exact kind match, so an unknown
|
|
102
|
+
# kind would slip past them. Fail closed -- any unrecognised kind invalidates the
|
|
103
|
+
# receipt. This closes the whole "self-declared label evades a filter" class:
|
|
104
|
+
# F1 (sig_alg), F11 (decision), F12 (kind) all now fail closed on unknown values.
|
|
105
|
+
priv, _ = gen_ed25519()
|
|
106
|
+
# an ungranted tool hidden under an unrecognised kind must not escape containment
|
|
107
|
+
log = EventLog(); log.append("TOOL_CALL", {"tool": "wire_transfer", "decision": "ALLOW", "result": "x"})
|
|
108
|
+
cert, _ = issue_certificate("prog", ["read_docs"], log, priv=priv)
|
|
109
|
+
r = verify_certificate(cert, log)
|
|
110
|
+
assert r["kinds_known"] is False and r["ok"] is False
|
|
111
|
+
# a model step hidden under an unrecognised kind must not verify either
|
|
112
|
+
log = EventLog(); log.append("LLM_CALL", {"prompt_hash": "p", "tokens": [1], "seed": 0, "model_digest": "d"})
|
|
113
|
+
cert, _ = issue_certificate("prog", [], log, priv=priv)
|
|
114
|
+
assert verify_certificate(cert, log)["ok"] is False
|
|
115
|
+
# every recognised kind still verifies
|
|
116
|
+
log = EventLog()
|
|
117
|
+
log.append(Kind.LLM_CALL, {"prompt_hash": "p", "tokens": [1], "seed": 0, "model_digest": "d"})
|
|
118
|
+
log.append(Kind.TOOL_CALL, {"tool": "read_docs", "decision": "ALLOW", "result": "r", "result_hash": "h"})
|
|
119
|
+
log.append(Kind.ENTROPY, {"source": "clock", "value": 1})
|
|
120
|
+
log.append(Kind.AGENT_STEP, {"state": "done"})
|
|
121
|
+
cert, _ = issue_certificate("prog", ["read_docs"], log, priv=priv)
|
|
122
|
+
assert verify_certificate(cert, log)["ok"] is True
|
|
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
|