compartment 2__tar.gz → 2.1__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 → compartment-2.1}/PKG-INFO +24 -1
- {compartment-2 → compartment-2.1}/pyproject.toml +34 -1
- {compartment-2 → compartment-2.1}/src/compartment/__init__.py +1 -1
- {compartment-2 → compartment-2.1}/src/compartment/menubar.py +87 -37
- {compartment-2 → compartment-2.1}/src/compartment/systray.py +105 -44
- {compartment-2 → compartment-2.1}/src/compartment.egg-info/PKG-INFO +24 -1
- {compartment-2 → compartment-2.1}/tests/test_systray.py +30 -0
- {compartment-2 → compartment-2.1}/README.md +0 -0
- {compartment-2 → compartment-2.1}/setup.cfg +0 -0
- {compartment-2 → compartment-2.1}/src/compartment/acl.py +0 -0
- {compartment-2 → compartment-2.1}/src/compartment/audit.py +0 -0
- {compartment-2 → compartment-2.1}/src/compartment/bench.py +0 -0
- {compartment-2 → compartment-2.1}/src/compartment/claude_hooks.py +0 -0
- {compartment-2 → compartment-2.1}/src/compartment/claude_memory.py +0 -0
- {compartment-2 → compartment-2.1}/src/compartment/cli.py +0 -0
- {compartment-2 → compartment-2.1}/src/compartment/crypto.py +0 -0
- {compartment-2 → compartment-2.1}/src/compartment/dash.py +0 -0
- {compartment-2 → compartment-2.1}/src/compartment/data/hermes-plugin/__init__.py +0 -0
- {compartment-2 → compartment-2.1}/src/compartment/data/hermes-plugin/plugin.yaml +0 -0
- {compartment-2 → compartment-2.1}/src/compartment/data/menubar.png +0 -0
- {compartment-2 → compartment-2.1}/src/compartment/data/menubar@2x.png +0 -0
- {compartment-2 → compartment-2.1}/src/compartment/data/starter.mpack +0 -0
- {compartment-2 → compartment-2.1}/src/compartment/data/tray.ico +0 -0
- {compartment-2 → compartment-2.1}/src/compartment/embed.py +0 -0
- {compartment-2 → compartment-2.1}/src/compartment/home.py +0 -0
- {compartment-2 → compartment-2.1}/src/compartment/longmemeval.py +0 -0
- {compartment-2 → compartment-2.1}/src/compartment/models/bge-small-en-v1.5-int8/model_quantized.onnx +0 -0
- {compartment-2 → compartment-2.1}/src/compartment/models/bge-small-en-v1.5-int8/tokenizer.json +0 -0
- {compartment-2 → compartment-2.1}/src/compartment/offline_guard.py +0 -0
- {compartment-2 → compartment-2.1}/src/compartment/packs.py +0 -0
- {compartment-2 → compartment-2.1}/src/compartment/platforms.py +0 -0
- {compartment-2 → compartment-2.1}/src/compartment/salience.py +0 -0
- {compartment-2 → compartment-2.1}/src/compartment/selftest.py +0 -0
- {compartment-2 → compartment-2.1}/src/compartment/server.py +0 -0
- {compartment-2 → compartment-2.1}/src/compartment/session.py +0 -0
- {compartment-2 → compartment-2.1}/src/compartment/store.py +0 -0
- {compartment-2 → compartment-2.1}/src/compartment/vault.py +0 -0
- {compartment-2 → compartment-2.1}/src/compartment/vaultfile.py +0 -0
- {compartment-2 → compartment-2.1}/src/compartment/vindex.py +0 -0
- {compartment-2 → compartment-2.1}/src/compartment.egg-info/SOURCES.txt +0 -0
- {compartment-2 → compartment-2.1}/src/compartment.egg-info/dependency_links.txt +0 -0
- {compartment-2 → compartment-2.1}/src/compartment.egg-info/entry_points.txt +0 -0
- {compartment-2 → compartment-2.1}/src/compartment.egg-info/requires.txt +0 -0
- {compartment-2 → compartment-2.1}/src/compartment.egg-info/top_level.txt +0 -0
- {compartment-2 → compartment-2.1}/tests/test_acl_audit_packs.py +0 -0
- {compartment-2 → compartment-2.1}/tests/test_claude_hooks_and_integrate.py +0 -0
- {compartment-2 → compartment-2.1}/tests/test_claude_memory_and_recent.py +0 -0
- {compartment-2 → compartment-2.1}/tests/test_crypto.py +0 -0
- {compartment-2 → compartment-2.1}/tests/test_dash.py +0 -0
- {compartment-2 → compartment-2.1}/tests/test_instructions.py +0 -0
- {compartment-2 → compartment-2.1}/tests/test_macos_bundle.py +0 -0
- {compartment-2 → compartment-2.1}/tests/test_menubar.py +0 -0
- {compartment-2 → compartment-2.1}/tests/test_offline_selftest_bench.py +0 -0
- {compartment-2 → compartment-2.1}/tests/test_ondisk_format.py +0 -0
- {compartment-2 → compartment-2.1}/tests/test_platforms.py +0 -0
- {compartment-2 → compartment-2.1}/tests/test_relations.py +0 -0
- {compartment-2 → compartment-2.1}/tests/test_salience.py +0 -0
- {compartment-2 → compartment-2.1}/tests/test_session_lock.py +0 -0
- {compartment-2 → compartment-2.1}/tests/test_starter_packs.py +0 -0
- {compartment-2 → compartment-2.1}/tests/test_tamper.py +0 -0
- {compartment-2 → compartment-2.1}/tests/test_twofa.py +0 -0
- {compartment-2 → compartment-2.1}/tests/test_vault_ops.py +0 -0
|
@@ -1,7 +1,30 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: compartment
|
|
3
|
-
Version: 2
|
|
3
|
+
Version: 2.1
|
|
4
4
|
Summary: Compartment - high-security, fully offline, encrypted vector memory for AI agents (MCP)
|
|
5
|
+
Project-URL: Homepage, https://github.com/MaxFreedomPollard/Compartment
|
|
6
|
+
Project-URL: Source, https://github.com/MaxFreedomPollard/Compartment
|
|
7
|
+
Project-URL: Issues, https://github.com/MaxFreedomPollard/Compartment/issues
|
|
8
|
+
Project-URL: Changelog, https://github.com/MaxFreedomPollard/Compartment/releases
|
|
9
|
+
Project-URL: Documentation, https://github.com/MaxFreedomPollard/Compartment#readme
|
|
10
|
+
Project-URL: Security, https://github.com/MaxFreedomPollard/Compartment/blob/main/SECURITY.md
|
|
11
|
+
Keywords: mcp,model-context-protocol,memory,agent-memory,vector-database,semantic-search,encryption,offline,local-first,claude,claude-code,privacy,rag
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Environment :: Console
|
|
15
|
+
Classifier: Environment :: MacOS X
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Operating System :: MacOS :: MacOS X
|
|
18
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
19
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
20
|
+
Classifier: Programming Language :: Python :: 3
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
24
|
+
Classifier: Topic :: Security :: Cryptography
|
|
25
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
26
|
+
Classifier: Topic :: Database :: Database Engines/Servers
|
|
27
|
+
Classifier: Typing :: Typed
|
|
5
28
|
Requires-Python: >=3.11
|
|
6
29
|
Description-Content-Type: text/markdown
|
|
7
30
|
Requires-Dist: pynacl>=1.5
|
|
@@ -4,10 +4,31 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "compartment"
|
|
7
|
-
version = "2"
|
|
7
|
+
version = "2.1"
|
|
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"
|
|
11
|
+
keywords = ["mcp", "model-context-protocol", "memory", "agent-memory",
|
|
12
|
+
"vector-database", "semantic-search", "encryption", "offline",
|
|
13
|
+
"local-first", "claude", "claude-code", "privacy", "rag"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 4 - Beta",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"Environment :: Console",
|
|
18
|
+
"Environment :: MacOS X",
|
|
19
|
+
"Operating System :: OS Independent",
|
|
20
|
+
"Operating System :: MacOS :: MacOS X",
|
|
21
|
+
"Operating System :: Microsoft :: Windows",
|
|
22
|
+
"Operating System :: POSIX :: Linux",
|
|
23
|
+
"Programming Language :: Python :: 3",
|
|
24
|
+
"Programming Language :: Python :: 3.11",
|
|
25
|
+
"Programming Language :: Python :: 3.12",
|
|
26
|
+
"Programming Language :: Python :: 3.13",
|
|
27
|
+
"Topic :: Security :: Cryptography",
|
|
28
|
+
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
|
29
|
+
"Topic :: Database :: Database Engines/Servers",
|
|
30
|
+
"Typing :: Typed",
|
|
31
|
+
]
|
|
11
32
|
dependencies = [
|
|
12
33
|
"pynacl>=1.5",
|
|
13
34
|
"argon2-cffi>=23.1",
|
|
@@ -17,6 +38,18 @@ dependencies = [
|
|
|
17
38
|
"mcp>=1.0",
|
|
18
39
|
]
|
|
19
40
|
|
|
41
|
+
# PyPI is the repo's declared homepage, so it is a real front door - and
|
|
42
|
+
# without these it is a dead end: no Source link, no Issues link, nothing
|
|
43
|
+
# carrying a reader back to GitHub. Directories that scrape PyPI were
|
|
44
|
+
# producing a listing with no repository link at all.
|
|
45
|
+
[project.urls]
|
|
46
|
+
Homepage = "https://github.com/MaxFreedomPollard/Compartment"
|
|
47
|
+
Source = "https://github.com/MaxFreedomPollard/Compartment"
|
|
48
|
+
Issues = "https://github.com/MaxFreedomPollard/Compartment/issues"
|
|
49
|
+
Changelog = "https://github.com/MaxFreedomPollard/Compartment/releases"
|
|
50
|
+
Documentation = "https://github.com/MaxFreedomPollard/Compartment#readme"
|
|
51
|
+
Security = "https://github.com/MaxFreedomPollard/Compartment/blob/main/SECURITY.md"
|
|
52
|
+
|
|
20
53
|
[project.scripts]
|
|
21
54
|
compartment = "compartment.cli:main"
|
|
22
55
|
cmpt = "compartment.cli:main"
|
|
@@ -396,6 +396,7 @@ def run(vault: str | None = None, show: bool = False,
|
|
|
396
396
|
NSRunningApplication,
|
|
397
397
|
NSFont, NSImage, NSMakeRect, NSMenu, NSMenuItem,
|
|
398
398
|
NSPanel, NSPopover, NSScreen, NSSegmentedControl,
|
|
399
|
+
NSClipView, NSScrollView,
|
|
399
400
|
NSSecureTextField, NSStackView, NSStatusBar,
|
|
400
401
|
NSSwitch, NSTextField,
|
|
401
402
|
NSButton, NSView, NSViewController,
|
|
@@ -468,6 +469,17 @@ def run(vault: str | None = None, show: bool = False,
|
|
|
468
469
|
CONTENT_WIDTH).setActive_(True)
|
|
469
470
|
return b
|
|
470
471
|
|
|
472
|
+
class TopClip(NSClipView):
|
|
473
|
+
"""A clip view whose origin is its top left corner.
|
|
474
|
+
|
|
475
|
+
AppKit's default is the bottom left, which inside a scroll view means
|
|
476
|
+
the panel opens showing its last line, with the title somewhere above
|
|
477
|
+
the fold. Flipping it makes "scrolled to the top" the resting state.
|
|
478
|
+
"""
|
|
479
|
+
|
|
480
|
+
def isFlipped(self):
|
|
481
|
+
return True
|
|
482
|
+
|
|
471
483
|
class Controller(NSObject):
|
|
472
484
|
def init(self):
|
|
473
485
|
self = objc.super(Controller, self).init()
|
|
@@ -485,8 +497,78 @@ def run(vault: str | None = None, show: bool = False,
|
|
|
485
497
|
return self
|
|
486
498
|
|
|
487
499
|
# ---- building the popover contents -----------------------------
|
|
500
|
+
@objc.python_method
|
|
501
|
+
def _finish(self, views):
|
|
502
|
+
stack = NSStackView.stackViewWithViews_(views)
|
|
503
|
+
stack.setOrientation_(_VERTICAL)
|
|
504
|
+
stack.setAlignment_(_LEADING)
|
|
505
|
+
stack.setSpacing_(6)
|
|
506
|
+
stack.setEdgeInsets_((CONTENT_INSET, CONTENT_INSET,
|
|
507
|
+
CONTENT_INSET, CONTENT_INSET))
|
|
508
|
+
stack.layoutSubtreeIfNeeded()
|
|
509
|
+
fit = stack.fittingSize()
|
|
510
|
+
stack.setFrameSize_((POPOVER_WIDTH, fit.height))
|
|
511
|
+
if fit.height <= POPOVER_MAX_HEIGHT:
|
|
512
|
+
return stack
|
|
513
|
+
|
|
514
|
+
# Taller than a popover is allowed to be. Scroll it. What this
|
|
515
|
+
# did before was cut the overflow off and say nothing, so the
|
|
516
|
+
# buttons below the fold were not merely out of reach - there
|
|
517
|
+
# was no sign they existed. That is how the passphrase form
|
|
518
|
+
# came to show one box out of two.
|
|
519
|
+
stack.setTranslatesAutoresizingMaskIntoConstraints_(True)
|
|
520
|
+
box = NSMakeRect(0, 0, POPOVER_WIDTH, POPOVER_MAX_HEIGHT)
|
|
521
|
+
scroll = NSScrollView.alloc().initWithFrame_(box)
|
|
522
|
+
scroll.setContentView_(TopClip.alloc().initWithFrame_(box))
|
|
523
|
+
scroll.setHasVerticalScroller_(True)
|
|
524
|
+
scroll.setDrawsBackground_(False)
|
|
525
|
+
scroll.setDocumentView_(stack)
|
|
526
|
+
return scroll
|
|
527
|
+
|
|
528
|
+
@objc.python_method
|
|
529
|
+
def buildChangeBody(self, st):
|
|
530
|
+
"""Changing the passphrase gets the whole panel to itself.
|
|
531
|
+
|
|
532
|
+
The ordinary panel already fills POPOVER_MAX_HEIGHT, and this
|
|
533
|
+
popover clips instead of scrolling. Appending the fields to the
|
|
534
|
+
bottom of it put the second box, Save, and the error note below
|
|
535
|
+
the visible edge: you saw one box, pressed Return, and the save
|
|
536
|
+
failed its repeat check against an empty field you could neither
|
|
537
|
+
fill nor read the complaint about. A focused view cannot overflow.
|
|
538
|
+
"""
|
|
539
|
+
def secure(placeholder):
|
|
540
|
+
f = NSSecureTextField.alloc().initWithFrame_(
|
|
541
|
+
NSMakeRect(0, 0, CONTENT_WIDTH, 24))
|
|
542
|
+
f.setPlaceholderString_(placeholder)
|
|
543
|
+
f.setTarget_(self)
|
|
544
|
+
f.setAction_("saveChangePw:") # Return saves
|
|
545
|
+
return f
|
|
546
|
+
|
|
547
|
+
self.pw_new = secure("New passphrase")
|
|
548
|
+
self.pw_repeat = secure("Repeat it")
|
|
549
|
+
views = [row(label("Change password", 15, bold=True), _spacer()),
|
|
550
|
+
label(summarise(st), 11, secondary=True),
|
|
551
|
+
divider(),
|
|
552
|
+
row(self.pw_new),
|
|
553
|
+
row(self.pw_repeat)]
|
|
554
|
+
if self.change_note:
|
|
555
|
+
views.append(label(self.change_note, 11, secondary=True,
|
|
556
|
+
wrap=True))
|
|
557
|
+
views.append(label("Both boxes must match. There is no recovery "
|
|
558
|
+
"phrase - if you forget this, the memories are "
|
|
559
|
+
"unrecoverable.", 10, secondary=True, wrap=True))
|
|
560
|
+
save_b = NSButton.buttonWithTitle_target_action_("Save", self,
|
|
561
|
+
"saveChangePw:")
|
|
562
|
+
save_b.setKeyEquivalent_("\r")
|
|
563
|
+
cancel_b = NSButton.buttonWithTitle_target_action_(
|
|
564
|
+
"Cancel", self, "cancelChangePw:")
|
|
565
|
+
views.append(row(save_b, cancel_b, _spacer()))
|
|
566
|
+
return self._finish(views)
|
|
567
|
+
|
|
488
568
|
def buildBody(self):
|
|
489
569
|
st = self.state
|
|
570
|
+
if self.changing_pw and st["exists"] and not st["locked"]:
|
|
571
|
+
return self.buildChangeBody(st)
|
|
490
572
|
views = []
|
|
491
573
|
title = row(label("Compartment", 15, bold=True),
|
|
492
574
|
label("locked" if st["locked"] else "unlocked", 11,
|
|
@@ -516,33 +598,11 @@ def run(vault: str | None = None, show: bool = False,
|
|
|
516
598
|
views.append(label("Stays unlocked until restart or Lock",
|
|
517
599
|
10, secondary=True))
|
|
518
600
|
|
|
519
|
-
# Changing the passphrase
|
|
520
|
-
# re-wraps a master key it can only hold while unlocked.
|
|
601
|
+
# Changing the passphrase is a separate view: see buildChangeBody.
|
|
521
602
|
self.pw_new = self.pw_repeat = None
|
|
522
|
-
if
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
NSMakeRect(0, 0, CONTENT_WIDTH - 92, 24))
|
|
526
|
-
f.setPlaceholderString_(placeholder)
|
|
527
|
-
f.setTarget_(self)
|
|
528
|
-
f.setAction_("saveChangePw:") # Return saves
|
|
529
|
-
return f
|
|
530
|
-
|
|
531
|
-
self.pw_new = secure("New passphrase")
|
|
532
|
-
self.pw_repeat = secure("Repeat it")
|
|
533
|
-
save_b = NSButton.buttonWithTitle_target_action_(
|
|
534
|
-
"Save", self, "saveChangePw:")
|
|
535
|
-
cancel_b = NSButton.buttonWithTitle_target_action_(
|
|
536
|
-
"Cancel", self, "cancelChangePw:")
|
|
537
|
-
views.append(self.pw_new)
|
|
538
|
-
views.append(row(self.pw_repeat, save_b))
|
|
539
|
-
views.append(row(cancel_b, _spacer()))
|
|
540
|
-
if self.change_note:
|
|
541
|
-
views.append(label(self.change_note, 11, secondary=True,
|
|
542
|
-
wrap=True))
|
|
543
|
-
views.append(label("There is no recovery phrase. If you forget "
|
|
544
|
-
"this, the memories are unrecoverable.",
|
|
545
|
-
10, secondary=True, wrap=True))
|
|
603
|
+
if self.change_note:
|
|
604
|
+
views.append(label(self.change_note, 11, secondary=True,
|
|
605
|
+
wrap=True))
|
|
546
606
|
views.append(divider())
|
|
547
607
|
|
|
548
608
|
views.append(label("SETTINGS", 10, bold=True, secondary=True))
|
|
@@ -613,17 +673,7 @@ def run(vault: str | None = None, show: bool = False,
|
|
|
613
673
|
else:
|
|
614
674
|
views.append(row(refresh, _spacer(), quit_b))
|
|
615
675
|
|
|
616
|
-
|
|
617
|
-
stack.setOrientation_(_VERTICAL)
|
|
618
|
-
stack.setAlignment_(_LEADING)
|
|
619
|
-
stack.setSpacing_(6)
|
|
620
|
-
stack.setEdgeInsets_((CONTENT_INSET, CONTENT_INSET,
|
|
621
|
-
CONTENT_INSET, CONTENT_INSET))
|
|
622
|
-
stack.layoutSubtreeIfNeeded()
|
|
623
|
-
fit = stack.fittingSize()
|
|
624
|
-
stack.setFrameSize_((POPOVER_WIDTH,
|
|
625
|
-
min(fit.height, POPOVER_MAX_HEIGHT)))
|
|
626
|
-
return stack
|
|
676
|
+
return self._finish(views)
|
|
627
677
|
|
|
628
678
|
# Not an action: PyObjC infers a selector from the method name, and
|
|
629
679
|
# a name with no trailing underscores means "takes no arguments".
|
|
@@ -152,6 +152,18 @@ def set_login(enabled: bool) -> str:
|
|
|
152
152
|
return f"error: {exc}"
|
|
153
153
|
|
|
154
154
|
|
|
155
|
+
def panel_geometry(content: int, maximum: int) -> tuple[int, bool]:
|
|
156
|
+
"""How tall to draw the panel, and whether it needs a scrollbar.
|
|
157
|
+
|
|
158
|
+
Split out from the Tk code so the rule is checked on every OS in CI: any
|
|
159
|
+
content taller than the panel scrolls. It is never cut off - the panel
|
|
160
|
+
losing its bottom silently is the whole reason this function exists.
|
|
161
|
+
"""
|
|
162
|
+
if content > maximum:
|
|
163
|
+
return maximum, True
|
|
164
|
+
return max(content, 1), False
|
|
165
|
+
|
|
166
|
+
|
|
155
167
|
# --- the app ---------------------------------------------------------------
|
|
156
168
|
|
|
157
169
|
def run(vault: str | None = None, show: bool = False,
|
|
@@ -192,14 +204,53 @@ def run(vault: str | None = None, show: bool = False,
|
|
|
192
204
|
root.withdraw() # no stray empty window
|
|
193
205
|
panel: dict = {"win": None, "note": None}
|
|
194
206
|
|
|
195
|
-
def
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
state = fetch_state(vault_path)
|
|
199
|
-
frame = ttk.Frame(win, padding=14)
|
|
200
|
-
frame.pack(fill="both", expand=True)
|
|
207
|
+
def _content(frame, state) -> None:
|
|
208
|
+
"""Everything the panel shows. Packs into `frame`, never sizes it -
|
|
209
|
+
deciding how tall the window gets is build()'s job, below."""
|
|
201
210
|
s = state["settings"]
|
|
202
211
|
|
|
212
|
+
# Changing the passphrase gets the whole panel to itself, rather than
|
|
213
|
+
# being appended below a panel that is already full. Two boxes, Save
|
|
214
|
+
# and the reason the last attempt failed all belong on screen at once.
|
|
215
|
+
if state["exists"] and not state["locked"] and panel.get("changing"):
|
|
216
|
+
ttk.Label(frame, text="Change password", wraplength=WRAP,
|
|
217
|
+
font=("Segoe UI", 10, "bold")).pack(anchor="w")
|
|
218
|
+
ttk.Label(frame, text=summarise(state), wraplength=WRAP,
|
|
219
|
+
foreground="#666").pack(anchor="w", pady=(2, 0))
|
|
220
|
+
ttk.Separator(frame).pack(fill="x", pady=10)
|
|
221
|
+
new = ttk.Entry(frame, show="•", width=34)
|
|
222
|
+
new.pack(anchor="w")
|
|
223
|
+
rep = ttk.Entry(frame, show="•", width=34)
|
|
224
|
+
rep.pack(anchor="w", pady=(6, 0))
|
|
225
|
+
new.focus_set()
|
|
226
|
+
|
|
227
|
+
def do_change(*_):
|
|
228
|
+
ok, note = change_passphrase(vault_path, new.get(), rep.get())
|
|
229
|
+
new.delete(0, "end") # never leave one on screen
|
|
230
|
+
rep.delete(0, "end")
|
|
231
|
+
panel["changing"] = not ok
|
|
232
|
+
panel["change_note"] = note
|
|
233
|
+
refresh()
|
|
234
|
+
|
|
235
|
+
new.bind("<Return>", do_change)
|
|
236
|
+
rep.bind("<Return>", do_change)
|
|
237
|
+
if panel.get("change_note"):
|
|
238
|
+
ttk.Label(frame, text=panel["change_note"], wraplength=WRAP,
|
|
239
|
+
foreground="#b00020").pack(anchor="w", pady=(8, 0))
|
|
240
|
+
ttk.Label(frame, text="Both boxes must match. There is no recovery "
|
|
241
|
+
"phrase - if you forget this, the memories "
|
|
242
|
+
"are unrecoverable.",
|
|
243
|
+
foreground="#666", wraplength=WRAP,
|
|
244
|
+
justify="left").pack(anchor="w", pady=(8, 0))
|
|
245
|
+
bar = ttk.Frame(frame)
|
|
246
|
+
bar.pack(fill="x", pady=(10, 0))
|
|
247
|
+
ttk.Button(bar, text="Save", command=do_change).pack(side="left")
|
|
248
|
+
ttk.Button(bar, text="Cancel",
|
|
249
|
+
command=lambda: (panel.update(changing=False,
|
|
250
|
+
change_note=None),
|
|
251
|
+
refresh())).pack(side="left", padx=6)
|
|
252
|
+
return
|
|
253
|
+
|
|
203
254
|
ttk.Label(frame, text=summarise(state), wraplength=WRAP,
|
|
204
255
|
font=("Segoe UI", 10, "bold")).pack(anchor="w")
|
|
205
256
|
if state.get("error"):
|
|
@@ -275,43 +326,7 @@ def run(vault: str | None = None, show: bool = False,
|
|
|
275
326
|
wraplength=WRAP,
|
|
276
327
|
justify="left").pack(anchor="w", pady=(4, 0))
|
|
277
328
|
|
|
278
|
-
if
|
|
279
|
-
ttk.Separator(frame).pack(fill="x", pady=10)
|
|
280
|
-
ttk.Label(frame, text="CHANGE PASSWORD",
|
|
281
|
-
font=("Segoe UI", 8, "bold")).pack(anchor="w")
|
|
282
|
-
new = ttk.Entry(frame, show="•", width=34)
|
|
283
|
-
new.pack(anchor="w", pady=(6, 0))
|
|
284
|
-
rep = ttk.Entry(frame, show="•", width=34)
|
|
285
|
-
rep.pack(anchor="w", pady=(4, 0))
|
|
286
|
-
new.focus_set()
|
|
287
|
-
|
|
288
|
-
def do_change(*_):
|
|
289
|
-
ok, note = change_passphrase(vault_path, new.get(), rep.get())
|
|
290
|
-
# Never leave a passphrase sitting in a field on screen.
|
|
291
|
-
new.delete(0, "end")
|
|
292
|
-
rep.delete(0, "end")
|
|
293
|
-
panel["changing"] = not ok
|
|
294
|
-
panel["change_note"] = note
|
|
295
|
-
refresh()
|
|
296
|
-
|
|
297
|
-
new.bind("<Return>", do_change)
|
|
298
|
-
rep.bind("<Return>", do_change)
|
|
299
|
-
bar = ttk.Frame(frame)
|
|
300
|
-
bar.pack(fill="x", pady=(6, 0))
|
|
301
|
-
ttk.Button(bar, text="Save", command=do_change).pack(side="left")
|
|
302
|
-
ttk.Button(bar, text="Cancel",
|
|
303
|
-
command=lambda: (panel.update(changing=False,
|
|
304
|
-
change_note=None),
|
|
305
|
-
refresh())).pack(side="left", padx=6)
|
|
306
|
-
if panel.get("change_note"):
|
|
307
|
-
ttk.Label(frame, text=panel["change_note"],
|
|
308
|
-
wraplength=WRAP).pack(anchor="w",
|
|
309
|
-
pady=(4, 0))
|
|
310
|
-
ttk.Label(frame, text="There is no recovery phrase. If you forget "
|
|
311
|
-
"this, the memories are unrecoverable.",
|
|
312
|
-
foreground="#666", wraplength=WRAP,
|
|
313
|
-
justify="left").pack(anchor="w", pady=(4, 0))
|
|
314
|
-
elif panel.get("change_note"):
|
|
329
|
+
if panel.get("change_note"): # result of the last attempt
|
|
315
330
|
ttk.Label(frame, text=panel["change_note"],
|
|
316
331
|
wraplength=WRAP).pack(anchor="w", pady=(8, 0))
|
|
317
332
|
|
|
@@ -332,10 +347,56 @@ def run(vault: str | None = None, show: bool = False,
|
|
|
332
347
|
refresh())).pack(side="left", padx=6)
|
|
333
348
|
ttk.Button(buttons, text="Quit", command=quit_app).pack(side="right")
|
|
334
349
|
|
|
350
|
+
def build(win) -> None:
|
|
351
|
+
"""Fill the window, and give it a scrollbar if the content overflows.
|
|
352
|
+
|
|
353
|
+
The content goes inside a canvas rather than straight into the window.
|
|
354
|
+
A fixed-height window cut its own bottom off and said nothing about
|
|
355
|
+
it, which is how the passphrase form came to show one box out of two:
|
|
356
|
+
the second box and the Save button were not merely out of reach, there
|
|
357
|
+
was no sign on screen that they existed at all.
|
|
358
|
+
"""
|
|
359
|
+
for child in win.winfo_children():
|
|
360
|
+
child.destroy()
|
|
361
|
+
state = fetch_state(vault_path)
|
|
362
|
+
|
|
363
|
+
canvas = tk.Canvas(win, highlightthickness=0, borderwidth=0, width=PW)
|
|
364
|
+
try: # match the themed background
|
|
365
|
+
canvas.configure(background=ttk.Style().lookup("TFrame",
|
|
366
|
+
"background"))
|
|
367
|
+
except tk.TclError: # a theme without one: leave it
|
|
368
|
+
pass
|
|
369
|
+
vbar = ttk.Scrollbar(win, orient="vertical", command=canvas.yview)
|
|
370
|
+
canvas.configure(yscrollcommand=vbar.set)
|
|
371
|
+
canvas.pack(side="left", fill="both", expand=True)
|
|
372
|
+
frame = ttk.Frame(canvas, padding=14)
|
|
373
|
+
canvas.create_window((0, 0), window=frame, anchor="nw", width=PW)
|
|
374
|
+
|
|
375
|
+
_content(frame, state)
|
|
376
|
+
|
|
377
|
+
frame.update_idletasks()
|
|
378
|
+
need = frame.winfo_reqheight()
|
|
379
|
+
height, scrolling = panel_geometry(need, PMH)
|
|
380
|
+
canvas.configure(height=height, scrollregion=(0, 0, PW, need))
|
|
381
|
+
if scrolling:
|
|
382
|
+
vbar.pack(side="right", fill="y")
|
|
383
|
+
# Bound on the window, not the canvas: the content covers the
|
|
384
|
+
# canvas, so the wheel event never reaches it. Windows reports
|
|
385
|
+
# the delta in multiples of 120.
|
|
386
|
+
win.bind("<MouseWheel>",
|
|
387
|
+
lambda e: canvas.yview_scroll(-(e.delta // 120), "units"))
|
|
388
|
+
vbar.update_idletasks()
|
|
389
|
+
panel["scroll_w"] = vbar.winfo_reqwidth()
|
|
390
|
+
else:
|
|
391
|
+
win.unbind("<MouseWheel>")
|
|
392
|
+
panel["scroll_w"] = 0
|
|
393
|
+
|
|
335
394
|
def place(win) -> None:
|
|
336
395
|
"""Bottom right, above the taskbar, where the tray icon is."""
|
|
337
396
|
win.update_idletasks()
|
|
338
|
-
|
|
397
|
+
# The scrollbar sits beside the content, so widen the window by it
|
|
398
|
+
# rather than letting it eat a strip off the right of every line.
|
|
399
|
+
w = PW + panel.get("scroll_w", 0)
|
|
339
400
|
h = min(win.winfo_reqheight(), PMH)
|
|
340
401
|
x = win.winfo_screenwidth() - w - 12
|
|
341
402
|
y = win.winfo_screenheight() - h - TBM
|
|
@@ -1,7 +1,30 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: compartment
|
|
3
|
-
Version: 2
|
|
3
|
+
Version: 2.1
|
|
4
4
|
Summary: Compartment - high-security, fully offline, encrypted vector memory for AI agents (MCP)
|
|
5
|
+
Project-URL: Homepage, https://github.com/MaxFreedomPollard/Compartment
|
|
6
|
+
Project-URL: Source, https://github.com/MaxFreedomPollard/Compartment
|
|
7
|
+
Project-URL: Issues, https://github.com/MaxFreedomPollard/Compartment/issues
|
|
8
|
+
Project-URL: Changelog, https://github.com/MaxFreedomPollard/Compartment/releases
|
|
9
|
+
Project-URL: Documentation, https://github.com/MaxFreedomPollard/Compartment#readme
|
|
10
|
+
Project-URL: Security, https://github.com/MaxFreedomPollard/Compartment/blob/main/SECURITY.md
|
|
11
|
+
Keywords: mcp,model-context-protocol,memory,agent-memory,vector-database,semantic-search,encryption,offline,local-first,claude,claude-code,privacy,rag
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Environment :: Console
|
|
15
|
+
Classifier: Environment :: MacOS X
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Operating System :: MacOS :: MacOS X
|
|
18
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
19
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
20
|
+
Classifier: Programming Language :: Python :: 3
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
24
|
+
Classifier: Topic :: Security :: Cryptography
|
|
25
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
26
|
+
Classifier: Topic :: Database :: Database Engines/Servers
|
|
27
|
+
Classifier: Typing :: Typed
|
|
5
28
|
Requires-Python: >=3.11
|
|
6
29
|
Description-Content-Type: text/markdown
|
|
7
30
|
Requires-Dist: pynacl>=1.5
|
|
@@ -152,3 +152,33 @@ def test_login_helpers_report_instead_of_raising_off_windows():
|
|
|
152
152
|
# No winreg here, so both must degrade to a message, not an exception.
|
|
153
153
|
assert isinstance(systray.login_status(), str)
|
|
154
154
|
assert systray.set_login(True).startswith("error:")
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
# The panel used to draw itself at min(content, PANEL_MAX_HEIGHT) and simply
|
|
158
|
+
# lose whatever fell below the line - no scrollbar, no ellipsis, nothing. The
|
|
159
|
+
# change-password form was the casualty: it rendered one of its two boxes,
|
|
160
|
+
# and the Save button and the "the two entries do not match" complaint were
|
|
161
|
+
# both below the fold, so the form looked like it worked and changed nothing.
|
|
162
|
+
def test_content_that_fits_is_drawn_whole_and_needs_no_scrollbar():
|
|
163
|
+
h, scrolling = systray.panel_geometry(300, systray.PANEL_MAX_HEIGHT)
|
|
164
|
+
assert (h, scrolling) == (300, False)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def test_content_taller_than_the_panel_scrolls_rather_than_being_cut_off():
|
|
168
|
+
tall = systray.PANEL_MAX_HEIGHT * 3
|
|
169
|
+
h, scrolling = systray.panel_geometry(tall, systray.PANEL_MAX_HEIGHT)
|
|
170
|
+
assert scrolling, "overflow must scroll, never be silently clipped"
|
|
171
|
+
assert h == systray.PANEL_MAX_HEIGHT
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def test_the_panel_never_exceeds_its_maximum():
|
|
175
|
+
for content in (1, 100, 639, 640, 641, 5000):
|
|
176
|
+
h, _ = systray.panel_geometry(content, systray.PANEL_MAX_HEIGHT)
|
|
177
|
+
assert 0 < h <= systray.PANEL_MAX_HEIGHT, content
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def test_exactly_full_height_does_not_scroll():
|
|
181
|
+
# The boundary: equal to the maximum still fits, so no scrollbar appears.
|
|
182
|
+
assert systray.panel_geometry(systray.PANEL_MAX_HEIGHT,
|
|
183
|
+
systray.PANEL_MAX_HEIGHT) == (
|
|
184
|
+
systray.PANEL_MAX_HEIGHT, 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
|
{compartment-2 → compartment-2.1}/src/compartment/models/bge-small-en-v1.5-int8/model_quantized.onnx
RENAMED
|
File without changes
|
{compartment-2 → compartment-2.1}/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
|