vitnify 0.2.1__tar.gz → 0.2.3__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 (35) hide show
  1. {vitnify-0.2.1/src/vitnify.egg-info → vitnify-0.2.3}/PKG-INFO +1 -1
  2. {vitnify-0.2.1 → vitnify-0.2.3}/pyproject.toml +1 -1
  3. {vitnify-0.2.1 → vitnify-0.2.3}/src/vitnify/certificate.py +21 -5
  4. {vitnify-0.2.1 → vitnify-0.2.3/src/vitnify.egg-info}/PKG-INFO +1 -1
  5. {vitnify-0.2.1 → vitnify-0.2.3}/tests/test_receipt_v2.py +21 -0
  6. {vitnify-0.2.1 → vitnify-0.2.3}/tests/test_verify_hardening.py +21 -0
  7. {vitnify-0.2.1 → vitnify-0.2.3}/LICENSE +0 -0
  8. {vitnify-0.2.1 → vitnify-0.2.3}/NOTICE +0 -0
  9. {vitnify-0.2.1 → vitnify-0.2.3}/README.md +0 -0
  10. {vitnify-0.2.1 → vitnify-0.2.3}/setup.cfg +0 -0
  11. {vitnify-0.2.1 → vitnify-0.2.3}/src/vitnify/__init__.py +0 -0
  12. {vitnify-0.2.1 → vitnify-0.2.3}/src/vitnify/_vendor/__init__.py +0 -0
  13. {vitnify-0.2.1 → vitnify-0.2.3}/src/vitnify/_vendor/pck/__init__.py +0 -0
  14. {vitnify-0.2.1 → vitnify-0.2.3}/src/vitnify/_vendor/pck/cas.py +0 -0
  15. {vitnify-0.2.1 → vitnify-0.2.3}/src/vitnify/capability.py +0 -0
  16. {vitnify-0.2.1 → vitnify-0.2.3}/src/vitnify/deterministic_llm.py +0 -0
  17. {vitnify-0.2.1 → vitnify-0.2.3}/src/vitnify/engine.py +0 -0
  18. {vitnify-0.2.1 → vitnify-0.2.3}/src/vitnify/events.py +0 -0
  19. {vitnify-0.2.1 → vitnify-0.2.3}/src/vitnify/langgraph_adapter.py +0 -0
  20. {vitnify-0.2.1 → vitnify-0.2.3}/src/vitnify/mcp_adapter.py +0 -0
  21. {vitnify-0.2.1 → vitnify-0.2.3}/src/vitnify/react.py +0 -0
  22. {vitnify-0.2.1 → vitnify-0.2.3}/src/vitnify/replayer.py +0 -0
  23. {vitnify-0.2.1 → vitnify-0.2.3}/src/vitnify/viewer.py +0 -0
  24. {vitnify-0.2.1 → vitnify-0.2.3}/src/vitnify.egg-info/SOURCES.txt +0 -0
  25. {vitnify-0.2.1 → vitnify-0.2.3}/src/vitnify.egg-info/dependency_links.txt +0 -0
  26. {vitnify-0.2.1 → vitnify-0.2.3}/src/vitnify.egg-info/requires.txt +0 -0
  27. {vitnify-0.2.1 → vitnify-0.2.3}/src/vitnify.egg-info/top_level.txt +0 -0
  28. {vitnify-0.2.1 → vitnify-0.2.3}/tests/test_adapters.py +0 -0
  29. {vitnify-0.2.1 → vitnify-0.2.3}/tests/test_attack_matrix.py +0 -0
  30. {vitnify-0.2.1 → vitnify-0.2.3}/tests/test_capability.py +0 -0
  31. {vitnify-0.2.1 → vitnify-0.2.3}/tests/test_certificate.py +0 -0
  32. {vitnify-0.2.1 → vitnify-0.2.3}/tests/test_ed25519.py +0 -0
  33. {vitnify-0.2.1 → vitnify-0.2.3}/tests/test_events.py +0 -0
  34. {vitnify-0.2.1 → vitnify-0.2.3}/tests/test_l2_recompute.py +0 -0
  35. {vitnify-0.2.1 → vitnify-0.2.3}/tests/test_verification.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: vitnify
3
- Version: 0.2.1
3
+ Version: 0.2.3
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.1"
7
+ version = "0.2.3"
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"
@@ -184,17 +184,33 @@ def verify_certificate(cert: ExecutionCertificate, log: EventLog, key: bytes | N
184
184
  cert.sig, hmac.new(key, cert.digest().encode(), hashlib.blake2b).hexdigest())
185
185
  checks["sig_valid"] = sig_valid
186
186
 
187
- # Capability consistency -- the receipt must PROVE containment held, not just
188
- # carry a capability list: every ALLOWED tool call has to be within the
189
- # declared, signed-over capability set. (Denied calls need not be granted.)
187
+ # Capability consistency -- the receipt must PROVE no ungranted tool executed,
188
+ # not just carry a capability list. Every tool call must be within the declared,
189
+ # signed-over capability set OR a CLEAN DENIAL (decision exactly "deny",
190
+ # case/space-insensitive, with no result). Keying only off decision=="allow"
191
+ # let a forged log slip an ungranted, result-bearing call through by relabelling
192
+ # the decision ("PERMIT", " allow", omitted...) -- F11. Failing closed on
193
+ # anything that is not a clean denial removes that, whatever string it carries.
190
194
  granted = set(cert.capabilities)
195
+
196
+ def _clean_denial(p: dict) -> bool:
197
+ return (str(p.get("decision", "")).strip().lower() == "deny"
198
+ and "result" not in p and "result_hash" not in p)
199
+
191
200
  checks["caps_consistent"] = all(
192
- e.payload.get("tool") in granted
201
+ e.payload.get("tool") in granted or _clean_denial(e.payload)
193
202
  for e in log.events
194
203
  if e.kind == Kind.TOOL_CALL.value
195
- and str(e.payload.get("decision", "")).lower() == "allow"
196
204
  )
197
205
 
206
+ # A verified receipt must carry NO data field its version does not sign. The v1
207
+ # body binds none of the v2 issuance fields, so a v1 receipt with issued_at /
208
+ # nonce / run_id set is carrying an unsigned, forgeable value (e.g. a backdated
209
+ # issued_at) -- reject it, so ok=True never blesses data outside the signature.
210
+ checks["fields_match_version"] = not (
211
+ cert.v == "vitnify-receipt v1"
212
+ and any(x is not None for x in (cert.issued_at, cert.nonce, cert.run_id)))
213
+
198
214
  # Optional signer pinning -- an embedded key proves continuity, not authority.
199
215
  # When an allow-list is supplied, the signer must be ed25519 and on the list.
200
216
  if pinned_pubkeys is not None:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: vitnify
3
- Version: 0.2.1
3
+ Version: 0.2.3
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
@@ -109,3 +109,24 @@ def test_f9_session_llm_records_provider():
109
109
  ev = next(e for e in s.log.events if e.kind == Kind.LLM_CALL.value)
110
110
  assert ev.payload["provider"] == {
111
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
@@ -74,3 +74,24 @@ 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
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