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.
- {vitnify-0.2.7/src/vitnify.egg-info → vitnify-0.2.8}/PKG-INFO +1 -1
- {vitnify-0.2.7 → vitnify-0.2.8}/pyproject.toml +1 -1
- {vitnify-0.2.7 → vitnify-0.2.8}/src/vitnify/viewer.py +26 -10
- {vitnify-0.2.7 → vitnify-0.2.8/src/vitnify.egg-info}/PKG-INFO +1 -1
- {vitnify-0.2.7 → vitnify-0.2.8}/tests/test_viewer.py +30 -11
- {vitnify-0.2.7 → vitnify-0.2.8}/LICENSE +0 -0
- {vitnify-0.2.7 → vitnify-0.2.8}/NOTICE +0 -0
- {vitnify-0.2.7 → vitnify-0.2.8}/README.md +0 -0
- {vitnify-0.2.7 → vitnify-0.2.8}/setup.cfg +0 -0
- {vitnify-0.2.7 → vitnify-0.2.8}/src/vitnify/__init__.py +0 -0
- {vitnify-0.2.7 → vitnify-0.2.8}/src/vitnify/_vendor/__init__.py +0 -0
- {vitnify-0.2.7 → vitnify-0.2.8}/src/vitnify/_vendor/pck/__init__.py +0 -0
- {vitnify-0.2.7 → vitnify-0.2.8}/src/vitnify/_vendor/pck/cas.py +0 -0
- {vitnify-0.2.7 → vitnify-0.2.8}/src/vitnify/capability.py +0 -0
- {vitnify-0.2.7 → vitnify-0.2.8}/src/vitnify/certificate.py +0 -0
- {vitnify-0.2.7 → vitnify-0.2.8}/src/vitnify/deterministic_llm.py +0 -0
- {vitnify-0.2.7 → vitnify-0.2.8}/src/vitnify/engine.py +0 -0
- {vitnify-0.2.7 → vitnify-0.2.8}/src/vitnify/events.py +0 -0
- {vitnify-0.2.7 → vitnify-0.2.8}/src/vitnify/langgraph_adapter.py +0 -0
- {vitnify-0.2.7 → vitnify-0.2.8}/src/vitnify/mcp_adapter.py +0 -0
- {vitnify-0.2.7 → vitnify-0.2.8}/src/vitnify/react.py +0 -0
- {vitnify-0.2.7 → vitnify-0.2.8}/src/vitnify/replayer.py +0 -0
- {vitnify-0.2.7 → vitnify-0.2.8}/src/vitnify.egg-info/SOURCES.txt +0 -0
- {vitnify-0.2.7 → vitnify-0.2.8}/src/vitnify.egg-info/dependency_links.txt +0 -0
- {vitnify-0.2.7 → vitnify-0.2.8}/src/vitnify.egg-info/requires.txt +0 -0
- {vitnify-0.2.7 → vitnify-0.2.8}/src/vitnify.egg-info/top_level.txt +0 -0
- {vitnify-0.2.7 → vitnify-0.2.8}/tests/test_adapters.py +0 -0
- {vitnify-0.2.7 → vitnify-0.2.8}/tests/test_attack_matrix.py +0 -0
- {vitnify-0.2.7 → vitnify-0.2.8}/tests/test_capability.py +0 -0
- {vitnify-0.2.7 → vitnify-0.2.8}/tests/test_certificate.py +0 -0
- {vitnify-0.2.7 → vitnify-0.2.8}/tests/test_ed25519.py +0 -0
- {vitnify-0.2.7 → vitnify-0.2.8}/tests/test_events.py +0 -0
- {vitnify-0.2.7 → vitnify-0.2.8}/tests/test_l2_recompute.py +0 -0
- {vitnify-0.2.7 → vitnify-0.2.8}/tests/test_receipt_v2.py +0 -0
- {vitnify-0.2.7 → vitnify-0.2.8}/tests/test_verification.py +0 -0
- {vitnify-0.2.7 → vitnify-0.2.8}/tests/test_verify_hardening.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.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
|
-
#
|
|
95
|
-
#
|
|
96
|
-
#
|
|
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
|
-
|
|
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
|
|
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", "")
|
|
121
|
+
containment_badge = badge(True, "Injection contained", "") # an ungranted call was refused
|
|
107
122
|
else:
|
|
108
|
-
containment_badge = badge(True, "Containment enforced", "")
|
|
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
|
|
115
|
-
#
|
|
116
|
-
claims = (["contained"] if
|
|
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;}}
|
|
@@ -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
|
|
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
|
-
|
|
88
|
+
html = render(run)
|
|
89
|
+
headline = _headline(html)
|
|
86
90
|
assert "cryptographically certified" not in headline
|
|
87
|
-
assert "contained" in headline.lower()
|
|
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
|
|
91
|
-
#
|
|
92
|
-
#
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
-
|
|
98
|
-
|
|
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
|
|
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
|