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.
Files changed (35) hide show
  1. {vitnify-0.2.4/src/vitnify.egg-info → vitnify-0.2.5}/PKG-INFO +1 -1
  2. {vitnify-0.2.4 → vitnify-0.2.5}/pyproject.toml +1 -1
  3. {vitnify-0.2.4 → vitnify-0.2.5}/src/vitnify/certificate.py +31 -13
  4. {vitnify-0.2.4 → vitnify-0.2.5}/src/vitnify/events.py +8 -8
  5. {vitnify-0.2.4 → vitnify-0.2.5/src/vitnify.egg-info}/PKG-INFO +1 -1
  6. {vitnify-0.2.4 → vitnify-0.2.5}/tests/test_verify_hardening.py +20 -0
  7. {vitnify-0.2.4 → vitnify-0.2.5}/LICENSE +0 -0
  8. {vitnify-0.2.4 → vitnify-0.2.5}/NOTICE +0 -0
  9. {vitnify-0.2.4 → vitnify-0.2.5}/README.md +0 -0
  10. {vitnify-0.2.4 → vitnify-0.2.5}/setup.cfg +0 -0
  11. {vitnify-0.2.4 → vitnify-0.2.5}/src/vitnify/__init__.py +0 -0
  12. {vitnify-0.2.4 → vitnify-0.2.5}/src/vitnify/_vendor/__init__.py +0 -0
  13. {vitnify-0.2.4 → vitnify-0.2.5}/src/vitnify/_vendor/pck/__init__.py +0 -0
  14. {vitnify-0.2.4 → vitnify-0.2.5}/src/vitnify/_vendor/pck/cas.py +0 -0
  15. {vitnify-0.2.4 → vitnify-0.2.5}/src/vitnify/capability.py +0 -0
  16. {vitnify-0.2.4 → vitnify-0.2.5}/src/vitnify/deterministic_llm.py +0 -0
  17. {vitnify-0.2.4 → vitnify-0.2.5}/src/vitnify/engine.py +0 -0
  18. {vitnify-0.2.4 → vitnify-0.2.5}/src/vitnify/langgraph_adapter.py +0 -0
  19. {vitnify-0.2.4 → vitnify-0.2.5}/src/vitnify/mcp_adapter.py +0 -0
  20. {vitnify-0.2.4 → vitnify-0.2.5}/src/vitnify/react.py +0 -0
  21. {vitnify-0.2.4 → vitnify-0.2.5}/src/vitnify/replayer.py +0 -0
  22. {vitnify-0.2.4 → vitnify-0.2.5}/src/vitnify/viewer.py +0 -0
  23. {vitnify-0.2.4 → vitnify-0.2.5}/src/vitnify.egg-info/SOURCES.txt +0 -0
  24. {vitnify-0.2.4 → vitnify-0.2.5}/src/vitnify.egg-info/dependency_links.txt +0 -0
  25. {vitnify-0.2.4 → vitnify-0.2.5}/src/vitnify.egg-info/requires.txt +0 -0
  26. {vitnify-0.2.4 → vitnify-0.2.5}/src/vitnify.egg-info/top_level.txt +0 -0
  27. {vitnify-0.2.4 → vitnify-0.2.5}/tests/test_adapters.py +0 -0
  28. {vitnify-0.2.4 → vitnify-0.2.5}/tests/test_attack_matrix.py +0 -0
  29. {vitnify-0.2.4 → vitnify-0.2.5}/tests/test_capability.py +0 -0
  30. {vitnify-0.2.4 → vitnify-0.2.5}/tests/test_certificate.py +0 -0
  31. {vitnify-0.2.4 → vitnify-0.2.5}/tests/test_ed25519.py +0 -0
  32. {vitnify-0.2.4 → vitnify-0.2.5}/tests/test_events.py +0 -0
  33. {vitnify-0.2.4 → vitnify-0.2.5}/tests/test_l2_recompute.py +0 -0
  34. {vitnify-0.2.4 → vitnify-0.2.5}/tests/test_receipt_v2.py +0 -0
  35. {vitnify-0.2.4 → vitnify-0.2.5}/tests/test_verification.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: vitnify
3
- Version: 0.2.4
3
+ Version: 0.2.5
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.4"
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
- try:
45
- import blake3 as _blake3
46
- def _digest32(b: bytes) -> str:
47
- return _blake3.blake3(b).hexdigest()
48
- except ImportError:
49
- import hashlib
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. Each receipt is
125
- stamped with an issuer-asserted UTC time, a random nonce, and a run id (pass
126
- `run_id` to set your own; a random one is used otherwise) so distinct runs
127
- produce distinct, time-placeable receipts. ed25519 (self-verifying) is the
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
- checks["ok"] = all(checks.values())
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
- try:
17
- import blake3 as _blake3
18
- def _h32(b: bytes) -> str:
19
- return _blake3.blake3(b).hexdigest()
20
- except ImportError: # stdlib fallback; wire format is identical either way
21
- import hashlib
22
- def _h32(b: bytes) -> str:
23
- return hashlib.blake2b(b, digest_size=32).hexdigest()
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:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: vitnify
3
- Version: 0.2.4
3
+ Version: 0.2.5
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
@@ -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