vitnify 0.2.3__tar.gz → 0.2.5__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.3/src/vitnify.egg-info → vitnify-0.2.5}/PKG-INFO +1 -1
- {vitnify-0.2.3 → vitnify-0.2.5}/pyproject.toml +1 -1
- {vitnify-0.2.3 → vitnify-0.2.5}/src/vitnify/certificate.py +38 -13
- {vitnify-0.2.3 → vitnify-0.2.5}/src/vitnify/events.py +8 -8
- {vitnify-0.2.3 → vitnify-0.2.5/src/vitnify.egg-info}/PKG-INFO +1 -1
- {vitnify-0.2.3 → vitnify-0.2.5}/tests/test_verify_hardening.py +45 -0
- {vitnify-0.2.3 → vitnify-0.2.5}/LICENSE +0 -0
- {vitnify-0.2.3 → vitnify-0.2.5}/NOTICE +0 -0
- {vitnify-0.2.3 → vitnify-0.2.5}/README.md +0 -0
- {vitnify-0.2.3 → vitnify-0.2.5}/setup.cfg +0 -0
- {vitnify-0.2.3 → vitnify-0.2.5}/src/vitnify/__init__.py +0 -0
- {vitnify-0.2.3 → vitnify-0.2.5}/src/vitnify/_vendor/__init__.py +0 -0
- {vitnify-0.2.3 → vitnify-0.2.5}/src/vitnify/_vendor/pck/__init__.py +0 -0
- {vitnify-0.2.3 → vitnify-0.2.5}/src/vitnify/_vendor/pck/cas.py +0 -0
- {vitnify-0.2.3 → vitnify-0.2.5}/src/vitnify/capability.py +0 -0
- {vitnify-0.2.3 → vitnify-0.2.5}/src/vitnify/deterministic_llm.py +0 -0
- {vitnify-0.2.3 → vitnify-0.2.5}/src/vitnify/engine.py +0 -0
- {vitnify-0.2.3 → vitnify-0.2.5}/src/vitnify/langgraph_adapter.py +0 -0
- {vitnify-0.2.3 → vitnify-0.2.5}/src/vitnify/mcp_adapter.py +0 -0
- {vitnify-0.2.3 → vitnify-0.2.5}/src/vitnify/react.py +0 -0
- {vitnify-0.2.3 → vitnify-0.2.5}/src/vitnify/replayer.py +0 -0
- {vitnify-0.2.3 → vitnify-0.2.5}/src/vitnify/viewer.py +0 -0
- {vitnify-0.2.3 → vitnify-0.2.5}/src/vitnify.egg-info/SOURCES.txt +0 -0
- {vitnify-0.2.3 → vitnify-0.2.5}/src/vitnify.egg-info/dependency_links.txt +0 -0
- {vitnify-0.2.3 → vitnify-0.2.5}/src/vitnify.egg-info/requires.txt +0 -0
- {vitnify-0.2.3 → vitnify-0.2.5}/src/vitnify.egg-info/top_level.txt +0 -0
- {vitnify-0.2.3 → vitnify-0.2.5}/tests/test_adapters.py +0 -0
- {vitnify-0.2.3 → vitnify-0.2.5}/tests/test_attack_matrix.py +0 -0
- {vitnify-0.2.3 → vitnify-0.2.5}/tests/test_capability.py +0 -0
- {vitnify-0.2.3 → vitnify-0.2.5}/tests/test_certificate.py +0 -0
- {vitnify-0.2.3 → vitnify-0.2.5}/tests/test_ed25519.py +0 -0
- {vitnify-0.2.3 → vitnify-0.2.5}/tests/test_events.py +0 -0
- {vitnify-0.2.3 → vitnify-0.2.5}/tests/test_l2_recompute.py +0 -0
- {vitnify-0.2.3 → vitnify-0.2.5}/tests/test_receipt_v2.py +0 -0
- {vitnify-0.2.3 → vitnify-0.2.5}/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.5"
|
|
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"
|
|
@@ -41,14 +41,12 @@ def _now_iso() -> str:
|
|
|
41
41
|
from datetime import datetime, timezone
|
|
42
42
|
return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
43
43
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
def _digest32(b: bytes) -> str:
|
|
51
|
-
return hashlib.blake2b(b, digest_size=32).hexdigest()
|
|
44
|
+
# BLAKE3 is a hard dependency and DEFINES the wire format: a blake2b digest is a
|
|
45
|
+
# different value, so there is no silent stdlib fallback (see events.py). A missing
|
|
46
|
+
# blake3 fails loudly rather than producing incompatible digests.
|
|
47
|
+
import blake3 as _blake3
|
|
48
|
+
def _digest32(b: bytes) -> str:
|
|
49
|
+
return _blake3.blake3(b).hexdigest()
|
|
52
50
|
|
|
53
51
|
# cryptography is imported lazily inside the ed25519 helpers so the fallback path
|
|
54
52
|
# still runs on a stock Python without the dependency.
|
|
@@ -121,10 +119,18 @@ def issue_certificate(program_hash, capabilities, log: EventLog, priv=None, key:
|
|
|
121
119
|
"""Build and sign a vitnify-receipt for a completed run.
|
|
122
120
|
|
|
123
121
|
The model-computation digests are read from the log's llm_call events, so the
|
|
124
|
-
receipt binds exactly the model steps that were recorded.
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
122
|
+
receipt binds exactly the model steps that were recorded. `model_digests` is
|
|
123
|
+
EMPTY for a run with no recomputable model step (a hosted API, or a torch /
|
|
124
|
+
non-deterministic backend) -- such a receipt is integrity-only: it binds the
|
|
125
|
+
transcript, not the computation.
|
|
126
|
+
|
|
127
|
+
`program_hash` is CALLER-ASSERTED -- the receipt binds whatever string you pass;
|
|
128
|
+
it does not itself hash your agent's code. Pass a hash of the exact program or
|
|
129
|
+
config that ran if you want the receipt to pin it.
|
|
130
|
+
|
|
131
|
+
Each receipt is stamped with an issuer-asserted UTC time, a random nonce, and a
|
|
132
|
+
run id (pass `run_id` to set your own; a random one is used otherwise) so distinct
|
|
133
|
+
runs produce distinct, time-placeable receipts. ed25519 (self-verifying) is the
|
|
128
134
|
canonical signing path; pass `priv`.
|
|
129
135
|
"""
|
|
130
136
|
cas = MerkleCAS(log.chunks())
|
|
@@ -158,8 +164,15 @@ def verify_certificate(cert: ExecutionCertificate, log: EventLog, key: bytes | N
|
|
|
158
164
|
needs the model weights.)
|
|
159
165
|
"""
|
|
160
166
|
cas = MerkleCAS(log.chunks())
|
|
167
|
+
# Every event kind must be recognised. The semantic checks below filter events
|
|
168
|
+
# by an EXACT kind match, so an unknown kind ("TOOL_CALL", "toolcall", ...) would
|
|
169
|
+
# slip past them silently -- the same self-declared-label class as an unknown
|
|
170
|
+
# sig_alg or an unknown decision string. Fail closed here so the whole class is
|
|
171
|
+
# gone: an unrecognised kind (or decision, or algorithm, or format) never verifies.
|
|
172
|
+
known_kinds = {k.value for k in Kind}
|
|
161
173
|
checks = {
|
|
162
174
|
"format": cert.v in SUPPORTED_FORMATS,
|
|
175
|
+
"kinds_known": all(e.kind in known_kinds for e in log.events),
|
|
163
176
|
"root_matches": cas.root == cert.event_root,
|
|
164
177
|
"head_matches": log.head() == cert.head_hash,
|
|
165
178
|
"count_matches": len(log) == cert.n_events,
|
|
@@ -217,5 +230,17 @@ def verify_certificate(cert: ExecutionCertificate, log: EventLog, key: bytes | N
|
|
|
217
230
|
checks["signer_pinned"] = (
|
|
218
231
|
sig_valid and cert.sig_alg == "ed25519" and cert.pubkey in set(pinned_pubkeys))
|
|
219
232
|
|
|
220
|
-
|
|
233
|
+
# Enforcement vs observation. An "allow"/"deny" decision came from the capability
|
|
234
|
+
# wall -- the call was GATED. Any other value (e.g. "observed" from the record-only
|
|
235
|
+
# callback adapter) means it was WATCHED, not enforced, so the receipt is a
|
|
236
|
+
# tamper-evident transcript, not proof containment was applied -- the containment
|
|
237
|
+
# analogue of an empty model_digests. Reported separately, NOT folded into ok, so a
|
|
238
|
+
# valid transcript stays valid while a containment PROOF requires ok AND this True.
|
|
239
|
+
checks["containment_enforced"] = all(
|
|
240
|
+
str(e.payload.get("decision", "")).strip().lower() in ("allow", "deny")
|
|
241
|
+
for e in log.events
|
|
242
|
+
if e.kind == Kind.TOOL_CALL.value
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
checks["ok"] = all(v for k, v in checks.items() if k != "containment_enforced")
|
|
221
246
|
return checks
|
|
@@ -13,14 +13,14 @@ import json
|
|
|
13
13
|
from dataclasses import dataclass, asdict
|
|
14
14
|
from enum import Enum
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
16
|
+
# BLAKE3 is a hard dependency and DEFINES the wire format: a blake2b digest is a
|
|
17
|
+
# different value, so there is no silent stdlib fallback -- one would make honest
|
|
18
|
+
# receipts indistinguishable from forgeries across implementations (and the receipt
|
|
19
|
+
# carries no hash-suite identifier to tell them apart). A normal `pip install
|
|
20
|
+
# vitnify` always resolves the wheel; a missing blake3 fails loudly, not silently.
|
|
21
|
+
import blake3 as _blake3
|
|
22
|
+
def _h32(b: bytes) -> str:
|
|
23
|
+
return _blake3.blake3(b).hexdigest()
|
|
24
24
|
|
|
25
25
|
|
|
26
26
|
def canon(obj) -> str:
|
|
@@ -95,3 +95,48 @@ def test_f11_containment_not_evaded_by_decision_string():
|
|
|
95
95
|
log = EventLog(); log.append(Kind.TOOL_CALL, {"tool": "send_email", "decision": "DENY"})
|
|
96
96
|
cert, _ = issue_certificate("prog", ["read_docs"], log, priv=priv)
|
|
97
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
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def test_observe_only_receipt_is_flagged_not_masqueraded():
|
|
126
|
+
# A record-only adapter records tool calls with decision "observed" -- watched,
|
|
127
|
+
# not gated. Such a receipt is a valid transcript (ok=True) but must be FLAGGED
|
|
128
|
+
# containment_enforced=False so it can't pass as a containment proof; an enforced
|
|
129
|
+
# allow/deny receipt reads containment_enforced=True.
|
|
130
|
+
priv, _ = gen_ed25519()
|
|
131
|
+
|
|
132
|
+
def receipt(decision, caps):
|
|
133
|
+
log = EventLog()
|
|
134
|
+
log.append(Kind.TOOL_CALL, {"tool": "send_email", "args": ["x"], "decision": decision, "result": "sent"})
|
|
135
|
+
cert, _ = issue_certificate("prog", caps, log, priv=priv)
|
|
136
|
+
return verify_certificate(cert, log)
|
|
137
|
+
|
|
138
|
+
r = receipt("OBSERVED", ["read_docs", "send_email"]) # observed + granted
|
|
139
|
+
assert r["ok"] is True and r["containment_enforced"] is False
|
|
140
|
+
assert receipt("OBSERVED", ["read_docs"])["ok"] is False # observed ungranted tool ran -> rejected
|
|
141
|
+
r = receipt("ALLOW", ["read_docs", "send_email"]) # enforced allow -> real proof
|
|
142
|
+
assert r["ok"] is True and r["containment_enforced"] 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
|