vitnify 0.2.4__tar.gz → 0.2.6__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.4/src/vitnify.egg-info → vitnify-0.2.6}/PKG-INFO +8 -3
- {vitnify-0.2.4 → vitnify-0.2.6}/README.md +7 -2
- {vitnify-0.2.4 → vitnify-0.2.6}/pyproject.toml +1 -1
- {vitnify-0.2.4 → vitnify-0.2.6}/src/vitnify/_vendor/pck/cas.py +15 -15
- {vitnify-0.2.4 → vitnify-0.2.6}/src/vitnify/certificate.py +34 -15
- {vitnify-0.2.4 → vitnify-0.2.6}/src/vitnify/engine.py +6 -8
- {vitnify-0.2.4 → vitnify-0.2.6}/src/vitnify/events.py +8 -8
- {vitnify-0.2.4 → vitnify-0.2.6}/src/vitnify/viewer.py +22 -5
- {vitnify-0.2.4 → vitnify-0.2.6/src/vitnify.egg-info}/PKG-INFO +8 -3
- {vitnify-0.2.4 → vitnify-0.2.6}/tests/test_verify_hardening.py +20 -0
- {vitnify-0.2.4 → vitnify-0.2.6}/LICENSE +0 -0
- {vitnify-0.2.4 → vitnify-0.2.6}/NOTICE +0 -0
- {vitnify-0.2.4 → vitnify-0.2.6}/setup.cfg +0 -0
- {vitnify-0.2.4 → vitnify-0.2.6}/src/vitnify/__init__.py +0 -0
- {vitnify-0.2.4 → vitnify-0.2.6}/src/vitnify/_vendor/__init__.py +0 -0
- {vitnify-0.2.4 → vitnify-0.2.6}/src/vitnify/_vendor/pck/__init__.py +0 -0
- {vitnify-0.2.4 → vitnify-0.2.6}/src/vitnify/capability.py +0 -0
- {vitnify-0.2.4 → vitnify-0.2.6}/src/vitnify/deterministic_llm.py +0 -0
- {vitnify-0.2.4 → vitnify-0.2.6}/src/vitnify/langgraph_adapter.py +0 -0
- {vitnify-0.2.4 → vitnify-0.2.6}/src/vitnify/mcp_adapter.py +0 -0
- {vitnify-0.2.4 → vitnify-0.2.6}/src/vitnify/react.py +0 -0
- {vitnify-0.2.4 → vitnify-0.2.6}/src/vitnify/replayer.py +0 -0
- {vitnify-0.2.4 → vitnify-0.2.6}/src/vitnify.egg-info/SOURCES.txt +0 -0
- {vitnify-0.2.4 → vitnify-0.2.6}/src/vitnify.egg-info/dependency_links.txt +0 -0
- {vitnify-0.2.4 → vitnify-0.2.6}/src/vitnify.egg-info/requires.txt +0 -0
- {vitnify-0.2.4 → vitnify-0.2.6}/src/vitnify.egg-info/top_level.txt +0 -0
- {vitnify-0.2.4 → vitnify-0.2.6}/tests/test_adapters.py +0 -0
- {vitnify-0.2.4 → vitnify-0.2.6}/tests/test_attack_matrix.py +0 -0
- {vitnify-0.2.4 → vitnify-0.2.6}/tests/test_capability.py +0 -0
- {vitnify-0.2.4 → vitnify-0.2.6}/tests/test_certificate.py +0 -0
- {vitnify-0.2.4 → vitnify-0.2.6}/tests/test_ed25519.py +0 -0
- {vitnify-0.2.4 → vitnify-0.2.6}/tests/test_events.py +0 -0
- {vitnify-0.2.4 → vitnify-0.2.6}/tests/test_l2_recompute.py +0 -0
- {vitnify-0.2.4 → vitnify-0.2.6}/tests/test_receipt_v2.py +0 -0
- {vitnify-0.2.4 → vitnify-0.2.6}/tests/test_verification.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: vitnify
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.6
|
|
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
|
|
@@ -64,8 +64,13 @@ log.append(Kind.TOOL_CALL, {"tool": "send_email", "decision": "deny"}) # ungran
|
|
|
64
64
|
priv, pub = gen_ed25519()
|
|
65
65
|
cert, _ = issue_certificate("program_hash", ["read_docs"], log, priv=priv)
|
|
66
66
|
|
|
67
|
-
verify_certificate(cert, log) # level 1: offline integrity — no model, no secret
|
|
68
|
-
#
|
|
67
|
+
checks = verify_certificate(cert, log) # level 1: offline integrity — no model, no secret
|
|
68
|
+
assert checks["ok"] # signed, unaltered, and no ungranted tool ran
|
|
69
|
+
assert checks["containment_enforced"] # every tool call was GATED, not merely observed
|
|
70
|
+
# A receipt can be ok=True yet containment_enforced=False — a valid transcript from a
|
|
71
|
+
# watch-only integration proves what ran, not that anything was contained. A containment
|
|
72
|
+
# claim requires BOTH. (level 2: re-run each step through the engine; every model_digest
|
|
73
|
+
# reproduces bit-for-bit.)
|
|
69
74
|
```
|
|
70
75
|
|
|
71
76
|
See [`vitnify-receipt-v2.md`](vitnify-receipt-v2.md) for the receipt format, and
|
|
@@ -38,8 +38,13 @@ log.append(Kind.TOOL_CALL, {"tool": "send_email", "decision": "deny"}) # ungran
|
|
|
38
38
|
priv, pub = gen_ed25519()
|
|
39
39
|
cert, _ = issue_certificate("program_hash", ["read_docs"], log, priv=priv)
|
|
40
40
|
|
|
41
|
-
verify_certificate(cert, log) # level 1: offline integrity — no model, no secret
|
|
42
|
-
#
|
|
41
|
+
checks = verify_certificate(cert, log) # level 1: offline integrity — no model, no secret
|
|
42
|
+
assert checks["ok"] # signed, unaltered, and no ungranted tool ran
|
|
43
|
+
assert checks["containment_enforced"] # every tool call was GATED, not merely observed
|
|
44
|
+
# A receipt can be ok=True yet containment_enforced=False — a valid transcript from a
|
|
45
|
+
# watch-only integration proves what ran, not that anything was contained. A containment
|
|
46
|
+
# claim requires BOTH. (level 2: re-run each step through the engine; every model_digest
|
|
47
|
+
# reproduces bit-for-bit.)
|
|
43
48
|
```
|
|
44
49
|
|
|
45
50
|
See [`vitnify-receipt-v2.md`](vitnify-receipt-v2.md) for the receipt format, and
|
|
@@ -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.6"
|
|
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"
|
|
@@ -13,29 +13,24 @@ root. The guarantees this buys, and why they matter for AI answers:
|
|
|
13
13
|
hallucination: a grounded claim points at a chunk with a valid proof; an invented
|
|
14
14
|
one cannot.
|
|
15
15
|
|
|
16
|
-
This mirrors the BLAKE3 + Merkle design of a native runtime's ``cas_store
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
``
|
|
16
|
+
This mirrors the BLAKE3 + Merkle design of a native runtime's ``cas_store``; here we
|
|
17
|
+
keep a dependency-light pure-Python implementation over ``blake3``. BLAKE3 is a hard
|
|
18
|
+
dependency and DEFINES the wire format: a ``blake2b`` digest is a different value under
|
|
19
|
+
the same ``hash_name``, so there is NO silent stdlib fallback -- one would make honest
|
|
20
|
+
proofs indistinguishable from forgeries. A missing blake3 fails loudly (matching
|
|
21
|
+
``events.py`` / ``certificate.py``), never silently downgrades.
|
|
20
22
|
"""
|
|
21
23
|
from __future__ import annotations
|
|
22
24
|
|
|
23
|
-
import hashlib
|
|
24
25
|
from dataclasses import dataclass, field
|
|
25
26
|
from typing import Optional
|
|
26
27
|
|
|
27
|
-
|
|
28
|
-
import blake3 as _blake3 # type: ignore
|
|
28
|
+
import blake3 as _blake3
|
|
29
29
|
|
|
30
|
-
|
|
30
|
+
_HASH_NAME = "blake3"
|
|
31
31
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
except Exception: # pragma: no cover - depends on environment
|
|
35
|
-
_HASH_NAME = "blake2b-256"
|
|
36
|
-
|
|
37
|
-
def _digest(b: bytes) -> str:
|
|
38
|
-
return hashlib.blake2b(b, digest_size=32).hexdigest()
|
|
32
|
+
def _digest(b: bytes) -> str:
|
|
33
|
+
return _blake3.blake3(b).hexdigest()
|
|
39
34
|
|
|
40
35
|
|
|
41
36
|
HASH_NAME = _HASH_NAME
|
|
@@ -87,6 +82,11 @@ class InclusionProof:
|
|
|
87
82
|
hash_name: str = HASH_NAME
|
|
88
83
|
|
|
89
84
|
def verify(self, root: str) -> bool:
|
|
85
|
+
# A proof carries the hash suite it was built under. This module only speaks
|
|
86
|
+
# BLAKE3, so a proof committed under any other suite cannot be recomputed here
|
|
87
|
+
# -- reject rather than silently recompute under the wrong hash.
|
|
88
|
+
if self.hash_name != HASH_NAME:
|
|
89
|
+
return False
|
|
90
90
|
return verify_proof(self.leaf, self.path, root)
|
|
91
91
|
|
|
92
92
|
def to_json(self) -> dict:
|
|
@@ -41,17 +41,16 @@ 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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
# cryptography is imported lazily inside the ed25519 helpers so
|
|
54
|
-
#
|
|
51
|
+
# `cryptography` is imported lazily inside the ed25519 helpers, so HMAC-only signing
|
|
52
|
+
# works without it installed. (blake3, above, is a hard dependency -- there is no
|
|
53
|
+
# hash-free path; the module will not import without it.)
|
|
55
54
|
|
|
56
55
|
|
|
57
56
|
def _canon(o) -> str:
|
|
@@ -121,10 +120,18 @@ def issue_certificate(program_hash, capabilities, log: EventLog, priv=None, key:
|
|
|
121
120
|
"""Build and sign a vitnify-receipt for a completed run.
|
|
122
121
|
|
|
123
122
|
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.
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
123
|
+
receipt binds exactly the model steps that were recorded. `model_digests` is
|
|
124
|
+
EMPTY for a run with no recomputable model step (a hosted API, or a torch /
|
|
125
|
+
non-deterministic backend) -- such a receipt is integrity-only: it binds the
|
|
126
|
+
transcript, not the computation.
|
|
127
|
+
|
|
128
|
+
`program_hash` is CALLER-ASSERTED -- the receipt binds whatever string you pass;
|
|
129
|
+
it does not itself hash your agent's code. Pass a hash of the exact program or
|
|
130
|
+
config that ran if you want the receipt to pin it.
|
|
131
|
+
|
|
132
|
+
Each receipt is stamped with an issuer-asserted UTC time, a random nonce, and a
|
|
133
|
+
run id (pass `run_id` to set your own; a random one is used otherwise) so distinct
|
|
134
|
+
runs produce distinct, time-placeable receipts. ed25519 (self-verifying) is the
|
|
128
135
|
canonical signing path; pass `priv`.
|
|
129
136
|
"""
|
|
130
137
|
cas = MerkleCAS(log.chunks())
|
|
@@ -224,5 +231,17 @@ def verify_certificate(cert: ExecutionCertificate, log: EventLog, key: bytes | N
|
|
|
224
231
|
checks["signer_pinned"] = (
|
|
225
232
|
sig_valid and cert.sig_alg == "ed25519" and cert.pubkey in set(pinned_pubkeys))
|
|
226
233
|
|
|
227
|
-
|
|
234
|
+
# Enforcement vs observation. An "allow"/"deny" decision came from the capability
|
|
235
|
+
# wall -- the call was GATED. Any other value (e.g. "observed" from the record-only
|
|
236
|
+
# callback adapter) means it was WATCHED, not enforced, so the receipt is a
|
|
237
|
+
# tamper-evident transcript, not proof containment was applied -- the containment
|
|
238
|
+
# analogue of an empty model_digests. Reported separately, NOT folded into ok, so a
|
|
239
|
+
# valid transcript stays valid while a containment PROOF requires ok AND this True.
|
|
240
|
+
checks["containment_enforced"] = all(
|
|
241
|
+
str(e.payload.get("decision", "")).strip().lower() in ("allow", "deny")
|
|
242
|
+
for e in log.events
|
|
243
|
+
if e.kind == Kind.TOOL_CALL.value
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
checks["ok"] = all(v for k, v in checks.items() if k != "containment_enforced")
|
|
228
247
|
return checks
|
|
@@ -10,14 +10,12 @@ The engine is the `vitni-receipt` binary (built from the vitni-tensor crate). Po
|
|
|
10
10
|
from __future__ import annotations
|
|
11
11
|
import os, json, subprocess
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
def _h32(b: bytes) -> str:
|
|
20
|
-
return hashlib.blake2b(b, digest_size=32).hexdigest()
|
|
13
|
+
# BLAKE3 is a hard dependency and DEFINES the wire format (see events.py): the prompt
|
|
14
|
+
# hash bound into a receipt must be blake3, so there is no silent blake2b fallback -- a
|
|
15
|
+
# missing blake3 fails loudly rather than binding an incompatible digest.
|
|
16
|
+
import blake3 as _blake3
|
|
17
|
+
def _h32(b: bytes) -> str:
|
|
18
|
+
return _blake3.blake3(b).hexdigest()
|
|
21
19
|
|
|
22
20
|
DEFAULT_BIN = os.environ.get("VITNI_RECEIPT_BIN", "vitni-receipt")
|
|
23
21
|
|
|
@@ -13,14 +13,14 @@ import json
|
|
|
13
13
|
from dataclasses import dataclass, asdict
|
|
14
14
|
from enum import Enum
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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:
|
|
@@ -85,9 +85,25 @@ def render(run: dict) -> str:
|
|
|
85
85
|
def badge(ok, yes, no):
|
|
86
86
|
return f'<div class="badge {"good" if ok else "bad"}"><span class="dot"></span>{yes if ok else no}</div>'
|
|
87
87
|
|
|
88
|
+
def warn_badge(text):
|
|
89
|
+
return f'<div class="badge warn"><span class="dot"></span>{text}</div>'
|
|
90
|
+
|
|
91
|
+
# Whether the receipt PROVES containment, derived from the events the same way the
|
|
92
|
+
# verifier does: every tool decision must be a gated allow/deny. An observe-only
|
|
93
|
+
# run (a watching callback that records but does not gate) is a valid transcript but
|
|
94
|
+
# proves no containment -- it must NOT render as a plain green "verified" receipt,
|
|
95
|
+
# or a non-engineer is handed a badge that claims more than the receipt shows.
|
|
96
|
+
containment_enforced = all(
|
|
97
|
+
str(e["payload"].get("decision", "")).strip().lower() in ("allow", "deny")
|
|
98
|
+
for e in events if e["kind"] == "tool_call")
|
|
99
|
+
containment_badge = (badge(True, "Containment enforced", "")
|
|
100
|
+
if containment_enforced
|
|
101
|
+
else warn_badge("Containment observed — not proven"))
|
|
102
|
+
|
|
88
103
|
badges = (badge(v["contained"], "Injection contained", "NOT contained")
|
|
89
104
|
+ badge(v["replay_identical"], "Replay bit-identical", "Replay diverged")
|
|
90
|
-
+ badge(v["cert_ok"], "Certificate verified", "Certificate FAILED")
|
|
105
|
+
+ badge(v["cert_ok"], "Certificate verified", "Certificate FAILED")
|
|
106
|
+
+ containment_badge)
|
|
91
107
|
|
|
92
108
|
cert_rows = "".join(
|
|
93
109
|
f'<div class="crow"><span class="ck">{k}</span><span class="cv mono">{_esc(val)}</span></div>'
|
|
@@ -105,17 +121,17 @@ def render(run: dict) -> str:
|
|
|
105
121
|
:root {{
|
|
106
122
|
--bg:#FBFBFD; --surface:#FFFFFF; --surface-2:#F4F5F8; --ink:#1b1f27; --ink-dim:#5a6270;
|
|
107
123
|
--border:#E4E7ED; --accent:#0FB5A6; --ok:#1F9D63; --ok-bg:#E7F5EE; --deny:#D6532E; --deny-bg:#FBECE6;
|
|
108
|
-
--tamper:#E5484D; --mono-bg:#F2F4F7; --shadow:0 1px 2px rgba(20,26,40,.05),0 8px 24px rgba(20,26,40,.05);
|
|
124
|
+
--warn:#B7791F; --tamper:#E5484D; --mono-bg:#F2F4F7; --shadow:0 1px 2px rgba(20,26,40,.05),0 8px 24px rgba(20,26,40,.05);
|
|
109
125
|
}}
|
|
110
126
|
@media (prefers-color-scheme:dark){{ :root:not([data-theme="light"]){{
|
|
111
127
|
--bg:#0E1116; --surface:#161A21; --surface-2:#1C222B; --ink:#E6E9EF; --ink-dim:#98A2B3;
|
|
112
128
|
--border:#262D38; --accent:#25C6B6; --ok:#3FBE7C; --ok-bg:#12291E; --deny:#EE7A54; --deny-bg:#2A1811;
|
|
113
|
-
--tamper:#F2585B; --mono-bg:#12161C; --shadow:0 1px 2px rgba(0,0,0,.4),0 12px 32px rgba(0,0,0,.35);
|
|
129
|
+
--warn:#E0A93B; --tamper:#F2585B; --mono-bg:#12161C; --shadow:0 1px 2px rgba(0,0,0,.4),0 12px 32px rgba(0,0,0,.35);
|
|
114
130
|
}} }}
|
|
115
131
|
:root[data-theme="dark"] {{
|
|
116
132
|
--bg:#0E1116; --surface:#161A21; --surface-2:#1C222B; --ink:#E6E9EF; --ink-dim:#98A2B3;
|
|
117
133
|
--border:#262D38; --accent:#25C6B6; --ok:#3FBE7C; --ok-bg:#12291E; --deny:#EE7A54; --deny-bg:#2A1811;
|
|
118
|
-
--tamper:#F2585B; --mono-bg:#12161C; --shadow:0 1px 2px rgba(0,0,0,.4),0 12px 32px rgba(0,0,0,.35);
|
|
134
|
+
--warn:#E0A93B; --tamper:#F2585B; --mono-bg:#12161C; --shadow:0 1px 2px rgba(0,0,0,.4),0 12px 32px rgba(0,0,0,.35);
|
|
119
135
|
}}
|
|
120
136
|
*{{box-sizing:border-box}}
|
|
121
137
|
body{{margin:0;background:var(--bg);color:var(--ink);
|
|
@@ -133,6 +149,7 @@ h1{{font-size:27px;margin:0;letter-spacing:-.01em;text-wrap:balance;}}
|
|
|
133
149
|
.badge .dot{{width:8px;height:8px;border-radius:50%;}}
|
|
134
150
|
.badge.good{{color:var(--ok);}} .badge.good .dot{{background:var(--ok);}}
|
|
135
151
|
.badge.bad{{color:var(--tamper);}} .badge.bad .dot{{background:var(--tamper);}}
|
|
152
|
+
.badge.warn{{color:var(--warn);}} .badge.warn .dot{{background:var(--warn);}}
|
|
136
153
|
section{{background:var(--surface);border:1px solid var(--border);border-radius:14px;padding:22px 22px;margin:16px 0;box-shadow:var(--shadow);}}
|
|
137
154
|
.sec-h{{display:flex;align-items:baseline;justify-content:space-between;gap:12px;margin:0 0 14px;}}
|
|
138
155
|
.sec-h h2{{font-size:13px;letter-spacing:.09em;text-transform:uppercase;color:var(--ink-dim);margin:0;font-weight:700;}}
|
|
@@ -174,7 +191,7 @@ footer{{color:var(--ink-dim);font-size:12px;margin-top:26px;text-align:center;}}
|
|
|
174
191
|
|
|
175
192
|
<div class="wrap">
|
|
176
193
|
<header class="top">
|
|
177
|
-
<div class="eyebrow">
|
|
194
|
+
<div class="eyebrow">Vitnify · Execution Certificate</div>
|
|
178
195
|
<h1>This agent run was contained, replayed bit-for-bit, and cryptographically certified.</h1>
|
|
179
196
|
<p class="task">Task recorded: “{_esc(run.get('task','(agent run)'))}”</p>
|
|
180
197
|
</header>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: vitnify
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.6
|
|
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
|
|
@@ -64,8 +64,13 @@ log.append(Kind.TOOL_CALL, {"tool": "send_email", "decision": "deny"}) # ungran
|
|
|
64
64
|
priv, pub = gen_ed25519()
|
|
65
65
|
cert, _ = issue_certificate("program_hash", ["read_docs"], log, priv=priv)
|
|
66
66
|
|
|
67
|
-
verify_certificate(cert, log) # level 1: offline integrity — no model, no secret
|
|
68
|
-
#
|
|
67
|
+
checks = verify_certificate(cert, log) # level 1: offline integrity — no model, no secret
|
|
68
|
+
assert checks["ok"] # signed, unaltered, and no ungranted tool ran
|
|
69
|
+
assert checks["containment_enforced"] # every tool call was GATED, not merely observed
|
|
70
|
+
# A receipt can be ok=True yet containment_enforced=False — a valid transcript from a
|
|
71
|
+
# watch-only integration proves what ran, not that anything was contained. A containment
|
|
72
|
+
# claim requires BOTH. (level 2: re-run each step through the engine; every model_digest
|
|
73
|
+
# reproduces bit-for-bit.)
|
|
69
74
|
```
|
|
70
75
|
|
|
71
76
|
See [`vitnify-receipt-v2.md`](vitnify-receipt-v2.md) for the receipt format, and
|
|
@@ -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
|
|
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
|