vitnify 0.2.0__tar.gz → 0.2.2__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.0/src/vitnify.egg-info → vitnify-0.2.2}/PKG-INFO +1 -1
- {vitnify-0.2.0 → vitnify-0.2.2}/pyproject.toml +1 -1
- {vitnify-0.2.0 → vitnify-0.2.2}/src/vitnify/certificate.py +27 -9
- {vitnify-0.2.0 → vitnify-0.2.2}/src/vitnify/replayer.py +17 -6
- {vitnify-0.2.0 → vitnify-0.2.2/src/vitnify.egg-info}/PKG-INFO +1 -1
- vitnify-0.2.2/tests/test_receipt_v2.py +132 -0
- vitnify-0.2.0/tests/test_receipt_v2.py +0 -68
- {vitnify-0.2.0 → vitnify-0.2.2}/LICENSE +0 -0
- {vitnify-0.2.0 → vitnify-0.2.2}/NOTICE +0 -0
- {vitnify-0.2.0 → vitnify-0.2.2}/README.md +0 -0
- {vitnify-0.2.0 → vitnify-0.2.2}/setup.cfg +0 -0
- {vitnify-0.2.0 → vitnify-0.2.2}/src/vitnify/__init__.py +0 -0
- {vitnify-0.2.0 → vitnify-0.2.2}/src/vitnify/_vendor/__init__.py +0 -0
- {vitnify-0.2.0 → vitnify-0.2.2}/src/vitnify/_vendor/pck/__init__.py +0 -0
- {vitnify-0.2.0 → vitnify-0.2.2}/src/vitnify/_vendor/pck/cas.py +0 -0
- {vitnify-0.2.0 → vitnify-0.2.2}/src/vitnify/capability.py +0 -0
- {vitnify-0.2.0 → vitnify-0.2.2}/src/vitnify/deterministic_llm.py +0 -0
- {vitnify-0.2.0 → vitnify-0.2.2}/src/vitnify/engine.py +0 -0
- {vitnify-0.2.0 → vitnify-0.2.2}/src/vitnify/events.py +0 -0
- {vitnify-0.2.0 → vitnify-0.2.2}/src/vitnify/langgraph_adapter.py +0 -0
- {vitnify-0.2.0 → vitnify-0.2.2}/src/vitnify/mcp_adapter.py +0 -0
- {vitnify-0.2.0 → vitnify-0.2.2}/src/vitnify/react.py +0 -0
- {vitnify-0.2.0 → vitnify-0.2.2}/src/vitnify/viewer.py +0 -0
- {vitnify-0.2.0 → vitnify-0.2.2}/src/vitnify.egg-info/SOURCES.txt +0 -0
- {vitnify-0.2.0 → vitnify-0.2.2}/src/vitnify.egg-info/dependency_links.txt +0 -0
- {vitnify-0.2.0 → vitnify-0.2.2}/src/vitnify.egg-info/requires.txt +0 -0
- {vitnify-0.2.0 → vitnify-0.2.2}/src/vitnify.egg-info/top_level.txt +0 -0
- {vitnify-0.2.0 → vitnify-0.2.2}/tests/test_adapters.py +0 -0
- {vitnify-0.2.0 → vitnify-0.2.2}/tests/test_attack_matrix.py +0 -0
- {vitnify-0.2.0 → vitnify-0.2.2}/tests/test_capability.py +0 -0
- {vitnify-0.2.0 → vitnify-0.2.2}/tests/test_certificate.py +0 -0
- {vitnify-0.2.0 → vitnify-0.2.2}/tests/test_ed25519.py +0 -0
- {vitnify-0.2.0 → vitnify-0.2.2}/tests/test_events.py +0 -0
- {vitnify-0.2.0 → vitnify-0.2.2}/tests/test_l2_recompute.py +0 -0
- {vitnify-0.2.0 → vitnify-0.2.2}/tests/test_verification.py +0 -0
- {vitnify-0.2.0 → vitnify-0.2.2}/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.2"
|
|
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"
|
|
@@ -27,6 +27,10 @@ from .events import EventLog, Kind
|
|
|
27
27
|
from ._vendor.pck.cas import MerkleCAS # noqa: E402
|
|
28
28
|
|
|
29
29
|
FORMAT = "vitnify-receipt v2"
|
|
30
|
+
# A published receipt format is frozen; older receipts must stay verifiable across a
|
|
31
|
+
# verifier upgrade. The verifier accepts every version it has ever issued and
|
|
32
|
+
# reconstructs each receipt's signed body per its own `v` (see body()).
|
|
33
|
+
SUPPORTED_FORMATS = frozenset({"vitnify-receipt v1", FORMAT})
|
|
30
34
|
|
|
31
35
|
|
|
32
36
|
def _now_iso() -> str:
|
|
@@ -78,14 +82,20 @@ class ExecutionCertificate:
|
|
|
78
82
|
pubkey: str | None = None # ed25519 public key (hex) -- verifier needs no secret
|
|
79
83
|
|
|
80
84
|
def body(self) -> dict:
|
|
81
|
-
# Everything here is signed
|
|
82
|
-
#
|
|
83
|
-
#
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
85
|
+
# Everything here is signed, and the body is VERSIONED: v2+ additionally
|
|
86
|
+
# binds issued_at/nonce/run_id (which place the receipt in time and make it
|
|
87
|
+
# unique). Reconstructing the body per the receipt's OWN `v` is what lets a
|
|
88
|
+
# v2 verifier still verify a receipt signed under v1 -- a published format is
|
|
89
|
+
# frozen, so older receipts must remain verifiable.
|
|
90
|
+
b = {"v": self.v, "program_hash": self.program_hash,
|
|
91
|
+
"capabilities": sorted(self.capabilities), "event_root": self.event_root,
|
|
92
|
+
"n_events": self.n_events, "head_hash": self.head_hash,
|
|
93
|
+
"model_digests": list(self.model_digests)}
|
|
94
|
+
if self.v != "vitnify-receipt v1":
|
|
95
|
+
b["issued_at"] = self.issued_at
|
|
96
|
+
b["nonce"] = self.nonce
|
|
97
|
+
b["run_id"] = self.run_id
|
|
98
|
+
return b
|
|
89
99
|
|
|
90
100
|
def digest(self) -> str:
|
|
91
101
|
return _digest32(_canon(self.body()).encode())
|
|
@@ -149,7 +159,7 @@ def verify_certificate(cert: ExecutionCertificate, log: EventLog, key: bytes | N
|
|
|
149
159
|
"""
|
|
150
160
|
cas = MerkleCAS(log.chunks())
|
|
151
161
|
checks = {
|
|
152
|
-
"format": cert.v
|
|
162
|
+
"format": cert.v in SUPPORTED_FORMATS,
|
|
153
163
|
"root_matches": cas.root == cert.event_root,
|
|
154
164
|
"head_matches": log.head() == cert.head_hash,
|
|
155
165
|
"count_matches": len(log) == cert.n_events,
|
|
@@ -185,6 +195,14 @@ def verify_certificate(cert: ExecutionCertificate, log: EventLog, key: bytes | N
|
|
|
185
195
|
and str(e.payload.get("decision", "")).lower() == "allow"
|
|
186
196
|
)
|
|
187
197
|
|
|
198
|
+
# A verified receipt must carry NO data field its version does not sign. The v1
|
|
199
|
+
# body binds none of the v2 issuance fields, so a v1 receipt with issued_at /
|
|
200
|
+
# nonce / run_id set is carrying an unsigned, forgeable value (e.g. a backdated
|
|
201
|
+
# issued_at) -- reject it, so ok=True never blesses data outside the signature.
|
|
202
|
+
checks["fields_match_version"] = not (
|
|
203
|
+
cert.v == "vitnify-receipt v1"
|
|
204
|
+
and any(x is not None for x in (cert.issued_at, cert.nonce, cert.run_id)))
|
|
205
|
+
|
|
188
206
|
# Optional signer pinning -- an embedded key proves continuity, not authority.
|
|
189
207
|
# When an allow-list is supplied, the signer must be ed25519 and on the list.
|
|
190
208
|
if pinned_pubkeys is not None:
|
|
@@ -12,28 +12,39 @@ from .capability import Broker, recorded_tool_results
|
|
|
12
12
|
from .certificate import issue_certificate
|
|
13
13
|
|
|
14
14
|
class Session:
|
|
15
|
-
def __init__(self, lm, caps, tools, invariant, recorded: EventLog | None = None, prod_load=0
|
|
15
|
+
def __init__(self, lm, caps, tools, invariant, recorded: EventLog | None = None, prod_load=0,
|
|
16
|
+
provider: dict | None = None):
|
|
16
17
|
self.lm = lm
|
|
17
18
|
self.invariant = invariant
|
|
18
19
|
self.recorded = recorded
|
|
19
20
|
self.prod_load = prod_load
|
|
21
|
+
# Hosted-provider identity to bind on every llm_call (see receipt-spec: for a
|
|
22
|
+
# hosted model, recording provider/model_version/system_fingerprint/response_id
|
|
23
|
+
# is what makes provider drift distinguishable from tampering). None for a
|
|
24
|
+
# local, recomputable model.
|
|
25
|
+
self.provider = provider
|
|
20
26
|
self.log = EventLog()
|
|
21
27
|
rr = recorded_tool_results(recorded) if recorded is not None else None
|
|
22
28
|
self.broker = Broker(caps, tools, self.log, replay=rr)
|
|
23
29
|
|
|
24
|
-
def llm(self, prompt: str, n_new=8):
|
|
30
|
+
def llm(self, prompt: str, n_new=8, provider: dict | None = None):
|
|
25
31
|
load = 0 if self.recorded is not None else self.prod_load # replay runs alone
|
|
26
32
|
toks, hashes = self.lm.generate(prompt, n_new=n_new, batch_load=load, invariant=self.invariant)
|
|
27
33
|
text = self.lm.tok.decode(toks, skip_special_tokens=True)
|
|
28
|
-
|
|
29
|
-
|
|
34
|
+
payload = {"prompt_hash": hash_text(prompt), "tokens": toks,
|
|
35
|
+
"logit_hashes": hashes, "text": text}
|
|
36
|
+
prov = {**(self.provider or {}), **(provider or {})} # per-call overrides session default
|
|
37
|
+
if prov:
|
|
38
|
+
payload["provider"] = prov
|
|
39
|
+
self.log.append(Kind.LLM_CALL, payload)
|
|
30
40
|
return text
|
|
31
41
|
|
|
32
42
|
def tool(self, name: str, *args):
|
|
33
43
|
return self.broker.call(name, *args)
|
|
34
44
|
|
|
35
|
-
def record_run(agent_fn, lm, caps, tools, key, prog_hash, prod_load=8, invariant=True, priv=None
|
|
36
|
-
|
|
45
|
+
def record_run(agent_fn, lm, caps, tools, key, prog_hash, prod_load=8, invariant=True, priv=None,
|
|
46
|
+
provider: dict | None = None):
|
|
47
|
+
s = Session(lm, caps, tools, invariant, recorded=None, prod_load=prod_load, provider=provider)
|
|
37
48
|
agent_fn(s)
|
|
38
49
|
cert, _ = issue_certificate(prog_hash, caps, s.log, priv=priv, key=(None if priv else key))
|
|
39
50
|
return s.log, cert
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"""vitnify-receipt v2 -- issuance identity (F4) and hosted-provider binding (F7).
|
|
2
|
+
|
|
3
|
+
v2 adds `issued_at`, `nonce`, and `run_id` to the signed body so a receipt can be
|
|
4
|
+
placed in time and one run's receipt can't stand in for another's, and supports
|
|
5
|
+
recording hosted-provider identity on the model step so provider drift is
|
|
6
|
+
distinguishable from tampering.
|
|
7
|
+
"""
|
|
8
|
+
import copy
|
|
9
|
+
|
|
10
|
+
from vitnify.events import EventLog, Kind
|
|
11
|
+
from vitnify.certificate import issue_certificate, verify_certificate, gen_ed25519, FORMAT
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _run():
|
|
15
|
+
log = EventLog()
|
|
16
|
+
log.append(Kind.TOOL_CALL, {"tool": "read_docs", "decision": "allow"})
|
|
17
|
+
return log
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def test_receipt_is_v2_and_carries_time_nonce_runid():
|
|
21
|
+
priv, _ = gen_ed25519()
|
|
22
|
+
log = _run()
|
|
23
|
+
cert, _ = issue_certificate("prog", ["read_docs"], log, priv=priv)
|
|
24
|
+
assert cert.v == FORMAT == "vitnify-receipt v2"
|
|
25
|
+
assert cert.issued_at and cert.issued_at.endswith("Z")
|
|
26
|
+
assert cert.nonce and cert.run_id
|
|
27
|
+
body = cert.body() # all three are inside the SIGNED body
|
|
28
|
+
assert body["issued_at"] == cert.issued_at
|
|
29
|
+
assert body["nonce"] == cert.nonce
|
|
30
|
+
assert body["run_id"] == cert.run_id
|
|
31
|
+
assert verify_certificate(cert, log)["ok"] is True
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def test_run_id_is_honoured_when_supplied():
|
|
35
|
+
priv, _ = gen_ed25519()
|
|
36
|
+
cert, _ = issue_certificate("prog", ["read_docs"], _run(), priv=priv, run_id="case-42")
|
|
37
|
+
assert cert.run_id == "case-42"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def test_tampering_time_nonce_or_runid_breaks_the_signature():
|
|
41
|
+
priv, _ = gen_ed25519()
|
|
42
|
+
log = _run()
|
|
43
|
+
cert, _ = issue_certificate("prog", ["read_docs"], log, priv=priv)
|
|
44
|
+
for f in ("issued_at", "nonce", "run_id"):
|
|
45
|
+
forged = copy.deepcopy(cert)
|
|
46
|
+
setattr(forged, f, "tampered")
|
|
47
|
+
assert verify_certificate(forged, log)["ok"] is False
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def test_hosted_provider_identity_is_bound_and_drift_is_detectable():
|
|
51
|
+
# F7: recording provider identity binds it into the receipt's event root, so a
|
|
52
|
+
# later check can tell a provider change apart from tampering.
|
|
53
|
+
priv, _ = gen_ed25519()
|
|
54
|
+
log = EventLog()
|
|
55
|
+
log.append_llm_call("ph", [1, 2], seed=0, model_digest="",
|
|
56
|
+
provider={"provider": "openai", "model_version": "gpt-x-2026-01",
|
|
57
|
+
"system_fingerprint": "fp_abc", "response_id": "resp_1"})
|
|
58
|
+
cert, _ = issue_certificate("prog", [], log, priv=priv)
|
|
59
|
+
assert verify_certificate(cert, log)["ok"] is True
|
|
60
|
+
|
|
61
|
+
# A different provider fingerprint yields a different event root, so the original
|
|
62
|
+
# receipt no longer matches -- drift is detected, and the bound provider fields
|
|
63
|
+
# make it attributable to the backend rather than to forgery.
|
|
64
|
+
drifted = EventLog()
|
|
65
|
+
drifted.append_llm_call("ph", [1, 2], seed=0, model_digest="",
|
|
66
|
+
provider={"provider": "openai", "model_version": "gpt-x-2026-02",
|
|
67
|
+
"system_fingerprint": "fp_XYZ", "response_id": "resp_2"})
|
|
68
|
+
assert verify_certificate(cert, drifted)["ok"] is False
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def test_v1_receipt_still_verifies_under_the_v2_verifier():
|
|
72
|
+
# F8: a receipt validly signed under v1 (7-field body) must still verify -- a
|
|
73
|
+
# published format is frozen and the product's premise is verify-years-later.
|
|
74
|
+
from vitnify.certificate import ExecutionCertificate, _canon, _digest32
|
|
75
|
+
from vitnify._vendor.pck.cas import MerkleCAS
|
|
76
|
+
import copy
|
|
77
|
+
priv, pub = gen_ed25519()
|
|
78
|
+
log = _run()
|
|
79
|
+
cas = MerkleCAS(log.chunks())
|
|
80
|
+
v1_body = {"v": "vitnify-receipt v1", "program_hash": "prog",
|
|
81
|
+
"capabilities": ["read_docs"], "event_root": cas.root,
|
|
82
|
+
"n_events": len(log), "head_hash": log.head(),
|
|
83
|
+
"model_digests": log.model_digests()}
|
|
84
|
+
sig = priv.sign(bytes.fromhex(_digest32(_canon(v1_body).encode()))).hex()
|
|
85
|
+
cert = ExecutionCertificate("prog", ["read_docs"], cas.root, len(log), log.head(),
|
|
86
|
+
model_digests=log.model_digests())
|
|
87
|
+
cert.v, cert.sig, cert.sig_alg, cert.pubkey = "vitnify-receipt v1", sig, "ed25519", pub
|
|
88
|
+
r = verify_certificate(cert, log)
|
|
89
|
+
assert r["format"] and r["sig_valid"] and r["ok"] # v1 fully verifies
|
|
90
|
+
bad = copy.deepcopy(cert); bad.program_hash = "evil" # ...but tampering is caught
|
|
91
|
+
assert verify_certificate(bad, log)["ok"] is False
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def test_f9_session_llm_records_provider():
|
|
95
|
+
from vitnify.replayer import Session
|
|
96
|
+
|
|
97
|
+
class _Tok:
|
|
98
|
+
def decode(self, toks, skip_special_tokens=True):
|
|
99
|
+
return "text"
|
|
100
|
+
|
|
101
|
+
class _LM:
|
|
102
|
+
tok = _Tok()
|
|
103
|
+
def generate(self, prompt, n_new, batch_load, invariant):
|
|
104
|
+
return [1, 2], ["h1", "h2"]
|
|
105
|
+
|
|
106
|
+
s = Session(_LM(), caps=[], tools={}, invariant=True,
|
|
107
|
+
provider={"provider": "openai", "model_version": "gpt-x"})
|
|
108
|
+
s.llm("hi", n_new=2, provider={"response_id": "resp_1"}) # per-call merges with session default
|
|
109
|
+
ev = next(e for e in s.log.events if e.kind == Kind.LLM_CALL.value)
|
|
110
|
+
assert ev.payload["provider"] == {
|
|
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
|
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
"""vitnify-receipt v2 -- issuance identity (F4) and hosted-provider binding (F7).
|
|
2
|
-
|
|
3
|
-
v2 adds `issued_at`, `nonce`, and `run_id` to the signed body so a receipt can be
|
|
4
|
-
placed in time and one run's receipt can't stand in for another's, and supports
|
|
5
|
-
recording hosted-provider identity on the model step so provider drift is
|
|
6
|
-
distinguishable from tampering.
|
|
7
|
-
"""
|
|
8
|
-
import copy
|
|
9
|
-
|
|
10
|
-
from vitnify.events import EventLog, Kind
|
|
11
|
-
from vitnify.certificate import issue_certificate, verify_certificate, gen_ed25519, FORMAT
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
def _run():
|
|
15
|
-
log = EventLog()
|
|
16
|
-
log.append(Kind.TOOL_CALL, {"tool": "read_docs", "decision": "allow"})
|
|
17
|
-
return log
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
def test_receipt_is_v2_and_carries_time_nonce_runid():
|
|
21
|
-
priv, _ = gen_ed25519()
|
|
22
|
-
log = _run()
|
|
23
|
-
cert, _ = issue_certificate("prog", ["read_docs"], log, priv=priv)
|
|
24
|
-
assert cert.v == FORMAT == "vitnify-receipt v2"
|
|
25
|
-
assert cert.issued_at and cert.issued_at.endswith("Z")
|
|
26
|
-
assert cert.nonce and cert.run_id
|
|
27
|
-
body = cert.body() # all three are inside the SIGNED body
|
|
28
|
-
assert body["issued_at"] == cert.issued_at
|
|
29
|
-
assert body["nonce"] == cert.nonce
|
|
30
|
-
assert body["run_id"] == cert.run_id
|
|
31
|
-
assert verify_certificate(cert, log)["ok"] is True
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
def test_run_id_is_honoured_when_supplied():
|
|
35
|
-
priv, _ = gen_ed25519()
|
|
36
|
-
cert, _ = issue_certificate("prog", ["read_docs"], _run(), priv=priv, run_id="case-42")
|
|
37
|
-
assert cert.run_id == "case-42"
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
def test_tampering_time_nonce_or_runid_breaks_the_signature():
|
|
41
|
-
priv, _ = gen_ed25519()
|
|
42
|
-
log = _run()
|
|
43
|
-
cert, _ = issue_certificate("prog", ["read_docs"], log, priv=priv)
|
|
44
|
-
for f in ("issued_at", "nonce", "run_id"):
|
|
45
|
-
forged = copy.deepcopy(cert)
|
|
46
|
-
setattr(forged, f, "tampered")
|
|
47
|
-
assert verify_certificate(forged, log)["ok"] is False
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
def test_hosted_provider_identity_is_bound_and_drift_is_detectable():
|
|
51
|
-
# F7: recording provider identity binds it into the receipt's event root, so a
|
|
52
|
-
# later check can tell a provider change apart from tampering.
|
|
53
|
-
priv, _ = gen_ed25519()
|
|
54
|
-
log = EventLog()
|
|
55
|
-
log.append_llm_call("ph", [1, 2], seed=0, model_digest="",
|
|
56
|
-
provider={"provider": "openai", "model_version": "gpt-x-2026-01",
|
|
57
|
-
"system_fingerprint": "fp_abc", "response_id": "resp_1"})
|
|
58
|
-
cert, _ = issue_certificate("prog", [], log, priv=priv)
|
|
59
|
-
assert verify_certificate(cert, log)["ok"] is True
|
|
60
|
-
|
|
61
|
-
# A different provider fingerprint yields a different event root, so the original
|
|
62
|
-
# receipt no longer matches -- drift is detected, and the bound provider fields
|
|
63
|
-
# make it attributable to the backend rather than to forgery.
|
|
64
|
-
drifted = EventLog()
|
|
65
|
-
drifted.append_llm_call("ph", [1, 2], seed=0, model_digest="",
|
|
66
|
-
provider={"provider": "openai", "model_version": "gpt-x-2026-02",
|
|
67
|
-
"system_fingerprint": "fp_XYZ", "response_id": "resp_2"})
|
|
68
|
-
assert verify_certificate(cert, drifted)["ok"] is False
|
|
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
|