vitnify 0.2.4__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.4/src/vitnify.egg-info → vitnify-0.2.5}/PKG-INFO +1 -1
- {vitnify-0.2.4 → vitnify-0.2.5}/pyproject.toml +1 -1
- {vitnify-0.2.4 → vitnify-0.2.5}/src/vitnify/certificate.py +31 -13
- {vitnify-0.2.4 → vitnify-0.2.5}/src/vitnify/events.py +8 -8
- {vitnify-0.2.4 → vitnify-0.2.5/src/vitnify.egg-info}/PKG-INFO +1 -1
- {vitnify-0.2.4 → vitnify-0.2.5}/tests/test_verify_hardening.py +20 -0
- {vitnify-0.2.4 → vitnify-0.2.5}/LICENSE +0 -0
- {vitnify-0.2.4 → vitnify-0.2.5}/NOTICE +0 -0
- {vitnify-0.2.4 → vitnify-0.2.5}/README.md +0 -0
- {vitnify-0.2.4 → vitnify-0.2.5}/setup.cfg +0 -0
- {vitnify-0.2.4 → vitnify-0.2.5}/src/vitnify/__init__.py +0 -0
- {vitnify-0.2.4 → vitnify-0.2.5}/src/vitnify/_vendor/__init__.py +0 -0
- {vitnify-0.2.4 → vitnify-0.2.5}/src/vitnify/_vendor/pck/__init__.py +0 -0
- {vitnify-0.2.4 → vitnify-0.2.5}/src/vitnify/_vendor/pck/cas.py +0 -0
- {vitnify-0.2.4 → vitnify-0.2.5}/src/vitnify/capability.py +0 -0
- {vitnify-0.2.4 → vitnify-0.2.5}/src/vitnify/deterministic_llm.py +0 -0
- {vitnify-0.2.4 → vitnify-0.2.5}/src/vitnify/engine.py +0 -0
- {vitnify-0.2.4 → vitnify-0.2.5}/src/vitnify/langgraph_adapter.py +0 -0
- {vitnify-0.2.4 → vitnify-0.2.5}/src/vitnify/mcp_adapter.py +0 -0
- {vitnify-0.2.4 → vitnify-0.2.5}/src/vitnify/react.py +0 -0
- {vitnify-0.2.4 → vitnify-0.2.5}/src/vitnify/replayer.py +0 -0
- {vitnify-0.2.4 → vitnify-0.2.5}/src/vitnify/viewer.py +0 -0
- {vitnify-0.2.4 → vitnify-0.2.5}/src/vitnify.egg-info/SOURCES.txt +0 -0
- {vitnify-0.2.4 → vitnify-0.2.5}/src/vitnify.egg-info/dependency_links.txt +0 -0
- {vitnify-0.2.4 → vitnify-0.2.5}/src/vitnify.egg-info/requires.txt +0 -0
- {vitnify-0.2.4 → vitnify-0.2.5}/src/vitnify.egg-info/top_level.txt +0 -0
- {vitnify-0.2.4 → vitnify-0.2.5}/tests/test_adapters.py +0 -0
- {vitnify-0.2.4 → vitnify-0.2.5}/tests/test_attack_matrix.py +0 -0
- {vitnify-0.2.4 → vitnify-0.2.5}/tests/test_capability.py +0 -0
- {vitnify-0.2.4 → vitnify-0.2.5}/tests/test_certificate.py +0 -0
- {vitnify-0.2.4 → vitnify-0.2.5}/tests/test_ed25519.py +0 -0
- {vitnify-0.2.4 → vitnify-0.2.5}/tests/test_events.py +0 -0
- {vitnify-0.2.4 → vitnify-0.2.5}/tests/test_l2_recompute.py +0 -0
- {vitnify-0.2.4 → vitnify-0.2.5}/tests/test_receipt_v2.py +0 -0
- {vitnify-0.2.4 → 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())
|
|
@@ -224,5 +230,17 @@ def verify_certificate(cert: ExecutionCertificate, log: EventLog, key: bytes | N
|
|
|
224
230
|
checks["signer_pinned"] = (
|
|
225
231
|
sig_valid and cert.sig_alg == "ed25519" and cert.pubkey in set(pinned_pubkeys))
|
|
226
232
|
|
|
227
|
-
|
|
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")
|
|
228
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:
|
|
@@ -120,3 +120,23 @@ def test_f12_unknown_event_kind_is_rejected():
|
|
|
120
120
|
log.append(Kind.AGENT_STEP, {"state": "done"})
|
|
121
121
|
cert, _ = issue_certificate("prog", ["read_docs"], log, priv=priv)
|
|
122
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
|