compartment 3.2__tar.gz → 3.4__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-3.2 → compartment-3.4}/PKG-INFO +6 -1
- {compartment-3.2 → compartment-3.4}/README.md +5 -0
- {compartment-3.2 → compartment-3.4}/pyproject.toml +1 -1
- {compartment-3.2 → compartment-3.4}/src/compartment/__init__.py +1 -1
- {compartment-3.2 → compartment-3.4}/src/compartment/acl.py +1 -0
- {compartment-3.2 → compartment-3.4}/src/compartment/cli.py +31 -11
- {compartment-3.2 → compartment-3.4}/src/compartment/menubar.py +82 -3
- {compartment-3.2 → compartment-3.4}/src/compartment/systray.py +41 -7
- {compartment-3.2 → compartment-3.4}/src/compartment/vault.py +18 -1
- {compartment-3.2 → compartment-3.4}/src/compartment.egg-info/PKG-INFO +6 -1
- {compartment-3.2 → compartment-3.4}/src/compartment.egg-info/SOURCES.txt +2 -0
- compartment-3.4/tests/test_connect_buttons.py +165 -0
- {compartment-3.2 → compartment-3.4}/tests/test_menubar.py +1 -1
- compartment-3.4/tests/test_starter_visibility.py +197 -0
- {compartment-3.2 → compartment-3.4}/tests/test_systray.py +2 -1
- {compartment-3.2 → compartment-3.4}/setup.cfg +0 -0
- {compartment-3.2 → compartment-3.4}/src/compartment/audit.py +0 -0
- {compartment-3.2 → compartment-3.4}/src/compartment/bench.py +0 -0
- {compartment-3.2 → compartment-3.4}/src/compartment/claude_hooks.py +0 -0
- {compartment-3.2 → compartment-3.4}/src/compartment/claude_memory.py +0 -0
- {compartment-3.2 → compartment-3.4}/src/compartment/crypto.py +0 -0
- {compartment-3.2 → compartment-3.4}/src/compartment/dash.py +0 -0
- {compartment-3.2 → compartment-3.4}/src/compartment/data/hermes-plugin/__init__.py +0 -0
- {compartment-3.2 → compartment-3.4}/src/compartment/data/hermes-plugin/plugin.yaml +0 -0
- {compartment-3.2 → compartment-3.4}/src/compartment/data/menubar.png +0 -0
- {compartment-3.2 → compartment-3.4}/src/compartment/data/menubar@2x.png +0 -0
- {compartment-3.2 → compartment-3.4}/src/compartment/data/starter.mpack +0 -0
- {compartment-3.2 → compartment-3.4}/src/compartment/data/tray.ico +0 -0
- {compartment-3.2 → compartment-3.4}/src/compartment/embed.py +0 -0
- {compartment-3.2 → compartment-3.4}/src/compartment/home.py +0 -0
- {compartment-3.2 → compartment-3.4}/src/compartment/longmemeval.py +0 -0
- {compartment-3.2 → compartment-3.4}/src/compartment/models/bge-small-en-v1.5-int8/model_quantized.onnx +0 -0
- {compartment-3.2 → compartment-3.4}/src/compartment/models/bge-small-en-v1.5-int8/tokenizer.json +0 -0
- {compartment-3.2 → compartment-3.4}/src/compartment/offline_guard.py +0 -0
- {compartment-3.2 → compartment-3.4}/src/compartment/packs.py +0 -0
- {compartment-3.2 → compartment-3.4}/src/compartment/platforms.py +0 -0
- {compartment-3.2 → compartment-3.4}/src/compartment/salience.py +0 -0
- {compartment-3.2 → compartment-3.4}/src/compartment/selftest.py +0 -0
- {compartment-3.2 → compartment-3.4}/src/compartment/server.py +0 -0
- {compartment-3.2 → compartment-3.4}/src/compartment/session.py +0 -0
- {compartment-3.2 → compartment-3.4}/src/compartment/store.py +0 -0
- {compartment-3.2 → compartment-3.4}/src/compartment/vaultfile.py +0 -0
- {compartment-3.2 → compartment-3.4}/src/compartment/vindex.py +0 -0
- {compartment-3.2 → compartment-3.4}/src/compartment/wire.py +0 -0
- {compartment-3.2 → compartment-3.4}/src/compartment.egg-info/dependency_links.txt +0 -0
- {compartment-3.2 → compartment-3.4}/src/compartment.egg-info/entry_points.txt +0 -0
- {compartment-3.2 → compartment-3.4}/src/compartment.egg-info/requires.txt +0 -0
- {compartment-3.2 → compartment-3.4}/src/compartment.egg-info/top_level.txt +0 -0
- {compartment-3.2 → compartment-3.4}/tests/test_acl_audit_packs.py +0 -0
- {compartment-3.2 → compartment-3.4}/tests/test_acl_precedence.py +0 -0
- {compartment-3.2 → compartment-3.4}/tests/test_attempt_pacing.py +0 -0
- {compartment-3.2 → compartment-3.4}/tests/test_audit_chain.py +0 -0
- {compartment-3.2 → compartment-3.4}/tests/test_claude_hooks_and_integrate.py +0 -0
- {compartment-3.2 → compartment-3.4}/tests/test_claude_memory_and_recent.py +0 -0
- {compartment-3.2 → compartment-3.4}/tests/test_crypto.py +0 -0
- {compartment-3.2 → compartment-3.4}/tests/test_dash.py +0 -0
- {compartment-3.2 → compartment-3.4}/tests/test_instructions.py +0 -0
- {compartment-3.2 → compartment-3.4}/tests/test_journal_framing.py +0 -0
- {compartment-3.2 → compartment-3.4}/tests/test_macos_bundle.py +0 -0
- {compartment-3.2 → compartment-3.4}/tests/test_offline_guard_paths.py +0 -0
- {compartment-3.2 → compartment-3.4}/tests/test_offline_selftest_bench.py +0 -0
- {compartment-3.2 → compartment-3.4}/tests/test_ondisk_format.py +0 -0
- {compartment-3.2 → compartment-3.4}/tests/test_pack_trust.py +0 -0
- {compartment-3.2 → compartment-3.4}/tests/test_platforms.py +0 -0
- {compartment-3.2 → compartment-3.4}/tests/test_relations.py +0 -0
- {compartment-3.2 → compartment-3.4}/tests/test_salience.py +0 -0
- {compartment-3.2 → compartment-3.4}/tests/test_session_entropy.py +0 -0
- {compartment-3.2 → compartment-3.4}/tests/test_session_lock.py +0 -0
- {compartment-3.2 → compartment-3.4}/tests/test_starter_data_quality.py +0 -0
- {compartment-3.2 → compartment-3.4}/tests/test_starter_packs.py +0 -0
- {compartment-3.2 → compartment-3.4}/tests/test_status_bar_install.py +0 -0
- {compartment-3.2 → compartment-3.4}/tests/test_tamper.py +0 -0
- {compartment-3.2 → compartment-3.4}/tests/test_twofa.py +0 -0
- {compartment-3.2 → compartment-3.4}/tests/test_vault_ops.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: compartment
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.4
|
|
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
|
|
@@ -243,6 +243,11 @@ network at all.
|
|
|
243
243
|
One command per platform. Each installs the package, creates your
|
|
244
244
|
encrypted vault, and wires the agent.
|
|
245
245
|
|
|
246
|
+
Every one of these is also a button in the app. Click the Compartment icon
|
|
247
|
+
in your menu bar or notification area, and under **CONNECT AN AGENT** press
|
|
248
|
+
Claude, Hermes or OpenClaw. The button runs the same `compartment integrate`
|
|
249
|
+
command for you, so nobody has to open a terminal a second time.
|
|
250
|
+
|
|
246
251
|
**Claude (Code + Desktop)** - macOS / Linux:
|
|
247
252
|
```bash
|
|
248
253
|
pip install compartment && compartment init && compartment integrate claude
|
|
@@ -197,6 +197,11 @@ network at all.
|
|
|
197
197
|
One command per platform. Each installs the package, creates your
|
|
198
198
|
encrypted vault, and wires the agent.
|
|
199
199
|
|
|
200
|
+
Every one of these is also a button in the app. Click the Compartment icon
|
|
201
|
+
in your menu bar or notification area, and under **CONNECT AN AGENT** press
|
|
202
|
+
Claude, Hermes or OpenClaw. The button runs the same `compartment integrate`
|
|
203
|
+
command for you, so nobody has to open a terminal a second time.
|
|
204
|
+
|
|
200
205
|
**Claude (Code + Desktop)** - macOS / Linux:
|
|
201
206
|
```bash
|
|
202
207
|
pip install compartment && compartment init && compartment integrate claude
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "compartment"
|
|
7
|
-
version = "3.
|
|
7
|
+
version = "3.4"
|
|
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"
|
|
@@ -86,11 +86,23 @@ def _pack_bytes(name: str) -> bytes | None:
|
|
|
86
86
|
return p.read_bytes() if p.is_file() else None
|
|
87
87
|
|
|
88
88
|
|
|
89
|
-
def
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
89
|
+
def _seed_blobs() -> list[tuple[str, bytes]]:
|
|
90
|
+
"""The starting memories, or a hard failure.
|
|
91
|
+
|
|
92
|
+
They are part of every install, command line or app, so an install that
|
|
93
|
+
cannot produce them is broken and has to say so. Returning nothing here
|
|
94
|
+
would create a vault that is empty but looks finished, which is the one
|
|
95
|
+
outcome a new user cannot diagnose."""
|
|
96
|
+
out = []
|
|
97
|
+
for name in _starter_pack_names():
|
|
98
|
+
blob = _pack_bytes(name)
|
|
99
|
+
if blob is None:
|
|
100
|
+
_die(f"this install is incomplete: {name}.mpack is missing from "
|
|
101
|
+
f"{_data_dir()}. The starting memories ship with every "
|
|
102
|
+
"install - reinstall Compartment rather than start with an "
|
|
103
|
+
"empty vault.")
|
|
104
|
+
out.append((name, blob))
|
|
105
|
+
return out
|
|
94
106
|
|
|
95
107
|
|
|
96
108
|
def _starter_pack_names() -> list[str]:
|
|
@@ -112,6 +124,9 @@ def cmd_init(args) -> None:
|
|
|
112
124
|
Path(path).parent.mkdir(parents=True, exist_ok=True)
|
|
113
125
|
if os.path.exists(path):
|
|
114
126
|
_die(f"{path} already exists - Compartment never overwrites a vault")
|
|
127
|
+
# Before a passphrase is chosen and before anything reaches disk: a broken
|
|
128
|
+
# install fails here, with nothing to clean up.
|
|
129
|
+
seeds = _seed_blobs()
|
|
115
130
|
print(f"Compartment {__version__} - creating vault: {path}")
|
|
116
131
|
print(f"Embedding model: {DEFAULT_MODEL} (bundled, offline)")
|
|
117
132
|
if args.passphrase:
|
|
@@ -131,10 +146,7 @@ def cmd_init(args) -> None:
|
|
|
131
146
|
print("(Optional second factor: `compartment 2fa enable` - see README.)")
|
|
132
147
|
print("\nFinishing vault setup (offline)…")
|
|
133
148
|
total = 0
|
|
134
|
-
for
|
|
135
|
-
blob = _pack_bytes(name)
|
|
136
|
-
if blob is None:
|
|
137
|
-
continue
|
|
149
|
+
for _, blob in seeds:
|
|
138
150
|
out = packs.seed_records(v, blob, caller=args.creator)
|
|
139
151
|
total += out["records"]
|
|
140
152
|
nrec = out["records"]
|
|
@@ -450,8 +462,16 @@ def cmd_recent(args) -> None:
|
|
|
450
462
|
print(f"{counts['total']} records | {counts['organic']} organic "
|
|
451
463
|
f"(stored during use) | {counts['seeded']} seeded")
|
|
452
464
|
if not out["results"]:
|
|
453
|
-
|
|
454
|
-
|
|
465
|
+
# A fresh vault is not an empty one. Saying "no memories" to
|
|
466
|
+
# someone who just watched several thousand load is how a working
|
|
467
|
+
# install gets mistaken for a broken one.
|
|
468
|
+
if counts["seeded"] and not args.all:
|
|
469
|
+
print(f"\nnothing stored during use yet. The "
|
|
470
|
+
f"{counts['seeded']:,} starting memories that came with "
|
|
471
|
+
f"the vault are loaded and searchable - list them with "
|
|
472
|
+
f"`compartment recent --all`.")
|
|
473
|
+
else:
|
|
474
|
+
print("\nno memories stored yet")
|
|
455
475
|
else:
|
|
456
476
|
print(f"\n{len(out['results'])} most recent, oldest first:\n")
|
|
457
477
|
for r in out["results"]:
|
|
@@ -146,7 +146,7 @@ def read_settings(vault: str) -> dict:
|
|
|
146
146
|
return {
|
|
147
147
|
"capture_hook": hook,
|
|
148
148
|
"search_starter_facts": bool(
|
|
149
|
-
cfg.settings.get("
|
|
149
|
+
cfg.settings.get("search_starter_facts", True)),
|
|
150
150
|
"auto_lock_minutes": int(cfg.settings.get("auto_lock_minutes", 30)),
|
|
151
151
|
}
|
|
152
152
|
|
|
@@ -162,7 +162,7 @@ def set_setting(vault: str, key: str, value) -> dict:
|
|
|
162
162
|
return read_settings(vault)
|
|
163
163
|
cfg = VaultConfig.load(vault)
|
|
164
164
|
if key == "search_starter_facts":
|
|
165
|
-
cfg.settings["
|
|
165
|
+
cfg.settings["search_starter_facts"] = bool(value)
|
|
166
166
|
elif key == "auto_lock_minutes":
|
|
167
167
|
cfg.settings["auto_lock_minutes"] = int(value)
|
|
168
168
|
else:
|
|
@@ -279,6 +279,40 @@ def change_passphrase(vault: str, new: str, repeat: str) -> tuple[bool, str]:
|
|
|
279
279
|
return False, (out[:120] or "could not change the passphrase")
|
|
280
280
|
|
|
281
281
|
|
|
282
|
+
#: The agents `compartment integrate` can wire, in the order they are offered.
|
|
283
|
+
#: Kept here rather than in the UI so the menu bar and the tray cannot drift
|
|
284
|
+
#: apart, and so adding a target is one edit.
|
|
285
|
+
INTEGRATION_TARGETS = (("claude", "Claude"),
|
|
286
|
+
("hermes", "Hermes"),
|
|
287
|
+
("openclaw", "OpenClaw"))
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def integrate(vault: str, target: str) -> tuple[bool, str]:
|
|
291
|
+
"""Connect an agent to this vault, from the panel. Returns (ok, message).
|
|
292
|
+
|
|
293
|
+
The same command the README gives, run for you. It is offered here
|
|
294
|
+
because the terminal step after installing is where people stop: the
|
|
295
|
+
vault exists, the icon is in the menu bar, and nothing is using it yet
|
|
296
|
+
because nobody knew there was a second command.
|
|
297
|
+
|
|
298
|
+
Wiring an agent edits that agent's own configuration, so this reports
|
|
299
|
+
what happened rather than assuming - a Claude Code CLI that is not
|
|
300
|
+
installed is a normal outcome, not an error to hide.
|
|
301
|
+
"""
|
|
302
|
+
if target not in dict(INTEGRATION_TARGETS):
|
|
303
|
+
return False, f"unknown target {target!r}"
|
|
304
|
+
code, out = _run([*_cli_argv(), "--vault", vault, "integrate", target],
|
|
305
|
+
timeout=300)
|
|
306
|
+
text = " ".join(out.split())
|
|
307
|
+
if code != 0:
|
|
308
|
+
return False, (text[:200] or f"could not connect {target}")
|
|
309
|
+
name = dict(INTEGRATION_TARGETS)[target]
|
|
310
|
+
if "not found" in text.lower():
|
|
311
|
+
return True, (f"{name} is not installed on this machine yet. "
|
|
312
|
+
"Compartment is ready for it when it is.")
|
|
313
|
+
return True, f"{name} is connected. Restart it to pick up the change."
|
|
314
|
+
|
|
315
|
+
|
|
282
316
|
def summarise(state: dict) -> str:
|
|
283
317
|
"""One line under the title. Also what --self-check prints."""
|
|
284
318
|
if not state["exists"]:
|
|
@@ -289,6 +323,19 @@ def summarise(state: dict) -> str:
|
|
|
289
323
|
f"{state['organic']:,} stored by you")
|
|
290
324
|
|
|
291
325
|
|
|
326
|
+
def starter_note(state: dict) -> str:
|
|
327
|
+
"""What the recent list says when nothing has been stored during use.
|
|
328
|
+
|
|
329
|
+
A new vault holds thousands of starting memories and no organic ones, so
|
|
330
|
+
the recent feed is legitimately empty on day one. "Nothing yet" there
|
|
331
|
+
reads as an empty vault, which is the opposite of what happened."""
|
|
332
|
+
seeded = max(int(state.get("records", 0)) - int(state.get("organic", 0)), 0)
|
|
333
|
+
if not seeded:
|
|
334
|
+
return "nothing stored yet"
|
|
335
|
+
return (f"nothing stored during use yet - {seeded:,} starting memories "
|
|
336
|
+
"came with the vault and are searchable now")
|
|
337
|
+
|
|
338
|
+
|
|
292
339
|
def auto_lock_label(minutes: int) -> str:
|
|
293
340
|
return "Never" if not minutes else f"{minutes} min"
|
|
294
341
|
|
|
@@ -579,6 +626,7 @@ def run(vault: str | None = None, show: bool = False,
|
|
|
579
626
|
self.pw_new = None
|
|
580
627
|
self.pw_repeat = None
|
|
581
628
|
self.change_note = None
|
|
629
|
+
self.connect_note = None # what the last Connect button reported
|
|
582
630
|
return self
|
|
583
631
|
|
|
584
632
|
# ---- building the popover contents -----------------------------
|
|
@@ -724,13 +772,35 @@ def run(vault: str | None = None, show: bool = False,
|
|
|
724
772
|
10, secondary=True))
|
|
725
773
|
views.append(divider())
|
|
726
774
|
|
|
775
|
+
# Installing leaves you with a vault and an icon, and nothing
|
|
776
|
+
# using either until an agent is wired to it. That step is one
|
|
777
|
+
# terminal command, which is one command too many for most
|
|
778
|
+
# people, so it is a button.
|
|
779
|
+
views.append(label("CONNECT AN AGENT", 10, bold=True,
|
|
780
|
+
secondary=True))
|
|
781
|
+
connect_buttons = []
|
|
782
|
+
for i, (_target, name) in enumerate(INTEGRATION_TARGETS):
|
|
783
|
+
b = NSButton.buttonWithTitle_target_action_(
|
|
784
|
+
name, self, "connectAgent:")
|
|
785
|
+
b.setTag_(i)
|
|
786
|
+
connect_buttons.append(b)
|
|
787
|
+
views.append(row(*connect_buttons, _spacer()))
|
|
788
|
+
views.append(label(
|
|
789
|
+
self.connect_note or
|
|
790
|
+
"Registers memory with the agent, writes its instructions "
|
|
791
|
+
"and turns on capture. The same as running `compartment "
|
|
792
|
+
"integrate claude` in a terminal.", 10, secondary=True,
|
|
793
|
+
wrap=True))
|
|
794
|
+
views.append(divider())
|
|
795
|
+
|
|
727
796
|
views.append(label(f"LAST {RECENT_COUNT} MEMORIES", 10, bold=True,
|
|
728
797
|
secondary=True))
|
|
729
798
|
if st["locked"]:
|
|
730
799
|
views.append(label("unlock the vault to see them", 11,
|
|
731
800
|
secondary=True))
|
|
732
801
|
elif not st["recent"]:
|
|
733
|
-
views.append(label(
|
|
802
|
+
views.append(label(starter_note(st), 11, secondary=True,
|
|
803
|
+
wrap=True))
|
|
734
804
|
else:
|
|
735
805
|
for r in st["recent"]:
|
|
736
806
|
text = " ".join((r.get("text") or "").split())
|
|
@@ -857,6 +927,15 @@ def run(vault: str | None = None, show: bool = False,
|
|
|
857
927
|
set_setting(vault_path, "auto_lock_minutes", AUTO_LOCK_CHOICES[idx])
|
|
858
928
|
self.rebuild()
|
|
859
929
|
|
|
930
|
+
def connectAgent_(self, sender):
|
|
931
|
+
try:
|
|
932
|
+
target = INTEGRATION_TARGETS[int(sender.tag())][0]
|
|
933
|
+
except (IndexError, ValueError):
|
|
934
|
+
return
|
|
935
|
+
_ok, msg = integrate(vault_path, target)
|
|
936
|
+
self.connect_note = msg
|
|
937
|
+
self.rebuild()
|
|
938
|
+
|
|
860
939
|
def refresh_(self, sender):
|
|
861
940
|
self.rebuild()
|
|
862
941
|
|
|
@@ -29,10 +29,11 @@ from pathlib import Path
|
|
|
29
29
|
|
|
30
30
|
from .home import env, home
|
|
31
31
|
|
|
32
|
-
from .menubar import (AUTO_LOCK_CHOICES,
|
|
33
|
-
change_passphrase, claim_first_run,
|
|
34
|
-
|
|
35
|
-
summarise,
|
|
32
|
+
from .menubar import (AUTO_LOCK_CHOICES, INTEGRATION_TARGETS, RECENT_COUNT,
|
|
33
|
+
auto_lock_label, change_passphrase, claim_first_run,
|
|
34
|
+
default_vault, fetch_state, integrate, lock_vault,
|
|
35
|
+
self_check, set_setting, starter_note, summarise,
|
|
36
|
+
unlock_vault)
|
|
36
37
|
|
|
37
38
|
PANEL_WIDTH = 360
|
|
38
39
|
PANEL_MAX_HEIGHT = 640
|
|
@@ -106,10 +107,20 @@ def panel_rows(state: dict) -> list[tuple[str, str]]:
|
|
|
106
107
|
f"{'on' if s['search_starter_facts'] else 'off'}"))
|
|
107
108
|
rows.append(("choice:auto_lock_minutes",
|
|
108
109
|
f"Auto-lock: {auto_lock_label(s['auto_lock_minutes'])}"))
|
|
110
|
+
# Installing leaves you with a vault that nothing is using until an agent
|
|
111
|
+
# is wired to it. One button per agent, so that step is not a terminal
|
|
112
|
+
# command someone has to know about.
|
|
113
|
+
rows.append(("heading", "CONNECT AN AGENT"))
|
|
114
|
+
for target, name in INTEGRATION_TARGETS:
|
|
115
|
+
rows.append((f"connect:{target}", name))
|
|
116
|
+
rows.append(("note", state.get("connect_note") or
|
|
117
|
+
"Registers memory with the agent, writes its instructions "
|
|
118
|
+
"and turns on capture. The same as running "
|
|
119
|
+
"`compartment integrate claude` in a terminal."))
|
|
109
120
|
rows.append(("heading", f"LAST {RECENT_COUNT} MEMORIES"))
|
|
110
121
|
recent = state.get("recent") or []
|
|
111
122
|
if not recent:
|
|
112
|
-
rows.append(("empty",
|
|
123
|
+
rows.append(("empty", starter_note(state)))
|
|
113
124
|
for r in recent:
|
|
114
125
|
rows.append(("memory", (r.get("text") or "").strip()))
|
|
115
126
|
return rows
|
|
@@ -335,13 +346,36 @@ def run(vault: str | None = None, show: bool = False,
|
|
|
335
346
|
*[auto_lock_label(m) for m in AUTO_LOCK_CHOICES],
|
|
336
347
|
command=set_lock).pack(side="right")
|
|
337
348
|
|
|
349
|
+
ttk.Separator(frame).pack(fill="x", pady=10)
|
|
350
|
+
ttk.Label(frame, text="CONNECT AN AGENT",
|
|
351
|
+
font=("Segoe UI", 8, "bold")).pack(anchor="w")
|
|
352
|
+
|
|
353
|
+
def connect(target):
|
|
354
|
+
_ok, note = integrate(vault_path, target)
|
|
355
|
+
panel["connect_note"] = note
|
|
356
|
+
refresh()
|
|
357
|
+
|
|
358
|
+
connect_row = ttk.Frame(frame)
|
|
359
|
+
connect_row.pack(fill="x", pady=(6, 0))
|
|
360
|
+
for _target, _name in INTEGRATION_TARGETS:
|
|
361
|
+
ttk.Button(connect_row, text=_name,
|
|
362
|
+
command=lambda t=_target: connect(t)
|
|
363
|
+
).pack(side="left", padx=(0, 6))
|
|
364
|
+
ttk.Label(frame, text=panel.get("connect_note") or
|
|
365
|
+
"Registers memory with the agent, writes its instructions "
|
|
366
|
+
"and turns on capture. The same as running "
|
|
367
|
+
"`compartment integrate claude` in a terminal.",
|
|
368
|
+
foreground="#666", wraplength=WRAP,
|
|
369
|
+
justify="left").pack(anchor="w", pady=(4, 0))
|
|
370
|
+
|
|
338
371
|
ttk.Separator(frame).pack(fill="x", pady=10)
|
|
339
372
|
ttk.Label(frame, text=f"LAST {RECENT_COUNT} MEMORIES",
|
|
340
373
|
font=("Segoe UI", 8, "bold")).pack(anchor="w")
|
|
341
374
|
recent = state.get("recent") or []
|
|
342
375
|
if not recent:
|
|
343
|
-
ttk.Label(frame, text=
|
|
344
|
-
|
|
376
|
+
ttk.Label(frame, text=starter_note(state), foreground="#666",
|
|
377
|
+
wraplength=WRAP,
|
|
378
|
+
justify="left").pack(anchor="w", pady=(4, 0))
|
|
345
379
|
for r in recent:
|
|
346
380
|
ttk.Label(frame, text=(r.get("text") or "").strip(),
|
|
347
381
|
wraplength=WRAP,
|
|
@@ -61,6 +61,15 @@ QUARANTINE_WARNING = (
|
|
|
61
61
|
)
|
|
62
62
|
|
|
63
63
|
|
|
64
|
+
def is_seeded(tags_json: str) -> bool:
|
|
65
|
+
"""Did this record arrive with the vault, rather than during use?
|
|
66
|
+
|
|
67
|
+
Seeding preserves each starting memory's stable id as an "id:" tag, and
|
|
68
|
+
nothing else writes one, so the tag is the mark. It is the same test
|
|
69
|
+
everywhere a caller asks to see one set without the other."""
|
|
70
|
+
return any(t.startswith("id:") for t in json.loads(tags_json))
|
|
71
|
+
|
|
72
|
+
|
|
64
73
|
class VaultLockedError(CryptoError):
|
|
65
74
|
pass
|
|
66
75
|
|
|
@@ -503,11 +512,19 @@ class Vault:
|
|
|
503
512
|
+ IMPORTANCE_WEIGHT * self._importance_of(rid)
|
|
504
513
|
for rid, s in scores.items()}
|
|
505
514
|
|
|
515
|
+
# The starting memories are ordinary records in "main", so a namespace
|
|
516
|
+
# filter cannot reach them. Anyone who wants recall limited to what the
|
|
517
|
+
# agent learned here turns them off, and that has to be honoured on the
|
|
518
|
+
# one path that returns memories to a model.
|
|
519
|
+
starter = self.config.settings.get("search_starter_facts", True)
|
|
520
|
+
|
|
506
521
|
results = []
|
|
507
522
|
for rid in sorted(boosted, key=boosted.get, reverse=True):
|
|
508
523
|
row = self.db.get_row(rid)
|
|
509
524
|
if row is None or row["ns"] not in allowed:
|
|
510
525
|
continue
|
|
526
|
+
if not starter and is_seeded(row["tags"]):
|
|
527
|
+
continue
|
|
511
528
|
if tags and not set(tags) <= set(json.loads(row["tags"])):
|
|
512
529
|
continue
|
|
513
530
|
if since and row["created"] < since:
|
|
@@ -564,7 +581,7 @@ class Vault:
|
|
|
564
581
|
if row["ns"] not in allowed:
|
|
565
582
|
continue
|
|
566
583
|
total += 1
|
|
567
|
-
seeded =
|
|
584
|
+
seeded = is_seeded(row["tags"])
|
|
568
585
|
if not seeded:
|
|
569
586
|
organic += 1
|
|
570
587
|
if seeded and not include_seeded:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: compartment
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.4
|
|
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
|
|
@@ -243,6 +243,11 @@ network at all.
|
|
|
243
243
|
One command per platform. Each installs the package, creates your
|
|
244
244
|
encrypted vault, and wires the agent.
|
|
245
245
|
|
|
246
|
+
Every one of these is also a button in the app. Click the Compartment icon
|
|
247
|
+
in your menu bar or notification area, and under **CONNECT AN AGENT** press
|
|
248
|
+
Claude, Hermes or OpenClaw. The button runs the same `compartment integrate`
|
|
249
|
+
command for you, so nobody has to open a terminal a second time.
|
|
250
|
+
|
|
246
251
|
**Claude (Code + Desktop)** - macOS / Linux:
|
|
247
252
|
```bash
|
|
248
253
|
pip install compartment && compartment init && compartment integrate claude
|
|
@@ -46,6 +46,7 @@ tests/test_attempt_pacing.py
|
|
|
46
46
|
tests/test_audit_chain.py
|
|
47
47
|
tests/test_claude_hooks_and_integrate.py
|
|
48
48
|
tests/test_claude_memory_and_recent.py
|
|
49
|
+
tests/test_connect_buttons.py
|
|
49
50
|
tests/test_crypto.py
|
|
50
51
|
tests/test_dash.py
|
|
51
52
|
tests/test_instructions.py
|
|
@@ -63,6 +64,7 @@ tests/test_session_entropy.py
|
|
|
63
64
|
tests/test_session_lock.py
|
|
64
65
|
tests/test_starter_data_quality.py
|
|
65
66
|
tests/test_starter_packs.py
|
|
67
|
+
tests/test_starter_visibility.py
|
|
66
68
|
tests/test_status_bar_install.py
|
|
67
69
|
tests/test_systray.py
|
|
68
70
|
tests/test_tamper.py
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
"""Connecting an agent is a button, on both platforms.
|
|
2
|
+
|
|
3
|
+
`compartment integrate <agent>` is the step people miss: the vault exists,
|
|
4
|
+
the icon is in the menu bar, and nothing is using either because the second
|
|
5
|
+
command was never run. The panel offers it directly, and these tests hold the
|
|
6
|
+
two front ends to the same list of agents as the CLI.
|
|
7
|
+
"""
|
|
8
|
+
import re
|
|
9
|
+
|
|
10
|
+
import pytest
|
|
11
|
+
|
|
12
|
+
from compartment import cli, menubar, systray
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _state(**over):
|
|
16
|
+
st = {"vault": "/v", "exists": True, "locked": False, "records": 6665,
|
|
17
|
+
"organic": 0, "recent": [], "error": None,
|
|
18
|
+
"settings": {"capture_hook": True, "search_starter_facts": True,
|
|
19
|
+
"auto_lock_minutes": 30}}
|
|
20
|
+
st.update(over)
|
|
21
|
+
return st
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# ------------------------------------------------------------- the agent list
|
|
25
|
+
|
|
26
|
+
@pytest.mark.parametrize("target,_name", menubar.INTEGRATION_TARGETS)
|
|
27
|
+
def test_every_button_names_a_target_the_cli_accepts(monkeypatch, target,
|
|
28
|
+
_name):
|
|
29
|
+
"""A button for an agent the CLI rejects is a button that cannot work.
|
|
30
|
+
Goes through the real parser, which is the only thing that can say."""
|
|
31
|
+
seen = {}
|
|
32
|
+
monkeypatch.setattr(cli, "cmd_integrate",
|
|
33
|
+
lambda args: seen.update(vars(args)))
|
|
34
|
+
cli.main(["integrate", target])
|
|
35
|
+
assert seen["target"] == target
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def test_no_agent_the_cli_accepts_is_missing_a_button(monkeypatch, capsys):
|
|
39
|
+
"""The other direction: adding a target to the CLI and forgetting the
|
|
40
|
+
button leaves an agent nobody can reach from the app. argparse names
|
|
41
|
+
every choice it accepts when it rejects one, so ask it."""
|
|
42
|
+
monkeypatch.setattr(cli, "cmd_integrate", lambda args: None)
|
|
43
|
+
with pytest.raises(SystemExit):
|
|
44
|
+
cli.main(["integrate", "no-such-agent"])
|
|
45
|
+
err = capsys.readouterr().err
|
|
46
|
+
choices = set(re.findall(r"'([a-z]+)'", err.split("choose from")[-1]))
|
|
47
|
+
assert choices == {t for t, _ in menubar.INTEGRATION_TARGETS}
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def test_the_agents_are_offered_in_a_fixed_order():
|
|
51
|
+
assert [t for t, _ in menubar.INTEGRATION_TARGETS] == \
|
|
52
|
+
["claude", "hermes", "openclaw"]
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def test_every_agent_has_a_display_name():
|
|
56
|
+
for target, name in menubar.INTEGRATION_TARGETS:
|
|
57
|
+
assert name and name.lower().replace(" ", "") == target
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
# ---------------------------------------------------------------- the action
|
|
61
|
+
|
|
62
|
+
def test_an_unknown_target_never_runs_anything(monkeypatch):
|
|
63
|
+
called = []
|
|
64
|
+
monkeypatch.setattr(menubar, "_run", lambda *a, **k: called.append(a))
|
|
65
|
+
ok, msg = menubar.integrate("/v", "notanagent")
|
|
66
|
+
assert ok is False and "notanagent" in msg
|
|
67
|
+
assert called == []
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def test_it_runs_the_documented_command(monkeypatch):
|
|
71
|
+
seen = {}
|
|
72
|
+
|
|
73
|
+
def fake_run(args, timeout=60):
|
|
74
|
+
seen["args"] = args
|
|
75
|
+
return 0, "registered."
|
|
76
|
+
monkeypatch.setattr(menubar, "_run", fake_run)
|
|
77
|
+
ok, _ = menubar.integrate("/path/to/v.vault", "claude")
|
|
78
|
+
assert ok is True
|
|
79
|
+
assert seen["args"][-3:] == ["v.vault", "integrate", "claude"] or \
|
|
80
|
+
seen["args"][-2:] == ["integrate", "claude"]
|
|
81
|
+
assert "--vault" in seen["args"]
|
|
82
|
+
assert "/path/to/v.vault" in seen["args"]
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def test_success_tells_the_user_to_restart_the_agent(monkeypatch):
|
|
86
|
+
monkeypatch.setattr(menubar, "_run", lambda *a, **k: (0, "registered."))
|
|
87
|
+
ok, msg = menubar.integrate("/v", "claude")
|
|
88
|
+
assert ok is True
|
|
89
|
+
assert "Claude" in msg and "estart" in msg
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def test_a_missing_agent_is_reported_as_normal_not_as_failure(monkeypatch):
|
|
93
|
+
"""Not having Hermes installed is not an error, and must not read as
|
|
94
|
+
one: the wiring that can be done is still done."""
|
|
95
|
+
monkeypatch.setattr(menubar, "_run",
|
|
96
|
+
lambda *a, **k: (0, "hermes not found; finish with…"))
|
|
97
|
+
ok, msg = menubar.integrate("/v", "hermes")
|
|
98
|
+
assert ok is True
|
|
99
|
+
assert "not installed" in msg
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def test_a_real_failure_surfaces_its_reason(monkeypatch):
|
|
103
|
+
monkeypatch.setattr(menubar, "_run",
|
|
104
|
+
lambda *a, **k: (1, "permission denied writing config"))
|
|
105
|
+
ok, msg = menubar.integrate("/v", "claude")
|
|
106
|
+
assert ok is False
|
|
107
|
+
assert "permission denied" in msg
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def test_a_failure_with_no_output_still_says_something(monkeypatch):
|
|
111
|
+
monkeypatch.setattr(menubar, "_run", lambda *a, **k: (1, ""))
|
|
112
|
+
ok, msg = menubar.integrate("/v", "openclaw")
|
|
113
|
+
assert ok is False and msg
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def test_wiring_gets_a_longer_leash_than_a_status_call(monkeypatch):
|
|
117
|
+
"""Installing into the Hermes venv can take a while; a 60 s default would
|
|
118
|
+
time out mid-pip and leave the agent half-wired."""
|
|
119
|
+
seen = {}
|
|
120
|
+
|
|
121
|
+
def fake_run(args, timeout=60):
|
|
122
|
+
seen["timeout"] = timeout
|
|
123
|
+
return 0, "ok"
|
|
124
|
+
monkeypatch.setattr(menubar, "_run", fake_run)
|
|
125
|
+
menubar.integrate("/v", "hermes")
|
|
126
|
+
assert seen["timeout"] >= 300
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
# ----------------------------------------------------------------- the panel
|
|
130
|
+
|
|
131
|
+
def test_the_tray_panel_has_a_button_per_agent():
|
|
132
|
+
rows = systray.panel_rows(_state())
|
|
133
|
+
connects = [text for kind, text in rows if kind.startswith("connect:")]
|
|
134
|
+
assert connects == ["Claude", "Hermes", "OpenClaw"]
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def test_the_tray_panel_heads_the_section():
|
|
138
|
+
rows = systray.panel_rows(_state())
|
|
139
|
+
assert ("heading", "CONNECT AN AGENT") in rows
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def test_the_tray_panel_explains_what_the_buttons_do():
|
|
143
|
+
rows = systray.panel_rows(_state())
|
|
144
|
+
note = [t for k, t in rows if k == "note"][0]
|
|
145
|
+
assert "integrate claude" in note
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def test_the_tray_panel_shows_the_last_result_once_there_is_one():
|
|
149
|
+
rows = systray.panel_rows(_state(connect_note="Claude is connected."))
|
|
150
|
+
note = [t for k, t in rows if k == "note"][0]
|
|
151
|
+
assert note == "Claude is connected."
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def test_the_buttons_are_offered_even_while_the_vault_is_locked():
|
|
155
|
+
"""Wiring an agent edits the agent's config, not the vault, so a locked
|
|
156
|
+
vault is no reason to hide it."""
|
|
157
|
+
rows = systray.panel_rows(_state(locked=True))
|
|
158
|
+
assert [t for k, t in rows if k.startswith("connect:")] == \
|
|
159
|
+
["Claude", "Hermes", "OpenClaw"]
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
@pytest.mark.parametrize("target,name", menubar.INTEGRATION_TARGETS)
|
|
163
|
+
def test_each_row_carries_the_target_the_action_needs(target, name):
|
|
164
|
+
rows = systray.panel_rows(_state())
|
|
165
|
+
assert (f"connect:{target}", name) in rows
|
|
@@ -43,7 +43,7 @@ def test_set_setting_persists_to_the_config_file(vault_path, monkeypatch):
|
|
|
43
43
|
menubar.set_setting(vault_path, "search_starter_facts", False)
|
|
44
44
|
menubar.set_setting(vault_path, "auto_lock_minutes", 0)
|
|
45
45
|
cfg = VaultConfig.load(vault_path) # straight from disk
|
|
46
|
-
assert cfg.settings["
|
|
46
|
+
assert cfg.settings["search_starter_facts"] is False
|
|
47
47
|
assert cfg.settings["auto_lock_minutes"] == 0
|
|
48
48
|
again = menubar.read_settings(vault_path)
|
|
49
49
|
assert again["search_starter_facts"] is False
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
"""The starting memories arrive with every install, and every surface says so.
|
|
2
|
+
|
|
3
|
+
Two separate failures hide behind "where are the starter memories?". One is an
|
|
4
|
+
install that never seeded and said nothing about it. The other is an install
|
|
5
|
+
that seeded perfectly and then reported itself as empty, because the recent
|
|
6
|
+
feed hides seeded records by design. Both look identical to the person who
|
|
7
|
+
just installed it, so both are tested here.
|
|
8
|
+
"""
|
|
9
|
+
import json
|
|
10
|
+
import pathlib
|
|
11
|
+
|
|
12
|
+
import pytest
|
|
13
|
+
|
|
14
|
+
from compartment import cli, menubar, packs
|
|
15
|
+
from compartment.acl import VaultConfig
|
|
16
|
+
from compartment.vault import Vault, is_seeded
|
|
17
|
+
|
|
18
|
+
DATA = pathlib.Path(__file__).resolve().parents[1] / "src" / "compartment" / "data"
|
|
19
|
+
PASS = "CorrectHorse"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
# --------------------------------------------------------------- the install
|
|
23
|
+
|
|
24
|
+
def test_a_missing_starter_pack_is_a_hard_failure(monkeypatch, tmp_path):
|
|
25
|
+
"""Never quietly hand back an empty vault."""
|
|
26
|
+
monkeypatch.setattr(cli, "_data_dir", lambda: tmp_path / "not-here")
|
|
27
|
+
with pytest.raises(SystemExit):
|
|
28
|
+
cli._seed_blobs()
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def test_the_failure_names_the_remedy(monkeypatch, tmp_path, capsys):
|
|
32
|
+
monkeypatch.setattr(cli, "_data_dir", lambda: tmp_path / "not-here")
|
|
33
|
+
with pytest.raises(SystemExit):
|
|
34
|
+
cli._seed_blobs()
|
|
35
|
+
err = capsys.readouterr().err
|
|
36
|
+
assert "starter.mpack" in err and "reinstall" in err.lower()
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def test_a_broken_install_leaves_no_vault_behind(monkeypatch, tmp_path):
|
|
40
|
+
"""The check runs before the passphrase prompt and before any write, so a
|
|
41
|
+
failed init cannot leave a half-made vault that `init` then refuses to
|
|
42
|
+
overwrite."""
|
|
43
|
+
monkeypatch.setattr(cli, "_data_dir", lambda: tmp_path / "not-here")
|
|
44
|
+
path = tmp_path / "v.vault"
|
|
45
|
+
args = _init_args(str(path))
|
|
46
|
+
with pytest.raises(SystemExit):
|
|
47
|
+
cli.cmd_init(args)
|
|
48
|
+
assert not path.exists()
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def test_the_shipped_install_finds_its_seed():
|
|
52
|
+
blobs = cli._seed_blobs()
|
|
53
|
+
assert [n for n, _ in blobs] == ["starter"]
|
|
54
|
+
assert len(blobs[0][1]) > 1_000_000
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _init_args(path):
|
|
58
|
+
class A:
|
|
59
|
+
vault = None
|
|
60
|
+
passphrase = PASS
|
|
61
|
+
creator = "test"
|
|
62
|
+
keychain = False
|
|
63
|
+
no_app = True
|
|
64
|
+
keyfile = None
|
|
65
|
+
a = A()
|
|
66
|
+
a.vault = path
|
|
67
|
+
return a
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
# ------------------------------------------------------------- the reporting
|
|
71
|
+
|
|
72
|
+
@pytest.fixture(scope="module")
|
|
73
|
+
def _own_seeded(tmp_path_factory):
|
|
74
|
+
"""This module's own vault. The shared session fixture is written to by
|
|
75
|
+
other tests, and these assertions are about exactly which records come
|
|
76
|
+
back."""
|
|
77
|
+
p = str(tmp_path_factory.mktemp("starter-vis") / "v.vault")
|
|
78
|
+
v = Vault.create(p, PASS, creator="test")
|
|
79
|
+
packs.seed_records(v, (DATA / "starter.mpack").read_bytes(), caller="test")
|
|
80
|
+
return v
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@pytest.fixture()
|
|
84
|
+
def sv(_own_seeded):
|
|
85
|
+
"""Settings are per-vault and this one is reused, so put the toggle back."""
|
|
86
|
+
_own_seeded.config.settings["search_starter_facts"] = True
|
|
87
|
+
yield _own_seeded
|
|
88
|
+
_own_seeded.config.settings["search_starter_facts"] = True
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def test_the_recent_feed_hides_seeded_memories(sv):
|
|
92
|
+
"""Unchanged behaviour, stated so the fix below cannot be read as a
|
|
93
|
+
licence to bury real memories under thousands of starting ones."""
|
|
94
|
+
out = sv.recent(caller="test", limit=5)
|
|
95
|
+
assert not any(r["seeded"] for r in out["results"])
|
|
96
|
+
assert out["counts"]["seeded"] == 6665
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def test_the_panel_says_the_starting_memories_are_there():
|
|
100
|
+
note = menubar.starter_note({"records": 6665, "organic": 0})
|
|
101
|
+
assert "6,665" in note and "searchable" in note
|
|
102
|
+
assert "nothing stored yet" != note
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def test_the_panel_still_says_empty_when_it_really_is_empty():
|
|
106
|
+
assert menubar.starter_note({"records": 0, "organic": 0}) == \
|
|
107
|
+
"nothing stored yet"
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def test_the_panel_counts_only_the_seeded_ones():
|
|
111
|
+
note = menubar.starter_note({"records": 6670, "organic": 5})
|
|
112
|
+
assert "6,665" in note
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def test_the_tray_and_the_menu_bar_say_the_same_thing():
|
|
116
|
+
from compartment import systray
|
|
117
|
+
state = {"vault": "/v", "exists": True, "locked": False,
|
|
118
|
+
"records": 6665, "organic": 0, "recent": [], "error": None,
|
|
119
|
+
"settings": {"capture_hook": False, "search_starter_facts": True,
|
|
120
|
+
"auto_lock_minutes": 30}}
|
|
121
|
+
rows = systray.panel_rows(state)
|
|
122
|
+
empty = [text for kind, text in rows if kind == "empty"]
|
|
123
|
+
assert empty and "6,665" in empty[0]
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
# ---------------------------------------------------------------- the toggle
|
|
127
|
+
|
|
128
|
+
def test_search_returns_starting_memories_by_default(sv):
|
|
129
|
+
hits = sv.search("capital of France", caller="test", top_k=3)
|
|
130
|
+
assert any("Paris" in h["text"] for h in hits["results"])
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def test_turning_starter_facts_off_removes_them_from_search(sv):
|
|
134
|
+
on = sv.search("capital of France", caller="test", top_k=5)["results"]
|
|
135
|
+
assert any("Paris" in h["text"] for h in on) # the toggle is what changed
|
|
136
|
+
sv.config.settings["search_starter_facts"] = False
|
|
137
|
+
off = sv.search("capital of France", caller="test", top_k=5)["results"]
|
|
138
|
+
assert not any("Paris" in h["text"] for h in off)
|
|
139
|
+
assert not any(is_seeded(json.dumps(h["tags"])) for h in off)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def test_turning_starter_facts_off_keeps_what_the_agent_stored(sv):
|
|
143
|
+
sv.store("Max prefers the master-detail layout", caller="test")
|
|
144
|
+
sv.config.settings["search_starter_facts"] = False
|
|
145
|
+
hits = sv.search("master-detail layout preference", caller="test", top_k=5)
|
|
146
|
+
texts = [h["text"] for h in hits["results"]]
|
|
147
|
+
assert "Max prefers the master-detail layout" in texts
|
|
148
|
+
assert not any(is_seeded(json.dumps(h["tags"])) for h in hits["results"])
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def test_the_toggle_writes_the_setting_it_claims_to(tmp_path):
|
|
152
|
+
"""It used to write include_packs_in_search, which stopped meaning
|
|
153
|
+
anything when the starting memories moved into "main"."""
|
|
154
|
+
path = str(tmp_path / "v.vault")
|
|
155
|
+
Vault.create(path, PASS, creator="t").lock()
|
|
156
|
+
menubar.set_setting(path, "search_starter_facts", False)
|
|
157
|
+
cfg = VaultConfig.load(path)
|
|
158
|
+
assert cfg.settings["search_starter_facts"] is False
|
|
159
|
+
assert cfg.settings.get("include_packs_in_search", True) is True
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def test_the_toggle_reads_back_what_it_wrote(tmp_path):
|
|
163
|
+
path = str(tmp_path / "v.vault")
|
|
164
|
+
Vault.create(path, PASS, creator="t").lock()
|
|
165
|
+
menubar.set_setting(path, "search_starter_facts", False)
|
|
166
|
+
assert menubar.read_settings(path)["search_starter_facts"] is False
|
|
167
|
+
menubar.set_setting(path, "search_starter_facts", True)
|
|
168
|
+
assert menubar.read_settings(path)["search_starter_facts"] is True
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def test_installed_packs_keep_their_own_switch(tmp_path):
|
|
172
|
+
"""Two different things, two different settings: the starter toggle must
|
|
173
|
+
not silently change what installed packs do."""
|
|
174
|
+
path = str(tmp_path / "v.vault")
|
|
175
|
+
Vault.create(path, PASS, creator="t").lock()
|
|
176
|
+
cfg = VaultConfig.load(path)
|
|
177
|
+
cfg.settings["include_packs_in_search"] = False
|
|
178
|
+
cfg.save(path)
|
|
179
|
+
menubar.set_setting(path, "search_starter_facts", True)
|
|
180
|
+
assert VaultConfig.load(path).settings["include_packs_in_search"] is False
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
# ------------------------------------------------------------------ the mark
|
|
184
|
+
|
|
185
|
+
def test_seeded_records_are_marked_by_their_id_tag(vault):
|
|
186
|
+
packs.seed_records(vault, (DATA / "starter.mpack").read_bytes(), caller="t")
|
|
187
|
+
row = vault.db.conn.execute(
|
|
188
|
+
"SELECT tags FROM records LIMIT 1").fetchone()
|
|
189
|
+
assert is_seeded(row["tags"])
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def test_what_the_agent_stores_is_not_marked_seeded(vault):
|
|
193
|
+
vault.store("something learned during use", caller="t")
|
|
194
|
+
row = vault.db.conn.execute(
|
|
195
|
+
"SELECT tags FROM records ORDER BY ikey DESC LIMIT 1").fetchone()
|
|
196
|
+
assert not is_seeded(row["tags"])
|
|
197
|
+
assert json.loads(row["tags"]) == []
|
|
@@ -91,7 +91,8 @@ def test_missing_vault_and_errors_are_shown_not_raised():
|
|
|
91
91
|
def test_settings_toggles_are_exactly_the_three_the_mac_panel_has():
|
|
92
92
|
from compartment import menubar
|
|
93
93
|
rows = systray.panel_rows(_state())
|
|
94
|
-
keys = {k.split(":", 1)[1] for k, _ in rows
|
|
94
|
+
keys = {k.split(":", 1)[1] for k, _ in rows
|
|
95
|
+
if k.startswith(("toggle:", "choice:"))}
|
|
95
96
|
assert keys == set(menubar.fetch_state("/does/not/exist")["settings"])
|
|
96
97
|
|
|
97
98
|
|
|
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-3.2 → compartment-3.4}/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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|