vitnify 0.2.7__tar.gz → 0.2.8__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.7/src/vitnify.egg-info → vitnify-0.2.8}/PKG-INFO +1 -1
  2. {vitnify-0.2.7 → vitnify-0.2.8}/pyproject.toml +1 -1
  3. {vitnify-0.2.7 → vitnify-0.2.8}/src/vitnify/viewer.py +26 -10
  4. {vitnify-0.2.7 → vitnify-0.2.8/src/vitnify.egg-info}/PKG-INFO +1 -1
  5. {vitnify-0.2.7 → vitnify-0.2.8}/tests/test_viewer.py +30 -11
  6. {vitnify-0.2.7 → vitnify-0.2.8}/LICENSE +0 -0
  7. {vitnify-0.2.7 → vitnify-0.2.8}/NOTICE +0 -0
  8. {vitnify-0.2.7 → vitnify-0.2.8}/README.md +0 -0
  9. {vitnify-0.2.7 → vitnify-0.2.8}/setup.cfg +0 -0
  10. {vitnify-0.2.7 → vitnify-0.2.8}/src/vitnify/__init__.py +0 -0
  11. {vitnify-0.2.7 → vitnify-0.2.8}/src/vitnify/_vendor/__init__.py +0 -0
  12. {vitnify-0.2.7 → vitnify-0.2.8}/src/vitnify/_vendor/pck/__init__.py +0 -0
  13. {vitnify-0.2.7 → vitnify-0.2.8}/src/vitnify/_vendor/pck/cas.py +0 -0
  14. {vitnify-0.2.7 → vitnify-0.2.8}/src/vitnify/capability.py +0 -0
  15. {vitnify-0.2.7 → vitnify-0.2.8}/src/vitnify/certificate.py +0 -0
  16. {vitnify-0.2.7 → vitnify-0.2.8}/src/vitnify/deterministic_llm.py +0 -0
  17. {vitnify-0.2.7 → vitnify-0.2.8}/src/vitnify/engine.py +0 -0
  18. {vitnify-0.2.7 → vitnify-0.2.8}/src/vitnify/events.py +0 -0
  19. {vitnify-0.2.7 → vitnify-0.2.8}/src/vitnify/langgraph_adapter.py +0 -0
  20. {vitnify-0.2.7 → vitnify-0.2.8}/src/vitnify/mcp_adapter.py +0 -0
  21. {vitnify-0.2.7 → vitnify-0.2.8}/src/vitnify/react.py +0 -0
  22. {vitnify-0.2.7 → vitnify-0.2.8}/src/vitnify/replayer.py +0 -0
  23. {vitnify-0.2.7 → vitnify-0.2.8}/src/vitnify.egg-info/SOURCES.txt +0 -0
  24. {vitnify-0.2.7 → vitnify-0.2.8}/src/vitnify.egg-info/dependency_links.txt +0 -0
  25. {vitnify-0.2.7 → vitnify-0.2.8}/src/vitnify.egg-info/requires.txt +0 -0
  26. {vitnify-0.2.7 → vitnify-0.2.8}/src/vitnify.egg-info/top_level.txt +0 -0
  27. {vitnify-0.2.7 → vitnify-0.2.8}/tests/test_adapters.py +0 -0
  28. {vitnify-0.2.7 → vitnify-0.2.8}/tests/test_attack_matrix.py +0 -0
  29. {vitnify-0.2.7 → vitnify-0.2.8}/tests/test_capability.py +0 -0
  30. {vitnify-0.2.7 → vitnify-0.2.8}/tests/test_certificate.py +0 -0
  31. {vitnify-0.2.7 → vitnify-0.2.8}/tests/test_ed25519.py +0 -0
  32. {vitnify-0.2.7 → vitnify-0.2.8}/tests/test_events.py +0 -0
  33. {vitnify-0.2.7 → vitnify-0.2.8}/tests/test_l2_recompute.py +0 -0
  34. {vitnify-0.2.7 → vitnify-0.2.8}/tests/test_receipt_v2.py +0 -0
  35. {vitnify-0.2.7 → vitnify-0.2.8}/tests/test_verification.py +0 -0
  36. {vitnify-0.2.7 → vitnify-0.2.8}/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.7
3
+ Version: 0.2.8
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.7"
7
+ version = "0.2.8"
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"
@@ -89,31 +89,46 @@ def render(run: dict) -> str:
89
89
  def warn_badge(text):
90
90
  return f'<div class="badge warn"><span class="dot"></span>{text}</div>'
91
91
 
92
+ def neutral_badge(text):
93
+ return f'<div class="badge neutral"><span class="dot"></span>{text}</div>'
94
+
92
95
  # Every containment claim on this page is DERIVED from the events with the SAME rule
93
96
  # the verifier uses (decision_is_gated) -- never taken from the caller's verdict dict.
94
- # An observe-only run (a watching callback that records but does not gate) is a valid
95
- # transcript that proves no containment; it must not read as "contained" anywhere,
96
- # headline included, or a non-engineer is handed a claim stronger than the receipt.
97
+ # But a claim is only as strong as the evidence under it, so the page asserts
98
+ # containment POSITIVELY only when both hold:
99
+ # * the certificate verifies -- otherwise the log is UNVERIFIED and may be the very
100
+ # forgery the certificate exists to catch, so nothing derived from it can be
101
+ # trusted, containment included; and
102
+ # * a tool was actually gated -- a run that called no tool has nothing to contain,
103
+ # so "contained" would be vacuously true.
104
+ # verify_certificate reports those cases as containment_enforced=True (a machine
105
+ # predicate over the events alone); the human page is deliberately stricter and never
106
+ # reads as "contained" on unverified or empty evidence.
97
107
  tool_events = [e for e in events if e["kind"] == "tool_call"]
98
- containment_enforced = all(decision_is_gated(e["payload"].get("decision")) for e in tool_events)
108
+ all_gated = all(decision_is_gated(e["payload"].get("decision")) for e in tool_events)
99
109
  blocked = any(str(e["payload"].get("decision", "")).strip().lower() == "deny" for e in tool_events)
100
110
  cert_ok = bool(v.get("cert_ok"))
101
111
  replay_ok = bool(v.get("replay_identical"))
112
+ containment_proven = cert_ok and bool(tool_events) and all_gated
102
113
 
103
- if not containment_enforced:
114
+ if not cert_ok:
115
+ containment_badge = warn_badge("Containment unverified — certificate failed")
116
+ elif not tool_events:
117
+ containment_badge = neutral_badge("No tool calls") # nothing to contain
118
+ elif not all_gated:
104
119
  containment_badge = warn_badge("Containment observed — not proven")
105
120
  elif blocked:
106
- containment_badge = badge(True, "Injection contained", "") # an ungranted call was refused
121
+ containment_badge = badge(True, "Injection contained", "") # an ungranted call was refused
107
122
  else:
108
- containment_badge = badge(True, "Containment enforced", "") # every call gated; none needed blocking
123
+ containment_badge = badge(True, "Containment enforced", "") # every call gated; none needed blocking
109
124
 
110
125
  badges = (containment_badge
111
126
  + badge(replay_ok, "Replay bit-identical", "Replay diverged")
112
127
  + badge(cert_ok, "Certificate verified", "Certificate FAILED"))
113
128
 
114
- # Headline asserts ONLY what holds. "contained" tracks containment_enforced (not the
115
- # caller), so the observe-only page drops the word instead of retracting it below.
116
- claims = (["contained"] if containment_enforced else []) \
129
+ # Headline asserts ONLY what the evidence supports: "contained" needs a verified log
130
+ # AND a gated tool; "cryptographically certified" needs the signature to check out.
131
+ claims = (["contained"] if containment_proven else []) \
117
132
  + (["replayed bit-for-bit"] if replay_ok else []) \
118
133
  + (["cryptographically certified"] if cert_ok else [])
119
134
  if not claims:
@@ -170,6 +185,7 @@ h1{{font-size:27px;margin:0;letter-spacing:-.01em;text-wrap:balance;}}
170
185
  .badge.good{{color:var(--ok);}} .badge.good .dot{{background:var(--ok);}}
171
186
  .badge.bad{{color:var(--tamper);}} .badge.bad .dot{{background:var(--tamper);}}
172
187
  .badge.warn{{color:var(--warn);}} .badge.warn .dot{{background:var(--warn);}}
188
+ .badge.neutral{{color:var(--ink-dim);}} .badge.neutral .dot{{background:var(--ink-dim);}}
173
189
  section{{background:var(--surface);border:1px solid var(--border);border-radius:14px;padding:22px 22px;margin:16px 0;box-shadow:var(--shadow);}}
174
190
  .sec-h{{display:flex;align-items:baseline;justify-content:space-between;gap:12px;margin:0 0 14px;}}
175
191
  .sec-h h2{{font-size:13px;letter-spacing:.09em;text-transform:uppercase;color:var(--ink-dim);margin:0;font-weight:700;}}
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: vitnify
3
- Version: 0.2.7
3
+ Version: 0.2.8
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
@@ -80,22 +80,41 @@ def test_enforced_all_allow_claims_containment():
80
80
  assert "Containment observed" not in html
81
81
 
82
82
 
83
- def test_headline_drops_uncertified_claim():
83
+ def test_cert_failed_drops_all_derived_claims():
84
+ # If the certificate does not verify, the event log is UNVERIFIED -- it may be the
85
+ # very forgery the certificate exists to catch -- so the page must not claim
86
+ # containment (or certification) from it, even though every event reads as gated.
84
87
  run, _ = _build(["allow"], cert_ok=False)
85
- headline = _headline(render(run))
88
+ html = render(run)
89
+ headline = _headline(html)
86
90
  assert "cryptographically certified" not in headline
87
- assert "contained" in headline.lower() # containment still holds
91
+ assert "contained" not in headline.lower()
92
+ assert ("warn", "Containment unverified — certificate failed") in _badges(html)
93
+ assert ("bad", "Certificate FAILED") in _badges(html)
94
+ assert not any(t in ("Injection contained", "Containment enforced") for _, t in _badges(html))
88
95
 
89
96
 
90
- def test_viewer_containment_matches_verifier():
91
- # The viewer and the verifier must never disagree about containment -- they share
92
- # one predicate (decision_is_gated). Assert the page's warn badge appears iff the
93
- # verifier reports containment_enforced=False, for both gated and observed runs.
94
- for decisions in (["allow", "deny"], ["observed"], ["allow"], ["PERMIT"]):
95
- run, log = _build(decisions)
97
+ def test_zero_tool_run_is_not_vacuously_contained():
98
+ # all([]) is True, so a tool-free run is containment_enforced at the API level -- but
99
+ # the page must not tell a human the run was "contained" when nothing was ever gated.
100
+ run, _ = _build([]) # llm_call only, no tool calls
101
+ html = render(run)
102
+ assert "contained" not in _headline(html).lower()
103
+ assert ("neutral", "No tool calls") in _badges(html)
104
+
105
+
106
+ def test_viewer_never_claims_containment_without_verified_gating():
107
+ # Soundness: the headline reads "contained" ONLY when the certificate verifies, a
108
+ # tool was actually gated, and the verifier's own predicate agrees gating held. The
109
+ # page never claims containment on unverified, empty, or ungated evidence.
110
+ cases = [(["allow", "deny"], True), (["observed"], True), (["allow"], True),
111
+ (["PERMIT"], True), (["allow"], False), ([], True)]
112
+ for decisions, cert_ok in cases:
113
+ run, log = _build(decisions, cert_ok=cert_ok)
114
+ says_contained = "contained" in _headline(render(run)).lower()
96
115
  enforced = verify_certificate(_recert(run), log)["containment_enforced"]
97
- warned = "Containment observed — not proven" in render(run)
98
- assert warned == (not enforced), decisions
116
+ if says_contained:
117
+ assert cert_ok and bool(decisions) and enforced, (decisions, cert_ok)
99
118
 
100
119
 
101
120
  def test_render_tolerates_missing_digest():
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