compartment 2.2__tar.gz → 2.3__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.
- {compartment-2.2 → compartment-2.3}/PKG-INFO +1 -1
- {compartment-2.2 → compartment-2.3}/pyproject.toml +1 -1
- {compartment-2.2 → compartment-2.3}/src/compartment/__init__.py +1 -1
- compartment-2.3/src/compartment/audit.py +103 -0
- {compartment-2.2 → compartment-2.3}/src/compartment/cli.py +21 -0
- {compartment-2.2 → compartment-2.3}/src/compartment/vault.py +7 -5
- {compartment-2.2 → compartment-2.3}/src/compartment.egg-info/PKG-INFO +1 -1
- {compartment-2.2 → compartment-2.3}/src/compartment.egg-info/SOURCES.txt +1 -0
- compartment-2.3/tests/test_audit_chain.py +123 -0
- compartment-2.2/src/compartment/audit.py +0 -54
- {compartment-2.2 → compartment-2.3}/README.md +0 -0
- {compartment-2.2 → compartment-2.3}/setup.cfg +0 -0
- {compartment-2.2 → compartment-2.3}/src/compartment/acl.py +0 -0
- {compartment-2.2 → compartment-2.3}/src/compartment/bench.py +0 -0
- {compartment-2.2 → compartment-2.3}/src/compartment/claude_hooks.py +0 -0
- {compartment-2.2 → compartment-2.3}/src/compartment/claude_memory.py +0 -0
- {compartment-2.2 → compartment-2.3}/src/compartment/crypto.py +0 -0
- {compartment-2.2 → compartment-2.3}/src/compartment/dash.py +0 -0
- {compartment-2.2 → compartment-2.3}/src/compartment/data/hermes-plugin/__init__.py +0 -0
- {compartment-2.2 → compartment-2.3}/src/compartment/data/hermes-plugin/plugin.yaml +0 -0
- {compartment-2.2 → compartment-2.3}/src/compartment/data/menubar.png +0 -0
- {compartment-2.2 → compartment-2.3}/src/compartment/data/menubar@2x.png +0 -0
- {compartment-2.2 → compartment-2.3}/src/compartment/data/starter.mpack +0 -0
- {compartment-2.2 → compartment-2.3}/src/compartment/data/tray.ico +0 -0
- {compartment-2.2 → compartment-2.3}/src/compartment/embed.py +0 -0
- {compartment-2.2 → compartment-2.3}/src/compartment/home.py +0 -0
- {compartment-2.2 → compartment-2.3}/src/compartment/longmemeval.py +0 -0
- {compartment-2.2 → compartment-2.3}/src/compartment/menubar.py +0 -0
- {compartment-2.2 → compartment-2.3}/src/compartment/models/bge-small-en-v1.5-int8/model_quantized.onnx +0 -0
- {compartment-2.2 → compartment-2.3}/src/compartment/models/bge-small-en-v1.5-int8/tokenizer.json +0 -0
- {compartment-2.2 → compartment-2.3}/src/compartment/offline_guard.py +0 -0
- {compartment-2.2 → compartment-2.3}/src/compartment/packs.py +0 -0
- {compartment-2.2 → compartment-2.3}/src/compartment/platforms.py +0 -0
- {compartment-2.2 → compartment-2.3}/src/compartment/salience.py +0 -0
- {compartment-2.2 → compartment-2.3}/src/compartment/selftest.py +0 -0
- {compartment-2.2 → compartment-2.3}/src/compartment/server.py +0 -0
- {compartment-2.2 → compartment-2.3}/src/compartment/session.py +0 -0
- {compartment-2.2 → compartment-2.3}/src/compartment/store.py +0 -0
- {compartment-2.2 → compartment-2.3}/src/compartment/systray.py +0 -0
- {compartment-2.2 → compartment-2.3}/src/compartment/vaultfile.py +0 -0
- {compartment-2.2 → compartment-2.3}/src/compartment/vindex.py +0 -0
- {compartment-2.2 → compartment-2.3}/src/compartment/wire.py +0 -0
- {compartment-2.2 → compartment-2.3}/src/compartment.egg-info/dependency_links.txt +0 -0
- {compartment-2.2 → compartment-2.3}/src/compartment.egg-info/entry_points.txt +0 -0
- {compartment-2.2 → compartment-2.3}/src/compartment.egg-info/requires.txt +0 -0
- {compartment-2.2 → compartment-2.3}/src/compartment.egg-info/top_level.txt +0 -0
- {compartment-2.2 → compartment-2.3}/tests/test_acl_audit_packs.py +0 -0
- {compartment-2.2 → compartment-2.3}/tests/test_claude_hooks_and_integrate.py +0 -0
- {compartment-2.2 → compartment-2.3}/tests/test_claude_memory_and_recent.py +0 -0
- {compartment-2.2 → compartment-2.3}/tests/test_crypto.py +0 -0
- {compartment-2.2 → compartment-2.3}/tests/test_dash.py +0 -0
- {compartment-2.2 → compartment-2.3}/tests/test_instructions.py +0 -0
- {compartment-2.2 → compartment-2.3}/tests/test_macos_bundle.py +0 -0
- {compartment-2.2 → compartment-2.3}/tests/test_menubar.py +0 -0
- {compartment-2.2 → compartment-2.3}/tests/test_offline_selftest_bench.py +0 -0
- {compartment-2.2 → compartment-2.3}/tests/test_ondisk_format.py +0 -0
- {compartment-2.2 → compartment-2.3}/tests/test_platforms.py +0 -0
- {compartment-2.2 → compartment-2.3}/tests/test_relations.py +0 -0
- {compartment-2.2 → compartment-2.3}/tests/test_salience.py +0 -0
- {compartment-2.2 → compartment-2.3}/tests/test_session_lock.py +0 -0
- {compartment-2.2 → compartment-2.3}/tests/test_starter_packs.py +0 -0
- {compartment-2.2 → compartment-2.3}/tests/test_systray.py +0 -0
- {compartment-2.2 → compartment-2.3}/tests/test_tamper.py +0 -0
- {compartment-2.2 → compartment-2.3}/tests/test_twofa.py +0 -0
- {compartment-2.2 → compartment-2.3}/tests/test_vault_ops.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: compartment
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.3
|
|
4
4
|
Summary: Compartment - high-security, fully offline, encrypted vector memory for AI agents (MCP)
|
|
5
5
|
Project-URL: Homepage, https://github.com/MaxFreedomPollard/Compartment
|
|
6
6
|
Project-URL: Source, https://github.com/MaxFreedomPollard/Compartment
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "compartment"
|
|
7
|
-
version = "2.
|
|
7
|
+
version = "2.3"
|
|
8
8
|
description = "Compartment - high-security, fully offline, encrypted vector memory for AI agents (MCP)"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.11"
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"""Hash-chained, tamper-evident audit log (stored inside the sealed payload).
|
|
2
|
+
|
|
3
|
+
Each entry's hash covers the previous entry's hash, so any edit, deletion,
|
|
4
|
+
or reordering of history breaks the chain at a detectable point.
|
|
5
|
+
A failure to write the audit entry fails the operation (fail-fast), never
|
|
6
|
+
the other way around.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import hashlib
|
|
11
|
+
import json
|
|
12
|
+
import time
|
|
13
|
+
|
|
14
|
+
GENESIS = "GENESIS"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _entry_hash(prev_hash: str, ts: float, caller: str, op: str, detail: str) -> str:
|
|
18
|
+
body = json.dumps(
|
|
19
|
+
{"prev": prev_hash, "ts": ts, "caller": caller, "op": op, "detail": detail},
|
|
20
|
+
sort_keys=True, separators=(",", ":"),
|
|
21
|
+
).encode()
|
|
22
|
+
return hashlib.sha256(body).hexdigest()
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def append(conn, caller: str, op: str, detail: str, ts: float | None = None) -> str:
|
|
26
|
+
"""Add an entry, linked to whatever is currently at the head.
|
|
27
|
+
|
|
28
|
+
`ts` is only passed when replaying a journalled operation, so the entry
|
|
29
|
+
keeps the time it actually happened. It is still linked to the head as it
|
|
30
|
+
stands at replay time, never to the head the original writer saw: that
|
|
31
|
+
head may never have reached disk (read operations are audited in RAM and
|
|
32
|
+
persist only on save), and chaining to an entry nobody else has is what
|
|
33
|
+
leaves a dangling link no future verify can resolve."""
|
|
34
|
+
row = conn.execute("SELECT hash FROM audit ORDER BY seq DESC LIMIT 1").fetchone()
|
|
35
|
+
prev = row["hash"] if row else GENESIS
|
|
36
|
+
ts = time.time() if ts is None else ts
|
|
37
|
+
h = _entry_hash(prev, ts, caller, op, detail)
|
|
38
|
+
conn.execute(
|
|
39
|
+
"INSERT INTO audit (ts, caller, op, detail, prev_hash, hash) VALUES (?,?,?,?,?,?)",
|
|
40
|
+
(ts, caller, op, detail, prev, h),
|
|
41
|
+
)
|
|
42
|
+
return h
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def head(conn) -> str:
|
|
46
|
+
row = conn.execute("SELECT hash FROM audit ORDER BY seq DESC LIMIT 1").fetchone()
|
|
47
|
+
return row["hash"] if row else GENESIS
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def verify(conn) -> tuple[bool, int, str]:
|
|
51
|
+
"""Walk the chain. Returns (ok, entries_checked, message)."""
|
|
52
|
+
prev = GENESIS
|
|
53
|
+
n = 0
|
|
54
|
+
for row in conn.execute("SELECT * FROM audit ORDER BY seq"):
|
|
55
|
+
if row["prev_hash"] != prev:
|
|
56
|
+
return False, n, f"chain break at seq {row['seq']}: prev_hash mismatch"
|
|
57
|
+
want = _entry_hash(prev, row["ts"], row["caller"], row["op"], row["detail"])
|
|
58
|
+
if row["hash"] != want:
|
|
59
|
+
return False, n, f"chain break at seq {row['seq']}: entry hash mismatch"
|
|
60
|
+
prev = row["hash"]
|
|
61
|
+
n += 1
|
|
62
|
+
return True, n, f"audit chain intact ({n} entries)"
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def relink(conn) -> tuple[int, int | None]:
|
|
66
|
+
"""Re-link entries whose prev_hash points at something not in the log.
|
|
67
|
+
|
|
68
|
+
Only for repairing damage done by builds before this one, which replayed a
|
|
69
|
+
journalled entry with the head its original writer saw rather than the head
|
|
70
|
+
the log actually has. The result was a permanent dangling link: verify
|
|
71
|
+
stopped at it and never reported anything after it again.
|
|
72
|
+
|
|
73
|
+
What this does NOT do is paper over an edit. Every entry's own hash covers
|
|
74
|
+
its content, so content tampering is caught by the self-hash check and this
|
|
75
|
+
refuses to touch a log that fails it. Only the links are rebuilt, and only
|
|
76
|
+
forward from the first break. Nothing is deleted, reordered or reworded:
|
|
77
|
+
ts, caller, op and detail are exactly what they were.
|
|
78
|
+
|
|
79
|
+
Returns (entries relinked, seq of the first break) - (0, None) if intact."""
|
|
80
|
+
rows = conn.execute("SELECT * FROM audit ORDER BY seq").fetchall()
|
|
81
|
+
for row in rows:
|
|
82
|
+
want = _entry_hash(row["prev_hash"], row["ts"], row["caller"], row["op"],
|
|
83
|
+
row["detail"])
|
|
84
|
+
if row["hash"] != want:
|
|
85
|
+
raise ValueError(
|
|
86
|
+
f"audit entry seq {row['seq']} does not hash to its own content. "
|
|
87
|
+
"That is content tampering, not a dangling link, and relinking "
|
|
88
|
+
"would destroy the evidence. Refusing.")
|
|
89
|
+
|
|
90
|
+
prev, first, changed = GENESIS, None, 0
|
|
91
|
+
for row in rows:
|
|
92
|
+
if row["prev_hash"] != prev:
|
|
93
|
+
if first is None:
|
|
94
|
+
first = row["seq"]
|
|
95
|
+
if first is not None: # rewrite everything after it
|
|
96
|
+
h = _entry_hash(prev, row["ts"], row["caller"], row["op"], row["detail"])
|
|
97
|
+
conn.execute("UPDATE audit SET prev_hash = ?, hash = ? WHERE seq = ?",
|
|
98
|
+
(prev, h, row["seq"]))
|
|
99
|
+
changed += 1
|
|
100
|
+
prev = h
|
|
101
|
+
else:
|
|
102
|
+
prev = row["hash"]
|
|
103
|
+
return changed, first
|
|
@@ -468,6 +468,25 @@ def cmd_audit(args) -> None:
|
|
|
468
468
|
sys.exit(2)
|
|
469
469
|
|
|
470
470
|
|
|
471
|
+
def cmd_audit_repair(args) -> None:
|
|
472
|
+
v = _open_vault(args)
|
|
473
|
+
try:
|
|
474
|
+
changed, first = audit.relink(v.db.conn)
|
|
475
|
+
except ValueError as exc:
|
|
476
|
+
_die(str(exc))
|
|
477
|
+
if not changed:
|
|
478
|
+
_print({"ok": True, "relinked": 0, "message": "audit chain already intact"})
|
|
479
|
+
return
|
|
480
|
+
v._audit_and_capture("user", "audit-repair",
|
|
481
|
+
f"relinked {changed} entries from seq {first}")
|
|
482
|
+
v.save()
|
|
483
|
+
ok, n, msg = audit.verify(v.db.conn)
|
|
484
|
+
_print({"ok": ok, "relinked": changed, "first_break_at_seq": first,
|
|
485
|
+
"entries": n, "message": msg, "head": audit.head(v.db.conn)})
|
|
486
|
+
if not ok:
|
|
487
|
+
sys.exit(2)
|
|
488
|
+
|
|
489
|
+
|
|
471
490
|
def cmd_verify(args) -> None:
|
|
472
491
|
loaded = read_vault_file(args.vault) # structure + format checks
|
|
473
492
|
out = {"vault": args.vault, "format": "ok",
|
|
@@ -1165,6 +1184,8 @@ def main(argv: list[str] | None = None) -> None:
|
|
|
1165
1184
|
pa_sub = pa.add_subparsers(dest="audit_cmd", required=True)
|
|
1166
1185
|
p = pa_sub.add_parser("verify", help="verify the hash chain")
|
|
1167
1186
|
p.set_defaults(fn=cmd_audit)
|
|
1187
|
+
p = pa_sub.add_parser("repair", help="re-link a chain broken by an older build")
|
|
1188
|
+
p.set_defaults(fn=cmd_audit_repair)
|
|
1168
1189
|
|
|
1169
1190
|
p = sub.add_parser("verify", help="check vault structure + signed manifest")
|
|
1170
1191
|
p.set_defaults(fn=cmd_verify)
|
|
@@ -359,12 +359,14 @@ class Vault:
|
|
|
359
359
|
self.db.delete_relation(e["id"])
|
|
360
360
|
else:
|
|
361
361
|
raise TamperError(f"Unknown journal op {op!r}")
|
|
362
|
+
# Link to the head this log actually has, keeping the original time.
|
|
363
|
+
# The writer's own head may never have reached disk - a read is audited
|
|
364
|
+
# in RAM and persists only on save - so replaying its prev_hash
|
|
365
|
+
# verbatim leaves a link pointing at an entry no one else has, and
|
|
366
|
+
# verify stops there permanently. The journal entry's contents are
|
|
367
|
+
# already AEAD-authenticated, so nothing is lost by re-linking.
|
|
362
368
|
a = e["audit"]
|
|
363
|
-
self.db.conn
|
|
364
|
-
"INSERT INTO audit (ts, caller, op, detail, prev_hash, hash)"
|
|
365
|
-
" VALUES (?,?,?,?,?,?)",
|
|
366
|
-
(a["ts"], a["caller"], a["op"], a["detail"], a["prev_hash"], a["hash"]),
|
|
367
|
-
)
|
|
369
|
+
audit.append(self.db.conn, a["caller"], a["op"], a["detail"], ts=a["ts"])
|
|
368
370
|
|
|
369
371
|
def _audit_and_capture(self, caller: str, op: str, detail: str) -> dict:
|
|
370
372
|
audit.append(self.db.conn, caller, op, detail)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: compartment
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.3
|
|
4
4
|
Summary: Compartment - high-security, fully offline, encrypted vector memory for AI agents (MCP)
|
|
5
5
|
Project-URL: Homepage, https://github.com/MaxFreedomPollard/Compartment
|
|
6
6
|
Project-URL: Source, https://github.com/MaxFreedomPollard/Compartment
|
|
@@ -41,6 +41,7 @@ src/compartment/data/hermes-plugin/plugin.yaml
|
|
|
41
41
|
src/compartment/models/bge-small-en-v1.5-int8/model_quantized.onnx
|
|
42
42
|
src/compartment/models/bge-small-en-v1.5-int8/tokenizer.json
|
|
43
43
|
tests/test_acl_audit_packs.py
|
|
44
|
+
tests/test_audit_chain.py
|
|
44
45
|
tests/test_claude_hooks_and_integrate.py
|
|
45
46
|
tests/test_claude_memory_and_recent.py
|
|
46
47
|
tests/test_crypto.py
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"""The audit chain has to survive ordinary multi-process use.
|
|
2
|
+
|
|
3
|
+
It did not. A write is journalled the moment it happens; a read is audited in
|
|
4
|
+
RAM and only reaches disk on the next save. So a `store` would chain off a
|
|
5
|
+
`search` entry that no other process had, its journal entry was replayed
|
|
6
|
+
verbatim into a log missing that entry, and the link dangled. verify() stopped
|
|
7
|
+
there and never reported on anything after it again.
|
|
8
|
+
|
|
9
|
+
That is not a hypothetical either: it happened five times in one day on a real
|
|
10
|
+
vault, every one of them a `store` from a long-running MCP server.
|
|
11
|
+
"""
|
|
12
|
+
import pytest
|
|
13
|
+
|
|
14
|
+
from compartment import audit, store, vaultfile
|
|
15
|
+
from compartment.vault import Vault
|
|
16
|
+
|
|
17
|
+
from conftest import PASS
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _audit_rows(conn):
|
|
21
|
+
return conn.execute("SELECT * FROM audit ORDER BY seq").fetchall()
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def test_replaying_a_journalled_write_keeps_the_chain_linked(vault_path):
|
|
25
|
+
"""The regression. A read between saves used to poison the next write."""
|
|
26
|
+
v = Vault.create(vault_path, PASS, creator="test")
|
|
27
|
+
v.save()
|
|
28
|
+
|
|
29
|
+
# a read: audited in RAM, deliberately not journalled and not saved
|
|
30
|
+
v.search("anything", caller="test")
|
|
31
|
+
# a write: journalled immediately, chaining off that RAM-only entry
|
|
32
|
+
v.store("a memory written after a search", caller="test")
|
|
33
|
+
# crash before save, so the payload never learns about either entry
|
|
34
|
+
del v
|
|
35
|
+
|
|
36
|
+
reopened = Vault.unlock(vault_path, passphrase=PASS)
|
|
37
|
+
ok, n, msg = audit.verify(reopened.db.conn)
|
|
38
|
+
assert ok, msg
|
|
39
|
+
texts = {reopened.db.decrypt_text(r, reopened._master)
|
|
40
|
+
for r in reopened.db.conn.execute("SELECT * FROM records")}
|
|
41
|
+
assert "a memory written after a search" in texts
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def test_replay_preserves_when_the_operation_happened(vault_path):
|
|
45
|
+
v = Vault.create(vault_path, PASS, creator="test")
|
|
46
|
+
v.save()
|
|
47
|
+
v.store("timed", caller="test")
|
|
48
|
+
stored_at = _audit_rows(v.db.conn)[-1]["ts"]
|
|
49
|
+
del v
|
|
50
|
+
|
|
51
|
+
reopened = Vault.unlock(vault_path, passphrase=PASS)
|
|
52
|
+
replayed = [r for r in _audit_rows(reopened.db.conn) if r["op"] == "store"][-1]
|
|
53
|
+
assert replayed["ts"] == stored_at, "replay must not restamp history"
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def test_append_links_to_the_head_it_can_see(vault_path):
|
|
57
|
+
v = Vault.create(vault_path, PASS, creator="test")
|
|
58
|
+
conn = v.db.conn
|
|
59
|
+
audit.append(conn, "a", "op1", "one")
|
|
60
|
+
head = audit.head(conn)
|
|
61
|
+
audit.append(conn, "b", "op2", "two", ts=123.0)
|
|
62
|
+
last = _audit_rows(conn)[-1]
|
|
63
|
+
assert last["prev_hash"] == head
|
|
64
|
+
assert last["ts"] == 123.0
|
|
65
|
+
ok, _, msg = audit.verify(conn)
|
|
66
|
+
assert ok, msg
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
# ---------------------------------------------------------------------------
|
|
70
|
+
# Repairing a log an older build already broke
|
|
71
|
+
# ---------------------------------------------------------------------------
|
|
72
|
+
|
|
73
|
+
def _break_the_chain(conn):
|
|
74
|
+
"""Exactly the old damage: a dangling prev_hash, contents untouched."""
|
|
75
|
+
rows = _audit_rows(conn)
|
|
76
|
+
victim = rows[len(rows) // 2]
|
|
77
|
+
bad = "f" * 64
|
|
78
|
+
h = audit._entry_hash(bad, victim["ts"], victim["caller"], victim["op"],
|
|
79
|
+
victim["detail"])
|
|
80
|
+
conn.execute("UPDATE audit SET prev_hash = ?, hash = ? WHERE seq = ?",
|
|
81
|
+
(bad, h, victim["seq"]))
|
|
82
|
+
return victim["seq"]
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def test_relink_repairs_a_dangling_link_and_keeps_every_entry(vault):
|
|
86
|
+
conn = vault.db.conn
|
|
87
|
+
for i in range(6):
|
|
88
|
+
audit.append(conn, "test", "store", f"entry {i}")
|
|
89
|
+
before = [(r["ts"], r["caller"], r["op"], r["detail"]) for r in _audit_rows(conn)]
|
|
90
|
+
|
|
91
|
+
broken_at = _break_the_chain(conn)
|
|
92
|
+
assert not audit.verify(conn)[0]
|
|
93
|
+
|
|
94
|
+
changed, first = audit.relink(conn)
|
|
95
|
+
assert first == broken_at
|
|
96
|
+
assert changed >= 1
|
|
97
|
+
ok, n, msg = audit.verify(conn)
|
|
98
|
+
assert ok, msg
|
|
99
|
+
assert n == len(before)
|
|
100
|
+
|
|
101
|
+
after = [(r["ts"], r["caller"], r["op"], r["detail"]) for r in _audit_rows(conn)]
|
|
102
|
+
assert after == before, "relink must not alter what the log says"
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def test_relink_is_a_no_op_on_an_intact_chain(vault):
|
|
106
|
+
conn = vault.db.conn
|
|
107
|
+
audit.append(conn, "test", "store", "x")
|
|
108
|
+
assert audit.relink(conn) == (0, None)
|
|
109
|
+
assert audit.verify(conn)[0]
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def test_relink_refuses_to_hide_edited_content(vault):
|
|
113
|
+
"""The distinction that matters: a dangling link is damage, a reworded
|
|
114
|
+
entry is tampering, and relinking the second would erase the proof."""
|
|
115
|
+
conn = vault.db.conn
|
|
116
|
+
audit.append(conn, "test", "forget", "id=secret-thing")
|
|
117
|
+
rows = _audit_rows(conn)
|
|
118
|
+
conn.execute("UPDATE audit SET detail = ? WHERE seq = ?",
|
|
119
|
+
("id=something-harmless", rows[-1]["seq"]))
|
|
120
|
+
|
|
121
|
+
with pytest.raises(ValueError, match="content tampering"):
|
|
122
|
+
audit.relink(conn)
|
|
123
|
+
assert not audit.verify(conn)[0], "and it stays broken, as it should"
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
"""Hash-chained, tamper-evident audit log (stored inside the sealed payload).
|
|
2
|
-
|
|
3
|
-
Each entry's hash covers the previous entry's hash, so any edit, deletion,
|
|
4
|
-
or reordering of history breaks the chain at a detectable point.
|
|
5
|
-
A failure to write the audit entry fails the operation (fail-fast), never
|
|
6
|
-
the other way around.
|
|
7
|
-
"""
|
|
8
|
-
from __future__ import annotations
|
|
9
|
-
|
|
10
|
-
import hashlib
|
|
11
|
-
import json
|
|
12
|
-
import time
|
|
13
|
-
|
|
14
|
-
GENESIS = "GENESIS"
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
def _entry_hash(prev_hash: str, ts: float, caller: str, op: str, detail: str) -> str:
|
|
18
|
-
body = json.dumps(
|
|
19
|
-
{"prev": prev_hash, "ts": ts, "caller": caller, "op": op, "detail": detail},
|
|
20
|
-
sort_keys=True, separators=(",", ":"),
|
|
21
|
-
).encode()
|
|
22
|
-
return hashlib.sha256(body).hexdigest()
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
def append(conn, caller: str, op: str, detail: str) -> str:
|
|
26
|
-
row = conn.execute("SELECT hash FROM audit ORDER BY seq DESC LIMIT 1").fetchone()
|
|
27
|
-
prev = row["hash"] if row else GENESIS
|
|
28
|
-
ts = time.time()
|
|
29
|
-
h = _entry_hash(prev, ts, caller, op, detail)
|
|
30
|
-
conn.execute(
|
|
31
|
-
"INSERT INTO audit (ts, caller, op, detail, prev_hash, hash) VALUES (?,?,?,?,?,?)",
|
|
32
|
-
(ts, caller, op, detail, prev, h),
|
|
33
|
-
)
|
|
34
|
-
return h
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
def head(conn) -> str:
|
|
38
|
-
row = conn.execute("SELECT hash FROM audit ORDER BY seq DESC LIMIT 1").fetchone()
|
|
39
|
-
return row["hash"] if row else GENESIS
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
def verify(conn) -> tuple[bool, int, str]:
|
|
43
|
-
"""Walk the chain. Returns (ok, entries_checked, message)."""
|
|
44
|
-
prev = GENESIS
|
|
45
|
-
n = 0
|
|
46
|
-
for row in conn.execute("SELECT * FROM audit ORDER BY seq"):
|
|
47
|
-
if row["prev_hash"] != prev:
|
|
48
|
-
return False, n, f"chain break at seq {row['seq']}: prev_hash mismatch"
|
|
49
|
-
want = _entry_hash(prev, row["ts"], row["caller"], row["op"], row["detail"])
|
|
50
|
-
if row["hash"] != want:
|
|
51
|
-
return False, n, f"chain break at seq {row['seq']}: entry hash mismatch"
|
|
52
|
-
prev = row["hash"]
|
|
53
|
-
n += 1
|
|
54
|
-
return True, n, f"audit chain intact ({n} entries)"
|
|
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
|
{compartment-2.2 → compartment-2.3}/src/compartment/models/bge-small-en-v1.5-int8/tokenizer.json
RENAMED
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|