vitnify 0.2.0__tar.gz → 0.2.2__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 (36) hide show
  1. {vitnify-0.2.0/src/vitnify.egg-info → vitnify-0.2.2}/PKG-INFO +1 -1
  2. {vitnify-0.2.0 → vitnify-0.2.2}/pyproject.toml +1 -1
  3. {vitnify-0.2.0 → vitnify-0.2.2}/src/vitnify/certificate.py +27 -9
  4. {vitnify-0.2.0 → vitnify-0.2.2}/src/vitnify/replayer.py +17 -6
  5. {vitnify-0.2.0 → vitnify-0.2.2/src/vitnify.egg-info}/PKG-INFO +1 -1
  6. vitnify-0.2.2/tests/test_receipt_v2.py +132 -0
  7. vitnify-0.2.0/tests/test_receipt_v2.py +0 -68
  8. {vitnify-0.2.0 → vitnify-0.2.2}/LICENSE +0 -0
  9. {vitnify-0.2.0 → vitnify-0.2.2}/NOTICE +0 -0
  10. {vitnify-0.2.0 → vitnify-0.2.2}/README.md +0 -0
  11. {vitnify-0.2.0 → vitnify-0.2.2}/setup.cfg +0 -0
  12. {vitnify-0.2.0 → vitnify-0.2.2}/src/vitnify/__init__.py +0 -0
  13. {vitnify-0.2.0 → vitnify-0.2.2}/src/vitnify/_vendor/__init__.py +0 -0
  14. {vitnify-0.2.0 → vitnify-0.2.2}/src/vitnify/_vendor/pck/__init__.py +0 -0
  15. {vitnify-0.2.0 → vitnify-0.2.2}/src/vitnify/_vendor/pck/cas.py +0 -0
  16. {vitnify-0.2.0 → vitnify-0.2.2}/src/vitnify/capability.py +0 -0
  17. {vitnify-0.2.0 → vitnify-0.2.2}/src/vitnify/deterministic_llm.py +0 -0
  18. {vitnify-0.2.0 → vitnify-0.2.2}/src/vitnify/engine.py +0 -0
  19. {vitnify-0.2.0 → vitnify-0.2.2}/src/vitnify/events.py +0 -0
  20. {vitnify-0.2.0 → vitnify-0.2.2}/src/vitnify/langgraph_adapter.py +0 -0
  21. {vitnify-0.2.0 → vitnify-0.2.2}/src/vitnify/mcp_adapter.py +0 -0
  22. {vitnify-0.2.0 → vitnify-0.2.2}/src/vitnify/react.py +0 -0
  23. {vitnify-0.2.0 → vitnify-0.2.2}/src/vitnify/viewer.py +0 -0
  24. {vitnify-0.2.0 → vitnify-0.2.2}/src/vitnify.egg-info/SOURCES.txt +0 -0
  25. {vitnify-0.2.0 → vitnify-0.2.2}/src/vitnify.egg-info/dependency_links.txt +0 -0
  26. {vitnify-0.2.0 → vitnify-0.2.2}/src/vitnify.egg-info/requires.txt +0 -0
  27. {vitnify-0.2.0 → vitnify-0.2.2}/src/vitnify.egg-info/top_level.txt +0 -0
  28. {vitnify-0.2.0 → vitnify-0.2.2}/tests/test_adapters.py +0 -0
  29. {vitnify-0.2.0 → vitnify-0.2.2}/tests/test_attack_matrix.py +0 -0
  30. {vitnify-0.2.0 → vitnify-0.2.2}/tests/test_capability.py +0 -0
  31. {vitnify-0.2.0 → vitnify-0.2.2}/tests/test_certificate.py +0 -0
  32. {vitnify-0.2.0 → vitnify-0.2.2}/tests/test_ed25519.py +0 -0
  33. {vitnify-0.2.0 → vitnify-0.2.2}/tests/test_events.py +0 -0
  34. {vitnify-0.2.0 → vitnify-0.2.2}/tests/test_l2_recompute.py +0 -0
  35. {vitnify-0.2.0 → vitnify-0.2.2}/tests/test_verification.py +0 -0
  36. {vitnify-0.2.0 → vitnify-0.2.2}/tests/test_verify_hardening.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: vitnify
3
- Version: 0.2.0
3
+ Version: 0.2.2
4
4
  Summary: Execution receipts for AI agents: contain, deterministically reconstruct, and cryptographically certify a run.
5
5
  Author: vitnify
6
6
  License: Apache-2.0
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "vitnify"
7
- version = "0.2.0"
7
+ version = "0.2.2"
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"
@@ -27,6 +27,10 @@ from .events import EventLog, Kind
27
27
  from ._vendor.pck.cas import MerkleCAS # noqa: E402
28
28
 
29
29
  FORMAT = "vitnify-receipt v2"
30
+ # A published receipt format is frozen; older receipts must stay verifiable across a
31
+ # verifier upgrade. The verifier accepts every version it has ever issued and
32
+ # reconstructs each receipt's signed body per its own `v` (see body()).
33
+ SUPPORTED_FORMATS = frozenset({"vitnify-receipt v1", FORMAT})
30
34
 
31
35
 
32
36
  def _now_iso() -> str:
@@ -78,14 +82,20 @@ class ExecutionCertificate:
78
82
  pubkey: str | None = None # ed25519 public key (hex) -- verifier needs no secret
79
83
 
80
84
  def body(self) -> dict:
81
- # Everything here is signed. issued_at/nonce/run_id place the receipt in
82
- # time and make it unique, so a receipt from one run cannot be presented as
83
- # evidence for another.
84
- return {"v": self.v, "program_hash": self.program_hash,
85
- "capabilities": sorted(self.capabilities), "event_root": self.event_root,
86
- "n_events": self.n_events, "head_hash": self.head_hash,
87
- "model_digests": list(self.model_digests),
88
- "issued_at": self.issued_at, "nonce": self.nonce, "run_id": self.run_id}
85
+ # Everything here is signed, and the body is VERSIONED: v2+ additionally
86
+ # binds issued_at/nonce/run_id (which place the receipt in time and make it
87
+ # unique). Reconstructing the body per the receipt's OWN `v` is what lets a
88
+ # v2 verifier still verify a receipt signed under v1 -- a published format is
89
+ # frozen, so older receipts must remain verifiable.
90
+ b = {"v": self.v, "program_hash": self.program_hash,
91
+ "capabilities": sorted(self.capabilities), "event_root": self.event_root,
92
+ "n_events": self.n_events, "head_hash": self.head_hash,
93
+ "model_digests": list(self.model_digests)}
94
+ if self.v != "vitnify-receipt v1":
95
+ b["issued_at"] = self.issued_at
96
+ b["nonce"] = self.nonce
97
+ b["run_id"] = self.run_id
98
+ return b
89
99
 
90
100
  def digest(self) -> str:
91
101
  return _digest32(_canon(self.body()).encode())
@@ -149,7 +159,7 @@ def verify_certificate(cert: ExecutionCertificate, log: EventLog, key: bytes | N
149
159
  """
150
160
  cas = MerkleCAS(log.chunks())
151
161
  checks = {
152
- "format": cert.v == FORMAT,
162
+ "format": cert.v in SUPPORTED_FORMATS,
153
163
  "root_matches": cas.root == cert.event_root,
154
164
  "head_matches": log.head() == cert.head_hash,
155
165
  "count_matches": len(log) == cert.n_events,
@@ -185,6 +195,14 @@ def verify_certificate(cert: ExecutionCertificate, log: EventLog, key: bytes | N
185
195
  and str(e.payload.get("decision", "")).lower() == "allow"
186
196
  )
187
197
 
198
+ # A verified receipt must carry NO data field its version does not sign. The v1
199
+ # body binds none of the v2 issuance fields, so a v1 receipt with issued_at /
200
+ # nonce / run_id set is carrying an unsigned, forgeable value (e.g. a backdated
201
+ # issued_at) -- reject it, so ok=True never blesses data outside the signature.
202
+ checks["fields_match_version"] = not (
203
+ cert.v == "vitnify-receipt v1"
204
+ and any(x is not None for x in (cert.issued_at, cert.nonce, cert.run_id)))
205
+
188
206
  # Optional signer pinning -- an embedded key proves continuity, not authority.
189
207
  # When an allow-list is supplied, the signer must be ed25519 and on the list.
190
208
  if pinned_pubkeys is not None:
@@ -12,28 +12,39 @@ from .capability import Broker, recorded_tool_results
12
12
  from .certificate import issue_certificate
13
13
 
14
14
  class Session:
15
- def __init__(self, lm, caps, tools, invariant, recorded: EventLog | None = None, prod_load=0):
15
+ def __init__(self, lm, caps, tools, invariant, recorded: EventLog | None = None, prod_load=0,
16
+ provider: dict | None = None):
16
17
  self.lm = lm
17
18
  self.invariant = invariant
18
19
  self.recorded = recorded
19
20
  self.prod_load = prod_load
21
+ # Hosted-provider identity to bind on every llm_call (see receipt-spec: for a
22
+ # hosted model, recording provider/model_version/system_fingerprint/response_id
23
+ # is what makes provider drift distinguishable from tampering). None for a
24
+ # local, recomputable model.
25
+ self.provider = provider
20
26
  self.log = EventLog()
21
27
  rr = recorded_tool_results(recorded) if recorded is not None else None
22
28
  self.broker = Broker(caps, tools, self.log, replay=rr)
23
29
 
24
- def llm(self, prompt: str, n_new=8):
30
+ def llm(self, prompt: str, n_new=8, provider: dict | None = None):
25
31
  load = 0 if self.recorded is not None else self.prod_load # replay runs alone
26
32
  toks, hashes = self.lm.generate(prompt, n_new=n_new, batch_load=load, invariant=self.invariant)
27
33
  text = self.lm.tok.decode(toks, skip_special_tokens=True)
28
- self.log.append(Kind.LLM_CALL, {"prompt_hash": hash_text(prompt), "tokens": toks,
29
- "logit_hashes": hashes, "text": text})
34
+ payload = {"prompt_hash": hash_text(prompt), "tokens": toks,
35
+ "logit_hashes": hashes, "text": text}
36
+ prov = {**(self.provider or {}), **(provider or {})} # per-call overrides session default
37
+ if prov:
38
+ payload["provider"] = prov
39
+ self.log.append(Kind.LLM_CALL, payload)
30
40
  return text
31
41
 
32
42
  def tool(self, name: str, *args):
33
43
  return self.broker.call(name, *args)
34
44
 
35
- def record_run(agent_fn, lm, caps, tools, key, prog_hash, prod_load=8, invariant=True, priv=None):
36
- s = Session(lm, caps, tools, invariant, recorded=None, prod_load=prod_load)
45
+ def record_run(agent_fn, lm, caps, tools, key, prog_hash, prod_load=8, invariant=True, priv=None,
46
+ provider: dict | None = None):
47
+ s = Session(lm, caps, tools, invariant, recorded=None, prod_load=prod_load, provider=provider)
37
48
  agent_fn(s)
38
49
  cert, _ = issue_certificate(prog_hash, caps, s.log, priv=priv, key=(None if priv else key))
39
50
  return s.log, cert
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: vitnify
3
- Version: 0.2.0
3
+ Version: 0.2.2
4
4
  Summary: Execution receipts for AI agents: contain, deterministically reconstruct, and cryptographically certify a run.
5
5
  Author: vitnify
6
6
  License: Apache-2.0
@@ -0,0 +1,132 @@
1
+ """vitnify-receipt v2 -- issuance identity (F4) and hosted-provider binding (F7).
2
+
3
+ v2 adds `issued_at`, `nonce`, and `run_id` to the signed body so a receipt can be
4
+ placed in time and one run's receipt can't stand in for another's, and supports
5
+ recording hosted-provider identity on the model step so provider drift is
6
+ distinguishable from tampering.
7
+ """
8
+ import copy
9
+
10
+ from vitnify.events import EventLog, Kind
11
+ from vitnify.certificate import issue_certificate, verify_certificate, gen_ed25519, FORMAT
12
+
13
+
14
+ def _run():
15
+ log = EventLog()
16
+ log.append(Kind.TOOL_CALL, {"tool": "read_docs", "decision": "allow"})
17
+ return log
18
+
19
+
20
+ def test_receipt_is_v2_and_carries_time_nonce_runid():
21
+ priv, _ = gen_ed25519()
22
+ log = _run()
23
+ cert, _ = issue_certificate("prog", ["read_docs"], log, priv=priv)
24
+ assert cert.v == FORMAT == "vitnify-receipt v2"
25
+ assert cert.issued_at and cert.issued_at.endswith("Z")
26
+ assert cert.nonce and cert.run_id
27
+ body = cert.body() # all three are inside the SIGNED body
28
+ assert body["issued_at"] == cert.issued_at
29
+ assert body["nonce"] == cert.nonce
30
+ assert body["run_id"] == cert.run_id
31
+ assert verify_certificate(cert, log)["ok"] is True
32
+
33
+
34
+ def test_run_id_is_honoured_when_supplied():
35
+ priv, _ = gen_ed25519()
36
+ cert, _ = issue_certificate("prog", ["read_docs"], _run(), priv=priv, run_id="case-42")
37
+ assert cert.run_id == "case-42"
38
+
39
+
40
+ def test_tampering_time_nonce_or_runid_breaks_the_signature():
41
+ priv, _ = gen_ed25519()
42
+ log = _run()
43
+ cert, _ = issue_certificate("prog", ["read_docs"], log, priv=priv)
44
+ for f in ("issued_at", "nonce", "run_id"):
45
+ forged = copy.deepcopy(cert)
46
+ setattr(forged, f, "tampered")
47
+ assert verify_certificate(forged, log)["ok"] is False
48
+
49
+
50
+ def test_hosted_provider_identity_is_bound_and_drift_is_detectable():
51
+ # F7: recording provider identity binds it into the receipt's event root, so a
52
+ # later check can tell a provider change apart from tampering.
53
+ priv, _ = gen_ed25519()
54
+ log = EventLog()
55
+ log.append_llm_call("ph", [1, 2], seed=0, model_digest="",
56
+ provider={"provider": "openai", "model_version": "gpt-x-2026-01",
57
+ "system_fingerprint": "fp_abc", "response_id": "resp_1"})
58
+ cert, _ = issue_certificate("prog", [], log, priv=priv)
59
+ assert verify_certificate(cert, log)["ok"] is True
60
+
61
+ # A different provider fingerprint yields a different event root, so the original
62
+ # receipt no longer matches -- drift is detected, and the bound provider fields
63
+ # make it attributable to the backend rather than to forgery.
64
+ drifted = EventLog()
65
+ drifted.append_llm_call("ph", [1, 2], seed=0, model_digest="",
66
+ provider={"provider": "openai", "model_version": "gpt-x-2026-02",
67
+ "system_fingerprint": "fp_XYZ", "response_id": "resp_2"})
68
+ assert verify_certificate(cert, drifted)["ok"] is False
69
+
70
+
71
+ def test_v1_receipt_still_verifies_under_the_v2_verifier():
72
+ # F8: a receipt validly signed under v1 (7-field body) must still verify -- a
73
+ # published format is frozen and the product's premise is verify-years-later.
74
+ from vitnify.certificate import ExecutionCertificate, _canon, _digest32
75
+ from vitnify._vendor.pck.cas import MerkleCAS
76
+ import copy
77
+ priv, pub = gen_ed25519()
78
+ log = _run()
79
+ cas = MerkleCAS(log.chunks())
80
+ v1_body = {"v": "vitnify-receipt v1", "program_hash": "prog",
81
+ "capabilities": ["read_docs"], "event_root": cas.root,
82
+ "n_events": len(log), "head_hash": log.head(),
83
+ "model_digests": log.model_digests()}
84
+ sig = priv.sign(bytes.fromhex(_digest32(_canon(v1_body).encode()))).hex()
85
+ cert = ExecutionCertificate("prog", ["read_docs"], cas.root, len(log), log.head(),
86
+ model_digests=log.model_digests())
87
+ cert.v, cert.sig, cert.sig_alg, cert.pubkey = "vitnify-receipt v1", sig, "ed25519", pub
88
+ r = verify_certificate(cert, log)
89
+ assert r["format"] and r["sig_valid"] and r["ok"] # v1 fully verifies
90
+ bad = copy.deepcopy(cert); bad.program_hash = "evil" # ...but tampering is caught
91
+ assert verify_certificate(bad, log)["ok"] is False
92
+
93
+
94
+ def test_f9_session_llm_records_provider():
95
+ from vitnify.replayer import Session
96
+
97
+ class _Tok:
98
+ def decode(self, toks, skip_special_tokens=True):
99
+ return "text"
100
+
101
+ class _LM:
102
+ tok = _Tok()
103
+ def generate(self, prompt, n_new, batch_load, invariant):
104
+ return [1, 2], ["h1", "h2"]
105
+
106
+ s = Session(_LM(), caps=[], tools={}, invariant=True,
107
+ provider={"provider": "openai", "model_version": "gpt-x"})
108
+ s.llm("hi", n_new=2, provider={"response_id": "resp_1"}) # per-call merges with session default
109
+ ev = next(e for e in s.log.events if e.kind == Kind.LLM_CALL.value)
110
+ assert ev.payload["provider"] == {
111
+ "provider": "openai", "model_version": "gpt-x", "response_id": "resp_1"}
112
+
113
+
114
+ def test_f10_v1_receipt_carrying_v2_fields_is_rejected():
115
+ # F10: v1 does not sign issued_at/nonce/run_id, so a v1 receipt that CARRIES them
116
+ # is carrying unsigned, forgeable data (e.g. a backdated timestamp) -- it must not
117
+ # verify, or ok=True would bless a value outside the signature.
118
+ from vitnify.certificate import ExecutionCertificate, _canon, _digest32
119
+ from vitnify._vendor.pck.cas import MerkleCAS
120
+ priv, pub = gen_ed25519()
121
+ log = _run(); cas = MerkleCAS(log.chunks())
122
+ b1 = {"v": "vitnify-receipt v1", "program_hash": "p", "capabilities": ["read_docs"],
123
+ "event_root": cas.root, "n_events": len(log), "head_hash": log.head(),
124
+ "model_digests": log.model_digests()}
125
+ sig = priv.sign(bytes.fromhex(_digest32(_canon(b1).encode()))).hex()
126
+ cert = ExecutionCertificate("p", ["read_docs"], cas.root, len(log), log.head(),
127
+ model_digests=log.model_digests())
128
+ cert.v, cert.sig, cert.sig_alg, cert.pubkey = "vitnify-receipt v1", sig, "ed25519", pub
129
+ assert verify_certificate(cert, log)["ok"] is True # genuine v1 still verifies
130
+ cert.issued_at = "2019-01-01T00:00:00Z" # forge a backdated timestamp
131
+ r = verify_certificate(cert, log)
132
+ assert r["fields_match_version"] is False and r["ok"] is False
@@ -1,68 +0,0 @@
1
- """vitnify-receipt v2 -- issuance identity (F4) and hosted-provider binding (F7).
2
-
3
- v2 adds `issued_at`, `nonce`, and `run_id` to the signed body so a receipt can be
4
- placed in time and one run's receipt can't stand in for another's, and supports
5
- recording hosted-provider identity on the model step so provider drift is
6
- distinguishable from tampering.
7
- """
8
- import copy
9
-
10
- from vitnify.events import EventLog, Kind
11
- from vitnify.certificate import issue_certificate, verify_certificate, gen_ed25519, FORMAT
12
-
13
-
14
- def _run():
15
- log = EventLog()
16
- log.append(Kind.TOOL_CALL, {"tool": "read_docs", "decision": "allow"})
17
- return log
18
-
19
-
20
- def test_receipt_is_v2_and_carries_time_nonce_runid():
21
- priv, _ = gen_ed25519()
22
- log = _run()
23
- cert, _ = issue_certificate("prog", ["read_docs"], log, priv=priv)
24
- assert cert.v == FORMAT == "vitnify-receipt v2"
25
- assert cert.issued_at and cert.issued_at.endswith("Z")
26
- assert cert.nonce and cert.run_id
27
- body = cert.body() # all three are inside the SIGNED body
28
- assert body["issued_at"] == cert.issued_at
29
- assert body["nonce"] == cert.nonce
30
- assert body["run_id"] == cert.run_id
31
- assert verify_certificate(cert, log)["ok"] is True
32
-
33
-
34
- def test_run_id_is_honoured_when_supplied():
35
- priv, _ = gen_ed25519()
36
- cert, _ = issue_certificate("prog", ["read_docs"], _run(), priv=priv, run_id="case-42")
37
- assert cert.run_id == "case-42"
38
-
39
-
40
- def test_tampering_time_nonce_or_runid_breaks_the_signature():
41
- priv, _ = gen_ed25519()
42
- log = _run()
43
- cert, _ = issue_certificate("prog", ["read_docs"], log, priv=priv)
44
- for f in ("issued_at", "nonce", "run_id"):
45
- forged = copy.deepcopy(cert)
46
- setattr(forged, f, "tampered")
47
- assert verify_certificate(forged, log)["ok"] is False
48
-
49
-
50
- def test_hosted_provider_identity_is_bound_and_drift_is_detectable():
51
- # F7: recording provider identity binds it into the receipt's event root, so a
52
- # later check can tell a provider change apart from tampering.
53
- priv, _ = gen_ed25519()
54
- log = EventLog()
55
- log.append_llm_call("ph", [1, 2], seed=0, model_digest="",
56
- provider={"provider": "openai", "model_version": "gpt-x-2026-01",
57
- "system_fingerprint": "fp_abc", "response_id": "resp_1"})
58
- cert, _ = issue_certificate("prog", [], log, priv=priv)
59
- assert verify_certificate(cert, log)["ok"] is True
60
-
61
- # A different provider fingerprint yields a different event root, so the original
62
- # receipt no longer matches -- drift is detected, and the bound provider fields
63
- # make it attributable to the backend rather than to forgery.
64
- drifted = EventLog()
65
- drifted.append_llm_call("ph", [1, 2], seed=0, model_digest="",
66
- provider={"provider": "openai", "model_version": "gpt-x-2026-02",
67
- "system_fingerprint": "fp_XYZ", "response_id": "resp_2"})
68
- assert verify_certificate(cert, drifted)["ok"] is False
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