compartment 3.0__tar.gz → 3.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {compartment-3.0 → compartment-3.2}/PKG-INFO +13 -12
- {compartment-3.0 → compartment-3.2}/README.md +9 -8
- {compartment-3.0 → compartment-3.2}/pyproject.toml +17 -11
- {compartment-3.0 → compartment-3.2}/src/compartment/__init__.py +1 -1
- {compartment-3.0 → compartment-3.2}/src/compartment/cli.py +211 -0
- {compartment-3.0 → compartment-3.2}/src/compartment/menubar.py +87 -3
- {compartment-3.0 → compartment-3.2}/src/compartment/systray.py +35 -2
- {compartment-3.0 → compartment-3.2}/src/compartment.egg-info/PKG-INFO +13 -12
- {compartment-3.0 → compartment-3.2}/src/compartment.egg-info/SOURCES.txt +1 -0
- {compartment-3.0 → compartment-3.2}/src/compartment.egg-info/requires.txt +7 -7
- {compartment-3.0 → compartment-3.2}/tests/test_offline_selftest_bench.py +8 -1
- {compartment-3.0 → compartment-3.2}/tests/test_session_entropy.py +5 -0
- compartment-3.2/tests/test_status_bar_install.py +160 -0
- {compartment-3.0 → compartment-3.2}/setup.cfg +0 -0
- {compartment-3.0 → compartment-3.2}/src/compartment/acl.py +0 -0
- {compartment-3.0 → compartment-3.2}/src/compartment/audit.py +0 -0
- {compartment-3.0 → compartment-3.2}/src/compartment/bench.py +0 -0
- {compartment-3.0 → compartment-3.2}/src/compartment/claude_hooks.py +0 -0
- {compartment-3.0 → compartment-3.2}/src/compartment/claude_memory.py +0 -0
- {compartment-3.0 → compartment-3.2}/src/compartment/crypto.py +0 -0
- {compartment-3.0 → compartment-3.2}/src/compartment/dash.py +0 -0
- {compartment-3.0 → compartment-3.2}/src/compartment/data/hermes-plugin/__init__.py +0 -0
- {compartment-3.0 → compartment-3.2}/src/compartment/data/hermes-plugin/plugin.yaml +0 -0
- {compartment-3.0 → compartment-3.2}/src/compartment/data/menubar.png +0 -0
- {compartment-3.0 → compartment-3.2}/src/compartment/data/menubar@2x.png +0 -0
- {compartment-3.0 → compartment-3.2}/src/compartment/data/starter.mpack +0 -0
- {compartment-3.0 → compartment-3.2}/src/compartment/data/tray.ico +0 -0
- {compartment-3.0 → compartment-3.2}/src/compartment/embed.py +0 -0
- {compartment-3.0 → compartment-3.2}/src/compartment/home.py +0 -0
- {compartment-3.0 → compartment-3.2}/src/compartment/longmemeval.py +0 -0
- {compartment-3.0 → compartment-3.2}/src/compartment/models/bge-small-en-v1.5-int8/model_quantized.onnx +0 -0
- {compartment-3.0 → compartment-3.2}/src/compartment/models/bge-small-en-v1.5-int8/tokenizer.json +0 -0
- {compartment-3.0 → compartment-3.2}/src/compartment/offline_guard.py +0 -0
- {compartment-3.0 → compartment-3.2}/src/compartment/packs.py +0 -0
- {compartment-3.0 → compartment-3.2}/src/compartment/platforms.py +0 -0
- {compartment-3.0 → compartment-3.2}/src/compartment/salience.py +0 -0
- {compartment-3.0 → compartment-3.2}/src/compartment/selftest.py +0 -0
- {compartment-3.0 → compartment-3.2}/src/compartment/server.py +0 -0
- {compartment-3.0 → compartment-3.2}/src/compartment/session.py +0 -0
- {compartment-3.0 → compartment-3.2}/src/compartment/store.py +0 -0
- {compartment-3.0 → compartment-3.2}/src/compartment/vault.py +0 -0
- {compartment-3.0 → compartment-3.2}/src/compartment/vaultfile.py +0 -0
- {compartment-3.0 → compartment-3.2}/src/compartment/vindex.py +0 -0
- {compartment-3.0 → compartment-3.2}/src/compartment/wire.py +0 -0
- {compartment-3.0 → compartment-3.2}/src/compartment.egg-info/dependency_links.txt +0 -0
- {compartment-3.0 → compartment-3.2}/src/compartment.egg-info/entry_points.txt +0 -0
- {compartment-3.0 → compartment-3.2}/src/compartment.egg-info/top_level.txt +0 -0
- {compartment-3.0 → compartment-3.2}/tests/test_acl_audit_packs.py +0 -0
- {compartment-3.0 → compartment-3.2}/tests/test_acl_precedence.py +0 -0
- {compartment-3.0 → compartment-3.2}/tests/test_attempt_pacing.py +0 -0
- {compartment-3.0 → compartment-3.2}/tests/test_audit_chain.py +0 -0
- {compartment-3.0 → compartment-3.2}/tests/test_claude_hooks_and_integrate.py +0 -0
- {compartment-3.0 → compartment-3.2}/tests/test_claude_memory_and_recent.py +0 -0
- {compartment-3.0 → compartment-3.2}/tests/test_crypto.py +0 -0
- {compartment-3.0 → compartment-3.2}/tests/test_dash.py +0 -0
- {compartment-3.0 → compartment-3.2}/tests/test_instructions.py +0 -0
- {compartment-3.0 → compartment-3.2}/tests/test_journal_framing.py +0 -0
- {compartment-3.0 → compartment-3.2}/tests/test_macos_bundle.py +0 -0
- {compartment-3.0 → compartment-3.2}/tests/test_menubar.py +0 -0
- {compartment-3.0 → compartment-3.2}/tests/test_offline_guard_paths.py +0 -0
- {compartment-3.0 → compartment-3.2}/tests/test_ondisk_format.py +0 -0
- {compartment-3.0 → compartment-3.2}/tests/test_pack_trust.py +0 -0
- {compartment-3.0 → compartment-3.2}/tests/test_platforms.py +0 -0
- {compartment-3.0 → compartment-3.2}/tests/test_relations.py +0 -0
- {compartment-3.0 → compartment-3.2}/tests/test_salience.py +0 -0
- {compartment-3.0 → compartment-3.2}/tests/test_session_lock.py +0 -0
- {compartment-3.0 → compartment-3.2}/tests/test_starter_data_quality.py +0 -0
- {compartment-3.0 → compartment-3.2}/tests/test_starter_packs.py +0 -0
- {compartment-3.0 → compartment-3.2}/tests/test_systray.py +0 -0
- {compartment-3.0 → compartment-3.2}/tests/test_tamper.py +0 -0
- {compartment-3.0 → compartment-3.2}/tests/test_twofa.py +0 -0
- {compartment-3.0 → compartment-3.2}/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.2
|
|
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
|
|
@@ -33,13 +33,13 @@ Requires-Dist: onnxruntime>=1.17
|
|
|
33
33
|
Requires-Dist: tokenizers>=0.15
|
|
34
34
|
Requires-Dist: numpy>=1.26
|
|
35
35
|
Requires-Dist: mcp>=1.0
|
|
36
|
+
Requires-Dist: pyobjc-framework-Cocoa>=10.0; sys_platform == "darwin"
|
|
37
|
+
Requires-Dist: pystray>=0.19; sys_platform == "win32"
|
|
38
|
+
Requires-Dist: pillow>=10; sys_platform == "win32"
|
|
36
39
|
Provides-Extra: hnsw
|
|
37
40
|
Requires-Dist: usearch>=2.9; extra == "hnsw"
|
|
38
41
|
Provides-Extra: menubar
|
|
39
|
-
Requires-Dist: pyobjc-framework-Cocoa>=10.0; sys_platform == "darwin" and extra == "menubar"
|
|
40
42
|
Provides-Extra: tray
|
|
41
|
-
Requires-Dist: pystray>=0.19; sys_platform == "win32" and extra == "tray"
|
|
42
|
-
Requires-Dist: pillow>=10; sys_platform == "win32" and extra == "tray"
|
|
43
43
|
Provides-Extra: dev
|
|
44
44
|
Requires-Dist: pytest>=8; extra == "dev"
|
|
45
45
|
Requires-Dist: usearch>=2.9; extra == "dev"
|
|
@@ -203,14 +203,15 @@ never break your editor - and it stays quiet when the vault is locked.
|
|
|
203
203
|
alt="The Compartment panel in the Windows notification area.">
|
|
204
204
|
</p>
|
|
205
205
|
|
|
206
|
-
**A menu bar and system tray app
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
206
|
+
**A menu bar and system tray app, from the same one install.** `pip install
|
|
207
|
+
compartment && compartment init` sets it up on macOS and on Windows: the app
|
|
208
|
+
starts straight away and comes back at every login. There is no separate
|
|
209
|
+
package, no extra to remember and no second command. On the Mac you can
|
|
210
|
+
instead open **Compartment.pkg** from the
|
|
211
|
+
[latest release](https://github.com/MaxFreedomPollard/Compartment/releases/latest),
|
|
212
|
+
which carries Python and every dependency inside it, so there is nothing to
|
|
213
|
+
install first. `compartment init --no-app` skips the app for headless boxes
|
|
214
|
+
and CI. Either way it puts Compartment in the status bar: click the icon and a
|
|
214
215
|
panel shows whether the vault is open, how much it has learned, the three
|
|
215
216
|
settings worth changing day to day (capture hook, whether starter facts join
|
|
216
217
|
searches, auto-lock), and the last five things it remembered. Unlock, lock and
|
|
@@ -157,14 +157,15 @@ never break your editor - and it stays quiet when the vault is locked.
|
|
|
157
157
|
alt="The Compartment panel in the Windows notification area.">
|
|
158
158
|
</p>
|
|
159
159
|
|
|
160
|
-
**A menu bar and system tray app
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
160
|
+
**A menu bar and system tray app, from the same one install.** `pip install
|
|
161
|
+
compartment && compartment init` sets it up on macOS and on Windows: the app
|
|
162
|
+
starts straight away and comes back at every login. There is no separate
|
|
163
|
+
package, no extra to remember and no second command. On the Mac you can
|
|
164
|
+
instead open **Compartment.pkg** from the
|
|
165
|
+
[latest release](https://github.com/MaxFreedomPollard/Compartment/releases/latest),
|
|
166
|
+
which carries Python and every dependency inside it, so there is nothing to
|
|
167
|
+
install first. `compartment init --no-app` skips the app for headless boxes
|
|
168
|
+
and CI. Either way it puts Compartment in the status bar: click the icon and a
|
|
168
169
|
panel shows whether the vault is open, how much it has learned, the three
|
|
169
170
|
settings worth changing day to day (capture hook, whether starter facts join
|
|
170
171
|
searches, auto-lock), and the last five things it remembered. Unlock, lock and
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "compartment"
|
|
7
|
-
version = "3.
|
|
7
|
+
version = "3.2"
|
|
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"
|
|
@@ -36,6 +36,16 @@ dependencies = [
|
|
|
36
36
|
"tokenizers>=0.15",
|
|
37
37
|
"numpy>=1.26",
|
|
38
38
|
"mcp>=1.0",
|
|
39
|
+
# The status bar app ships with every install, on the platforms that have
|
|
40
|
+
# one. It is not an add-on: unlocking, locking and changing the passphrase
|
|
41
|
+
# are things people do through the icon, not through a terminal, so an
|
|
42
|
+
# install that only lays down a CLI is an incomplete install. The markers
|
|
43
|
+
# keep it honest - nothing macOS-only is fetched on Windows, nothing
|
|
44
|
+
# Windows-only on macOS, and Linux (which has no status bar app here)
|
|
45
|
+
# installs neither and compiles nothing.
|
|
46
|
+
"pyobjc-framework-Cocoa>=10.0; sys_platform == 'darwin'",
|
|
47
|
+
"pystray>=0.19; sys_platform == 'win32'",
|
|
48
|
+
"pillow>=10; sys_platform == 'win32'",
|
|
39
49
|
]
|
|
40
50
|
|
|
41
51
|
# PyPI is the repo's declared homepage, so it is a real front door - and
|
|
@@ -64,16 +74,12 @@ engram = "compartment.cli:main"
|
|
|
64
74
|
# `pip install` compiles nothing and installs cleanly everywhere; large
|
|
65
75
|
# corpora add it with `pip install compartment[hnsw]`.
|
|
66
76
|
hnsw = ["usearch>=2.9"]
|
|
67
|
-
# The
|
|
68
|
-
#
|
|
69
|
-
# compartment[
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
# install still compiles nothing: `pip install compartment[tray]`, then
|
|
74
|
-
# `compartment tray`.
|
|
75
|
-
tray = ["pystray>=0.19; sys_platform == 'win32'",
|
|
76
|
-
"pillow>=10; sys_platform == 'win32'"]
|
|
77
|
+
# The status bar app is part of the base install now, so these two are kept
|
|
78
|
+
# only so that an existing `pip install compartment[menubar]` or
|
|
79
|
+
# `compartment[tray]` in someone's notes or script keeps resolving instead of
|
|
80
|
+
# failing on an unknown extra. They add nothing.
|
|
81
|
+
menubar = []
|
|
82
|
+
tray = []
|
|
77
83
|
dev = ["pytest>=8", "usearch>=2.9"]
|
|
78
84
|
|
|
79
85
|
[tool.setuptools.packages.find]
|
|
@@ -11,7 +11,10 @@ import getpass
|
|
|
11
11
|
import hashlib
|
|
12
12
|
import json
|
|
13
13
|
import os
|
|
14
|
+
import shutil
|
|
15
|
+
import subprocess
|
|
14
16
|
import sys
|
|
17
|
+
import time
|
|
15
18
|
import zipfile
|
|
16
19
|
from pathlib import Path
|
|
17
20
|
|
|
@@ -151,10 +154,201 @@ def cmd_init(args) -> None:
|
|
|
151
154
|
"`compartment lock`")
|
|
152
155
|
st = v.status()
|
|
153
156
|
v.save()
|
|
157
|
+
if args.no_app:
|
|
158
|
+
print(" command-line only install (--no-app): no status bar app")
|
|
159
|
+
elif _cli_only_requested():
|
|
160
|
+
print(" command-line only install: no status bar app, no login item")
|
|
161
|
+
print(" add it later with `compartment menubar --login on`")
|
|
162
|
+
else:
|
|
163
|
+
_start_status_bar_app(path)
|
|
154
164
|
print(f"\nVault ready: {st['records']} records, projected RAM "
|
|
155
165
|
f"~{st['projected_ram_mb']}MB. Run `compartment selftest` to verify.")
|
|
156
166
|
|
|
157
167
|
|
|
168
|
+
def _cli_only_requested(seconds: float = 5.0) -> bool:
|
|
169
|
+
"""Offer a way out of the GUI, without making anyone wait for it.
|
|
170
|
+
|
|
171
|
+
A window, not a question: the install carries on by itself when the time
|
|
172
|
+
is up, so someone who is not looking at the terminal loses nothing. Only
|
|
173
|
+
a real keyboard gets asked - a piped or redirected stdin (installer
|
|
174
|
+
script, CI, `yes | ...`) skips this instantly rather than blocking for
|
|
175
|
+
five seconds on input that is never coming.
|
|
176
|
+
"""
|
|
177
|
+
try:
|
|
178
|
+
if not sys.stdin.isatty():
|
|
179
|
+
return False
|
|
180
|
+
except Exception: # noqa: BLE001
|
|
181
|
+
return False
|
|
182
|
+
print('\nThis is a normal install. Press the letter "s" within 5 seconds '
|
|
183
|
+
'if you want to do a command-line only install.')
|
|
184
|
+
try:
|
|
185
|
+
if sys.platform == "win32":
|
|
186
|
+
import msvcrt
|
|
187
|
+
deadline = time.monotonic() + seconds
|
|
188
|
+
while time.monotonic() < deadline:
|
|
189
|
+
if msvcrt.kbhit():
|
|
190
|
+
return msvcrt.getwch().lower() == "s"
|
|
191
|
+
time.sleep(0.05)
|
|
192
|
+
return False
|
|
193
|
+
import select
|
|
194
|
+
import termios
|
|
195
|
+
import tty
|
|
196
|
+
fd = sys.stdin.fileno()
|
|
197
|
+
saved = termios.tcgetattr(fd)
|
|
198
|
+
try:
|
|
199
|
+
tty.setcbreak(fd) # single keypress, no Enter needed
|
|
200
|
+
ready, _, _ = select.select([sys.stdin], [], [], seconds)
|
|
201
|
+
if not ready:
|
|
202
|
+
return False
|
|
203
|
+
return sys.stdin.read(1).lower() == "s"
|
|
204
|
+
finally:
|
|
205
|
+
termios.tcsetattr(fd, termios.TCSADRAIN, saved)
|
|
206
|
+
except Exception: # noqa: BLE001
|
|
207
|
+
return False # never let this block an install
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def _start_status_bar_app(vault: str) -> None:
|
|
211
|
+
"""Put the status bar app up now, and again at every login.
|
|
212
|
+
|
|
213
|
+
This runs as part of `init` because the icon is the product for most
|
|
214
|
+
people: unlocking, locking and changing the passphrase all live there.
|
|
215
|
+
An install that leaves you with only a terminal command is an install
|
|
216
|
+
that has not finished. Never fatal - a headless box, an SSH session or a
|
|
217
|
+
platform with no status bar still gets a perfectly good CLI and MCP
|
|
218
|
+
server, so a failure here is reported and stepped over.
|
|
219
|
+
"""
|
|
220
|
+
if sys.platform not in ("darwin", "win32"):
|
|
221
|
+
return # no status bar app here
|
|
222
|
+
try:
|
|
223
|
+
app = _tray_app()
|
|
224
|
+
except Exception as exc: # noqa: BLE001
|
|
225
|
+
print(f" status bar app unavailable ({exc}); the CLI is unaffected")
|
|
226
|
+
return
|
|
227
|
+
try:
|
|
228
|
+
state = app.set_login(True)
|
|
229
|
+
print(f" start at login: {state}")
|
|
230
|
+
except Exception as exc: # noqa: BLE001
|
|
231
|
+
print(f" could not enable start at login ({exc})")
|
|
232
|
+
try:
|
|
233
|
+
exe = shutil.which("compartment")
|
|
234
|
+
argv = ([exe, "--vault", vault, "menubar"] if exe else
|
|
235
|
+
[sys.executable, "-m", "compartment.cli", "--vault", vault,
|
|
236
|
+
"menubar"])
|
|
237
|
+
kwargs = {"stdout": subprocess.DEVNULL, "stderr": subprocess.DEVNULL}
|
|
238
|
+
if sys.platform == "win32":
|
|
239
|
+
# no console window behind an app that has no window
|
|
240
|
+
kwargs["creationflags"] = 0x00000008 # DETACHED_PROCESS
|
|
241
|
+
else:
|
|
242
|
+
kwargs["start_new_session"] = True # outlive this shell
|
|
243
|
+
subprocess.Popen(argv, **kwargs)
|
|
244
|
+
print(" status bar app started - look for the icon in your "
|
|
245
|
+
+ ("menu bar" if sys.platform == "darwin"
|
|
246
|
+
else "notification area"))
|
|
247
|
+
except Exception as exc: # noqa: BLE001
|
|
248
|
+
print(f" could not start the status bar app ({exc}); run "
|
|
249
|
+
"`compartment menubar` yourself")
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def _install_kind() -> tuple[str, list[str]]:
|
|
253
|
+
"""How this copy was installed, and the command that upgrades it."""
|
|
254
|
+
exe = Path(sys.argv[0]).resolve()
|
|
255
|
+
if "uv/tools" in str(exe) or shutil.which("uv") and "uv/tools" in str(
|
|
256
|
+
Path(shutil.which("compartment") or "").resolve()):
|
|
257
|
+
return "uv", ["uv", "tool", "upgrade", "compartment"]
|
|
258
|
+
return "pip", [sys.executable, "-m", "pip", "install", "--upgrade",
|
|
259
|
+
"compartment"]
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def cmd_update(args) -> None:
|
|
263
|
+
"""Upgrade Compartment in place, then put the app back up."""
|
|
264
|
+
kind, cmd = _install_kind()
|
|
265
|
+
if args.source:
|
|
266
|
+
target = "git+https://github.com/MaxFreedomPollard/Compartment@main"
|
|
267
|
+
cmd = (["uv", "tool", "install", "--force", target] if kind == "uv"
|
|
268
|
+
else [sys.executable, "-m", "pip", "install", "--upgrade",
|
|
269
|
+
"--force-reinstall", target])
|
|
270
|
+
print(f"Compartment {__version__} - updating ({kind} install)")
|
|
271
|
+
print(" " + " ".join(cmd))
|
|
272
|
+
try:
|
|
273
|
+
res = subprocess.run(cmd, timeout=1800)
|
|
274
|
+
except (OSError, subprocess.SubprocessError) as exc:
|
|
275
|
+
_die(f"update failed to run: {exc}")
|
|
276
|
+
if res.returncode != 0:
|
|
277
|
+
_die(f"update failed (exit {res.returncode})")
|
|
278
|
+
ver = "?"
|
|
279
|
+
try:
|
|
280
|
+
out = subprocess.run([shutil.which("compartment") or "compartment",
|
|
281
|
+
"--version"], capture_output=True, text=True,
|
|
282
|
+
timeout=120)
|
|
283
|
+
ver = (out.stdout or "").strip() or "?"
|
|
284
|
+
except (OSError, subprocess.SubprocessError):
|
|
285
|
+
pass
|
|
286
|
+
print(f" updated: now {ver}")
|
|
287
|
+
print(" your vault and settings are untouched")
|
|
288
|
+
if not args.no_app and sys.platform in ("darwin", "win32"):
|
|
289
|
+
_restart_status_bar_app(args.vault)
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def _restart_status_bar_app(vault: str) -> None:
|
|
293
|
+
"""Stop the running status bar app and start the new build."""
|
|
294
|
+
try:
|
|
295
|
+
app = _tray_app()
|
|
296
|
+
app.quit_running()
|
|
297
|
+
except Exception: # noqa: BLE001
|
|
298
|
+
pass
|
|
299
|
+
_start_status_bar_app(vault)
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def cmd_uninstall(args) -> None:
|
|
303
|
+
"""Remove Compartment from this machine. The vault is KEPT unless asked."""
|
|
304
|
+
print(f"Compartment {__version__} - uninstalling")
|
|
305
|
+
if sys.platform in ("darwin", "win32"):
|
|
306
|
+
try:
|
|
307
|
+
app = _tray_app()
|
|
308
|
+
try:
|
|
309
|
+
app.quit_running()
|
|
310
|
+
print(" status bar app stopped")
|
|
311
|
+
except Exception: # noqa: BLE001
|
|
312
|
+
pass
|
|
313
|
+
print(f" start at login: {app.set_login(False)}")
|
|
314
|
+
except Exception as exc: # noqa: BLE001
|
|
315
|
+
print(f" status bar app: nothing to remove ({exc})")
|
|
316
|
+
try:
|
|
317
|
+
if claude_hooks.is_installed():
|
|
318
|
+
claude_hooks.uninstall()
|
|
319
|
+
print(" Claude Code capture hook removed")
|
|
320
|
+
except Exception as exc: # noqa: BLE001
|
|
321
|
+
print(f" capture hook: {exc}")
|
|
322
|
+
from .home import LEGACY_NAME, NAME
|
|
323
|
+
for name in (NAME, LEGACY_NAME):
|
|
324
|
+
try:
|
|
325
|
+
r = subprocess.run(["claude", "mcp", "remove", name],
|
|
326
|
+
capture_output=True, text=True, timeout=120)
|
|
327
|
+
if r.returncode == 0:
|
|
328
|
+
print(f" MCP registration removed: {name}")
|
|
329
|
+
except (OSError, subprocess.SubprocessError):
|
|
330
|
+
pass
|
|
331
|
+
if args.purge:
|
|
332
|
+
# only on an explicit --purge: this is the only copy of everything
|
|
333
|
+
# the agent has ever learned, and it is not recoverable.
|
|
334
|
+
for f in (args.vault, args.vault + ".config.json",
|
|
335
|
+
args.vault + ".flock"):
|
|
336
|
+
try:
|
|
337
|
+
os.remove(f)
|
|
338
|
+
print(f" deleted {f}")
|
|
339
|
+
except OSError:
|
|
340
|
+
pass
|
|
341
|
+
print(" vault deleted (--purge)")
|
|
342
|
+
else:
|
|
343
|
+
print(f" vault KEPT at {args.vault}")
|
|
344
|
+
print(" delete it yourself, or re-run with --purge, once you are sure")
|
|
345
|
+
kind, _ = _install_kind()
|
|
346
|
+
final = ("uv tool uninstall compartment" if kind == "uv"
|
|
347
|
+
else f"{sys.executable} -m pip uninstall compartment")
|
|
348
|
+
print("\nOne step left, which cannot remove itself while it is running:")
|
|
349
|
+
print(f" {final}")
|
|
350
|
+
|
|
351
|
+
|
|
158
352
|
def _ask_yn(q: str) -> bool:
|
|
159
353
|
if not sys.stdin.isatty():
|
|
160
354
|
return False
|
|
@@ -1047,6 +1241,9 @@ def main(argv: list[str] | None = None) -> None:
|
|
|
1047
1241
|
help="store a reboot-surviving Keychain credential (macOS)")
|
|
1048
1242
|
p.add_argument("--no-session", action="store_true",
|
|
1049
1243
|
help="do not stay unlocked after init")
|
|
1244
|
+
p.add_argument("--no-app", action="store_true",
|
|
1245
|
+
help="do not start the status bar app or enable it at "
|
|
1246
|
+
"login (headless installs, CI)")
|
|
1050
1247
|
p.set_defaults(fn=cmd_init)
|
|
1051
1248
|
|
|
1052
1249
|
p = sub.add_parser(
|
|
@@ -1279,6 +1476,20 @@ def main(argv: list[str] | None = None) -> None:
|
|
|
1279
1476
|
help="only packs signed by the project are trusted by default. Name an author's 64 hex character public key to trust it deliberately; repeatable")
|
|
1280
1477
|
p.set_defaults(fn=cmd_pack_export)
|
|
1281
1478
|
|
|
1479
|
+
p = sub.add_parser("update", help="upgrade Compartment in place")
|
|
1480
|
+
p.add_argument("--source", action="store_true",
|
|
1481
|
+
help="update from the GitHub main branch instead of PyPI")
|
|
1482
|
+
p.add_argument("--no-app", action="store_true",
|
|
1483
|
+
help="do not restart the status bar app afterwards")
|
|
1484
|
+
p.set_defaults(fn=cmd_update)
|
|
1485
|
+
|
|
1486
|
+
p = sub.add_parser("uninstall",
|
|
1487
|
+
help="remove Compartment from this machine "
|
|
1488
|
+
"(the vault is kept unless --purge)")
|
|
1489
|
+
p.add_argument("--purge", action="store_true",
|
|
1490
|
+
help="ALSO delete the vault and its config, permanently")
|
|
1491
|
+
p.set_defaults(fn=cmd_uninstall)
|
|
1492
|
+
|
|
1282
1493
|
p = sub.add_parser("integrate",
|
|
1283
1494
|
help="one-command wiring into claude, hermes, or openclaw")
|
|
1284
1495
|
p.add_argument("target", choices=["claude", "hermes", "openclaw"])
|
|
@@ -307,7 +307,13 @@ def _app_service():
|
|
|
307
307
|
from Foundation import NSBundle
|
|
308
308
|
except ImportError:
|
|
309
309
|
return None
|
|
310
|
-
|
|
310
|
+
# It has to be OUR bundle, not merely SOME bundle. A framework Python
|
|
311
|
+
# reports bundleIdentifier "org.python.python" with a bundle path inside
|
|
312
|
+
# Python.framework, so a bare "is there an identifier" test passes on an
|
|
313
|
+
# ordinary pip install. SMAppService would then act on Python.app: status
|
|
314
|
+
# comes back "not found", and registering would put PYTHON in the user's
|
|
315
|
+
# login items instead of Compartment.
|
|
316
|
+
if (NSBundle.mainBundle().bundleIdentifier() or "") != BUNDLE_ID:
|
|
311
317
|
return None
|
|
312
318
|
try:
|
|
313
319
|
objc.loadBundle(
|
|
@@ -318,10 +324,66 @@ def _app_service():
|
|
|
318
324
|
return None
|
|
319
325
|
|
|
320
326
|
|
|
327
|
+
# A pip install has no app bundle, and SMAppService can only register one. So
|
|
328
|
+
# the bundle path is used when there is a bundle, and a LaunchAgent otherwise.
|
|
329
|
+
# Without this, `pip install compartment` could never start at login on macOS,
|
|
330
|
+
# which would make the status bar app a second-class citizen of the very
|
|
331
|
+
# install most people use.
|
|
332
|
+
# Must match tools/build_macos_app.py: the identity of the real app bundle.
|
|
333
|
+
BUNDLE_ID = "io.github.maxfreedompollard.compartment"
|
|
334
|
+
LAUNCH_AGENT_LABEL = f"{BUNDLE_ID}.menubar"
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
def _agent_plist() -> Path:
|
|
338
|
+
return Path.home() / "Library" / "LaunchAgents" / f"{LAUNCH_AGENT_LABEL}.plist"
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def _launcher_argv() -> list[str]:
|
|
342
|
+
"""How to start the status bar app again later, from wherever we live."""
|
|
343
|
+
exe = shutil.which("compartment")
|
|
344
|
+
if exe:
|
|
345
|
+
return [exe, "menubar"]
|
|
346
|
+
return [sys.executable, "-m", "compartment.cli", "menubar"]
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
def _set_login_agent(enabled: bool) -> str:
|
|
350
|
+
plist = _agent_plist()
|
|
351
|
+
if not enabled:
|
|
352
|
+
try:
|
|
353
|
+
subprocess.run(["launchctl", "unload", str(plist)],
|
|
354
|
+
capture_output=True, timeout=10)
|
|
355
|
+
except (OSError, subprocess.SubprocessError):
|
|
356
|
+
pass
|
|
357
|
+
plist.unlink(missing_ok=True)
|
|
358
|
+
return "off"
|
|
359
|
+
argv = _launcher_argv()
|
|
360
|
+
args = "".join(f" <string>{a}</string>\n" for a in argv)
|
|
361
|
+
plist.parent.mkdir(parents=True, exist_ok=True)
|
|
362
|
+
plist.write_text(
|
|
363
|
+
'<?xml version="1.0" encoding="UTF-8"?>\n'
|
|
364
|
+
'<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" '
|
|
365
|
+
'"http://www.apple.com/DTDs/PropertyList-1.0.dtd">\n'
|
|
366
|
+
'<plist version="1.0">\n'
|
|
367
|
+
' <dict>\n'
|
|
368
|
+
f' <key>Label</key><string>{LAUNCH_AGENT_LABEL}</string>\n'
|
|
369
|
+
' <key>ProgramArguments</key>\n'
|
|
370
|
+
f' <array>\n{args} </array>\n'
|
|
371
|
+
' <key>RunAtLoad</key><true/>\n'
|
|
372
|
+
' <key>KeepAlive</key><false/>\n'
|
|
373
|
+
' </dict>\n'
|
|
374
|
+
'</plist>\n', encoding="utf-8")
|
|
375
|
+
try:
|
|
376
|
+
subprocess.run(["launchctl", "load", str(plist)],
|
|
377
|
+
capture_output=True, timeout=10)
|
|
378
|
+
except (OSError, subprocess.SubprocessError) as exc:
|
|
379
|
+
return f"failed: {exc}"
|
|
380
|
+
return "on"
|
|
381
|
+
|
|
382
|
+
|
|
321
383
|
def login_status() -> str:
|
|
322
384
|
svc = _app_service()
|
|
323
385
|
if svc is None:
|
|
324
|
-
return "
|
|
386
|
+
return "on" if _agent_plist().is_file() else "off"
|
|
325
387
|
return LOGIN_STATUS.get(svc.status(), str(svc.status()))
|
|
326
388
|
|
|
327
389
|
|
|
@@ -330,7 +392,7 @@ def set_login(enabled: bool) -> str:
|
|
|
330
392
|
name with its icon instead of an anonymous "legacy agent" entry."""
|
|
331
393
|
svc = _app_service()
|
|
332
394
|
if svc is None:
|
|
333
|
-
return
|
|
395
|
+
return _set_login_agent(enabled) # pip install: no bundle to register
|
|
334
396
|
try:
|
|
335
397
|
res = (svc.registerAndReturnError_(None) if enabled
|
|
336
398
|
else svc.unregisterAndReturnError_(None))
|
|
@@ -351,6 +413,28 @@ def set_login(enabled: bool) -> str:
|
|
|
351
413
|
return login_status()
|
|
352
414
|
|
|
353
415
|
|
|
416
|
+
def quit_running() -> bool:
|
|
417
|
+
"""Stop a running status bar app, so an update or uninstall does not leave
|
|
418
|
+
the old build sitting in the menu bar with its binary already gone."""
|
|
419
|
+
import signal
|
|
420
|
+
killed = False
|
|
421
|
+
try:
|
|
422
|
+
out = subprocess.run(["pgrep", "-f", "compartment.*menubar"],
|
|
423
|
+
capture_output=True, text=True, timeout=15).stdout
|
|
424
|
+
except (OSError, subprocess.SubprocessError):
|
|
425
|
+
return False
|
|
426
|
+
me = os.getpid()
|
|
427
|
+
for pid in [int(x) for x in out.split() if x.isdigit()]:
|
|
428
|
+
if pid == me:
|
|
429
|
+
continue
|
|
430
|
+
try:
|
|
431
|
+
os.kill(pid, signal.SIGTERM)
|
|
432
|
+
killed = True
|
|
433
|
+
except OSError:
|
|
434
|
+
pass
|
|
435
|
+
return killed
|
|
436
|
+
|
|
437
|
+
|
|
354
438
|
# ----------------------------------------------------------------- self test
|
|
355
439
|
|
|
356
440
|
def self_check(vault: str | None = None) -> int:
|
|
@@ -23,9 +23,12 @@ Design notes:
|
|
|
23
23
|
from __future__ import annotations
|
|
24
24
|
|
|
25
25
|
import os
|
|
26
|
+
import shutil
|
|
26
27
|
import sys
|
|
27
28
|
from pathlib import Path
|
|
28
29
|
|
|
30
|
+
from .home import env, home
|
|
31
|
+
|
|
29
32
|
from .menubar import (AUTO_LOCK_CHOICES, RECENT_COUNT, auto_lock_label,
|
|
30
33
|
change_passphrase, claim_first_run, default_vault,
|
|
31
34
|
fetch_state, lock_vault, self_check, set_setting,
|
|
@@ -122,6 +125,24 @@ def _winreg():
|
|
|
122
125
|
return winreg
|
|
123
126
|
|
|
124
127
|
|
|
128
|
+
def _autostart_command(vault: str | None = None) -> str:
|
|
129
|
+
"""What Windows runs at sign-in.
|
|
130
|
+
|
|
131
|
+
Two things this has to get right. It launches through the console script
|
|
132
|
+
or pythonw.exe rather than python.exe, because python.exe opens a console
|
|
133
|
+
window behind a tray app that has no window, at every single sign-in. And
|
|
134
|
+
it carries the vault path, because a user running a non-default vault
|
|
135
|
+
would otherwise silently get the default one back after a reboot.
|
|
136
|
+
"""
|
|
137
|
+
vault = vault or env("VAULT") or str(home() / "memory.vault")
|
|
138
|
+
exe = shutil.which("compartment")
|
|
139
|
+
if exe:
|
|
140
|
+
return f'"{exe}" --vault "{vault}" tray'
|
|
141
|
+
pyw = Path(sys.executable).with_name("pythonw.exe")
|
|
142
|
+
runner = str(pyw) if pyw.is_file() else sys.executable
|
|
143
|
+
return f'"{runner}" -m compartment.cli --vault "{vault}" tray'
|
|
144
|
+
|
|
145
|
+
|
|
125
146
|
def login_status() -> str:
|
|
126
147
|
try:
|
|
127
148
|
winreg = _winreg()
|
|
@@ -141,7 +162,7 @@ def set_login(enabled: bool) -> str:
|
|
|
141
162
|
with winreg.CreateKey(winreg.HKEY_CURRENT_USER, RUN_KEY) as k:
|
|
142
163
|
if enabled:
|
|
143
164
|
winreg.SetValueEx(k, RUN_VALUE, 0, winreg.REG_SZ,
|
|
144
|
-
|
|
165
|
+
_autostart_command())
|
|
145
166
|
return "on"
|
|
146
167
|
try:
|
|
147
168
|
winreg.DeleteValue(k, RUN_VALUE)
|
|
@@ -465,5 +486,17 @@ def run(vault: str | None = None, show: bool = False,
|
|
|
465
486
|
return 0
|
|
466
487
|
|
|
467
488
|
|
|
468
|
-
|
|
489
|
+
def quit_running() -> bool:
|
|
490
|
+
"""Stop a running tray app before an update or uninstall replaces it."""
|
|
491
|
+
import subprocess
|
|
492
|
+
try:
|
|
493
|
+
subprocess.run(["taskkill", "/F", "/IM", "compartment.exe"],
|
|
494
|
+
capture_output=True, timeout=30)
|
|
495
|
+
return True
|
|
496
|
+
except (OSError, subprocess.SubprocessError):
|
|
497
|
+
return False
|
|
498
|
+
|
|
499
|
+
|
|
500
|
+
__all__ = ["run", "self_check", "login_status", "set_login", "quit_running",
|
|
501
|
+
"panel_rows",
|
|
469
502
|
"icon_path"]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: compartment
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.2
|
|
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
|
|
@@ -33,13 +33,13 @@ Requires-Dist: onnxruntime>=1.17
|
|
|
33
33
|
Requires-Dist: tokenizers>=0.15
|
|
34
34
|
Requires-Dist: numpy>=1.26
|
|
35
35
|
Requires-Dist: mcp>=1.0
|
|
36
|
+
Requires-Dist: pyobjc-framework-Cocoa>=10.0; sys_platform == "darwin"
|
|
37
|
+
Requires-Dist: pystray>=0.19; sys_platform == "win32"
|
|
38
|
+
Requires-Dist: pillow>=10; sys_platform == "win32"
|
|
36
39
|
Provides-Extra: hnsw
|
|
37
40
|
Requires-Dist: usearch>=2.9; extra == "hnsw"
|
|
38
41
|
Provides-Extra: menubar
|
|
39
|
-
Requires-Dist: pyobjc-framework-Cocoa>=10.0; sys_platform == "darwin" and extra == "menubar"
|
|
40
42
|
Provides-Extra: tray
|
|
41
|
-
Requires-Dist: pystray>=0.19; sys_platform == "win32" and extra == "tray"
|
|
42
|
-
Requires-Dist: pillow>=10; sys_platform == "win32" and extra == "tray"
|
|
43
43
|
Provides-Extra: dev
|
|
44
44
|
Requires-Dist: pytest>=8; extra == "dev"
|
|
45
45
|
Requires-Dist: usearch>=2.9; extra == "dev"
|
|
@@ -203,14 +203,15 @@ never break your editor - and it stays quiet when the vault is locked.
|
|
|
203
203
|
alt="The Compartment panel in the Windows notification area.">
|
|
204
204
|
</p>
|
|
205
205
|
|
|
206
|
-
**A menu bar and system tray app
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
206
|
+
**A menu bar and system tray app, from the same one install.** `pip install
|
|
207
|
+
compartment && compartment init` sets it up on macOS and on Windows: the app
|
|
208
|
+
starts straight away and comes back at every login. There is no separate
|
|
209
|
+
package, no extra to remember and no second command. On the Mac you can
|
|
210
|
+
instead open **Compartment.pkg** from the
|
|
211
|
+
[latest release](https://github.com/MaxFreedomPollard/Compartment/releases/latest),
|
|
212
|
+
which carries Python and every dependency inside it, so there is nothing to
|
|
213
|
+
install first. `compartment init --no-app` skips the app for headless boxes
|
|
214
|
+
and CI. Either way it puts Compartment in the status bar: click the icon and a
|
|
214
215
|
panel shows whether the vault is open, how much it has learned, the three
|
|
215
216
|
settings worth changing day to day (capture hook, whether starter facts join
|
|
216
217
|
searches, auto-lock), and the last five things it remembered. Unlock, lock and
|
|
@@ -5,6 +5,13 @@ tokenizers>=0.15
|
|
|
5
5
|
numpy>=1.26
|
|
6
6
|
mcp>=1.0
|
|
7
7
|
|
|
8
|
+
[:sys_platform == "darwin"]
|
|
9
|
+
pyobjc-framework-Cocoa>=10.0
|
|
10
|
+
|
|
11
|
+
[:sys_platform == "win32"]
|
|
12
|
+
pystray>=0.19
|
|
13
|
+
pillow>=10
|
|
14
|
+
|
|
8
15
|
[dev]
|
|
9
16
|
pytest>=8
|
|
10
17
|
usearch>=2.9
|
|
@@ -14,11 +21,4 @@ usearch>=2.9
|
|
|
14
21
|
|
|
15
22
|
[menubar]
|
|
16
23
|
|
|
17
|
-
[menubar:sys_platform == "darwin"]
|
|
18
|
-
pyobjc-framework-Cocoa>=10.0
|
|
19
|
-
|
|
20
24
|
[tray]
|
|
21
|
-
|
|
22
|
-
[tray:sys_platform == "win32"]
|
|
23
|
-
pystray>=0.19
|
|
24
|
-
pillow>=10
|
|
@@ -78,7 +78,14 @@ def test_index_add_remove():
|
|
|
78
78
|
def test_bench_budgets(seeded_vault):
|
|
79
79
|
out = bench.run(seeded_vault, synthetic_n=5000, queries=20)
|
|
80
80
|
assert out["budgets"]["vector_search_p95_under_100ms"], out
|
|
81
|
-
|
|
81
|
+
# None means the platform cannot measure RSS at all (no `resource`
|
|
82
|
+
# module, ie. Windows). It must never read as a pass, and it must not be
|
|
83
|
+
# asserted as one either.
|
|
84
|
+
rss = out["budgets"]["rss_under_1gb"]
|
|
85
|
+
if out["peak_rss_mb"] is None:
|
|
86
|
+
assert rss is None, out
|
|
87
|
+
else:
|
|
88
|
+
assert rss, out
|
|
82
89
|
|
|
83
90
|
|
|
84
91
|
# --------------------------------------------------------------- bench fixes
|
|
@@ -180,6 +180,9 @@ def test_clear_keeps_the_secret_while_another_vault_still_needs_it():
|
|
|
180
180
|
|
|
181
181
|
# --- file permissions ------------------------------------------------------
|
|
182
182
|
|
|
183
|
+
@pytest.mark.skipif(os.name == "nt",
|
|
184
|
+
reason="POSIX mode bits: Windows reports 0o666 for any "
|
|
185
|
+
"writable file whatever mode os.open was given")
|
|
183
186
|
def test_the_file_is_created_0600_under_any_umask():
|
|
184
187
|
old = os.umask(0o000) # the friendliest possible umask
|
|
185
188
|
try:
|
|
@@ -189,6 +192,8 @@ def test_the_file_is_created_0600_under_any_umask():
|
|
|
189
192
|
assert oct(p.stat().st_mode & 0o777) == "0o600"
|
|
190
193
|
|
|
191
194
|
|
|
195
|
+
@pytest.mark.skipif(os.name == "nt",
|
|
196
|
+
reason="POSIX mode bits: see above")
|
|
192
197
|
def test_an_existing_wider_file_is_narrowed_by_the_write():
|
|
193
198
|
"""os.open with a mode does nothing to a file that already exists, so the
|
|
194
199
|
write has to narrow it as well as create it narrow."""
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
"""The status bar app is part of a normal install, on macOS and on Windows.
|
|
2
|
+
|
|
3
|
+
Unlocking, locking and changing the passphrase are things people do through
|
|
4
|
+
the icon. An install that lays down only a CLI has not finished, and until
|
|
5
|
+
this it did not: the GUI dependencies were optional extras, `init` never
|
|
6
|
+
started the app, and on macOS start-at-login was impossible without the .app
|
|
7
|
+
bundle.
|
|
8
|
+
"""
|
|
9
|
+
import sys
|
|
10
|
+
import tomllib
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
import pytest
|
|
14
|
+
|
|
15
|
+
from compartment import menubar
|
|
16
|
+
|
|
17
|
+
ROOT = Path(__file__).resolve().parents[1]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
# --- one install, not two --------------------------------------------------
|
|
21
|
+
|
|
22
|
+
def _pyproject():
|
|
23
|
+
return tomllib.loads((ROOT / "pyproject.toml").read_text(encoding="utf-8"))
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def test_the_gui_deps_ship_with_a_plain_install():
|
|
27
|
+
"""`pip install compartment` has to be enough. No extra, no second step."""
|
|
28
|
+
deps = " ".join(_pyproject()["project"]["dependencies"])
|
|
29
|
+
assert "pyobjc-framework-Cocoa" in deps, "macOS status bar missing"
|
|
30
|
+
assert "pystray" in deps and "pillow" in deps, "Windows tray missing"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def test_the_gui_deps_are_platform_gated():
|
|
34
|
+
"""Nothing macOS-only on Windows, nothing Windows-only on macOS."""
|
|
35
|
+
for dep in _pyproject()["project"]["dependencies"]:
|
|
36
|
+
if "pyobjc" in dep:
|
|
37
|
+
assert "sys_platform == 'darwin'" in dep
|
|
38
|
+
if "pystray" in dep or "pillow" in dep:
|
|
39
|
+
assert "sys_platform == 'win32'" in dep
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def test_the_old_extras_still_resolve():
|
|
43
|
+
"""Someone's script or notes may still say compartment[menubar]. That must
|
|
44
|
+
keep working rather than failing on an unknown extra."""
|
|
45
|
+
extras = _pyproject()["project"]["optional-dependencies"]
|
|
46
|
+
assert extras["menubar"] == [] and extras["tray"] == []
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def test_one_command_covers_both_platforms():
|
|
50
|
+
from compartment import cli
|
|
51
|
+
assert 'aliases=["tray"]' in (ROOT / "src/compartment/cli.py").read_text(
|
|
52
|
+
encoding="utf-8"), "menubar/tray must stay one command"
|
|
53
|
+
assert hasattr(cli, "_start_status_bar_app")
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
# --- macOS start at login, without an app bundle ---------------------------
|
|
57
|
+
|
|
58
|
+
@pytest.fixture()
|
|
59
|
+
def fake_home(tmp_path, monkeypatch):
|
|
60
|
+
monkeypatch.setenv("HOME", str(tmp_path))
|
|
61
|
+
return tmp_path
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@pytest.mark.skipif(sys.platform != "darwin", reason="macOS login item")
|
|
65
|
+
def test_a_pip_install_is_not_mistaken_for_the_app_bundle():
|
|
66
|
+
"""The regression that would put PYTHON in the user's login items.
|
|
67
|
+
|
|
68
|
+
A framework Python reports bundleIdentifier "org.python.python" with a
|
|
69
|
+
bundle path inside Python.framework, so a bare "is there a bundle?" test
|
|
70
|
+
passes on an ordinary pip install and SMAppService then acts on Python.app.
|
|
71
|
+
"""
|
|
72
|
+
assert menubar._app_service() is None
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@pytest.mark.skipif(sys.platform != "darwin", reason="macOS login item")
|
|
76
|
+
def test_start_at_login_round_trips_without_a_bundle(fake_home):
|
|
77
|
+
plist = menubar._agent_plist()
|
|
78
|
+
assert menubar.login_status() == "off"
|
|
79
|
+
assert menubar.set_login(True) == "on"
|
|
80
|
+
assert plist.is_file()
|
|
81
|
+
assert menubar.login_status() == "on"
|
|
82
|
+
assert menubar.set_login(False) == "off"
|
|
83
|
+
assert not plist.exists()
|
|
84
|
+
assert menubar.login_status() == "off"
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@pytest.mark.skipif(sys.platform != "darwin", reason="macOS login item")
|
|
88
|
+
def test_the_login_agent_names_a_real_launcher(fake_home):
|
|
89
|
+
menubar.set_login(True)
|
|
90
|
+
body = menubar._agent_plist().read_text(encoding="utf-8")
|
|
91
|
+
assert menubar.LAUNCH_AGENT_LABEL in body
|
|
92
|
+
assert "<key>RunAtLoad</key><true/>" in body
|
|
93
|
+
assert "menubar" in body
|
|
94
|
+
menubar.set_login(False)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
# --- Windows autostart -----------------------------------------------------
|
|
98
|
+
|
|
99
|
+
def test_windows_autostart_avoids_a_console_window_and_keeps_the_vault():
|
|
100
|
+
"""python.exe opens a console window behind an app that has no window, at
|
|
101
|
+
every sign-in, and an autostart that drops --vault silently sends a user
|
|
102
|
+
with a non-default vault back to the default one."""
|
|
103
|
+
from compartment import systray
|
|
104
|
+
cmd = systray._autostart_command("/tmp/some.vault")
|
|
105
|
+
assert "/tmp/some.vault" in cmd, "the vault path must survive a reboot"
|
|
106
|
+
assert "python.exe" not in cmd.lower() or "pythonw.exe" in cmd.lower()
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
# --- opting out of the GUI at install time ---------------------------------
|
|
110
|
+
|
|
111
|
+
def test_a_scripted_install_never_waits_for_a_keypress(monkeypatch, capsys):
|
|
112
|
+
"""The window must cost a non-interactive install nothing. A piped stdin
|
|
113
|
+
(installer script, CI, `yes | ...`) has no keyboard behind it, so waiting
|
|
114
|
+
five seconds for input that cannot arrive would be five seconds wasted on
|
|
115
|
+
every automated install."""
|
|
116
|
+
import time as _time
|
|
117
|
+
from compartment import cli
|
|
118
|
+
|
|
119
|
+
class NotATerminal:
|
|
120
|
+
def isatty(self):
|
|
121
|
+
return False
|
|
122
|
+
|
|
123
|
+
monkeypatch.setattr(cli.sys, "stdin", NotATerminal())
|
|
124
|
+
start = _time.monotonic()
|
|
125
|
+
assert cli._cli_only_requested(5.0) is False
|
|
126
|
+
assert _time.monotonic() - start < 0.5, "a scripted install must not stall"
|
|
127
|
+
assert "5 seconds" not in capsys.readouterr().out, "and must not be asked"
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def test_the_offer_survives_a_terminal_that_refuses_raw_mode(monkeypatch):
|
|
131
|
+
"""Some terminals refuse cbreak. The install continues normally rather
|
|
132
|
+
than dying at the very last step, after the vault already exists."""
|
|
133
|
+
from compartment import cli
|
|
134
|
+
|
|
135
|
+
class Terminal:
|
|
136
|
+
def isatty(self):
|
|
137
|
+
return True
|
|
138
|
+
|
|
139
|
+
def fileno(self):
|
|
140
|
+
raise OSError("no fileno here")
|
|
141
|
+
|
|
142
|
+
monkeypatch.setattr(cli.sys, "stdin", Terminal())
|
|
143
|
+
assert cli._cli_only_requested(0.2) is False
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def test_the_prompt_says_what_max_asked_it_to_say(monkeypatch, capsys):
|
|
147
|
+
from compartment import cli
|
|
148
|
+
|
|
149
|
+
class Terminal:
|
|
150
|
+
def isatty(self):
|
|
151
|
+
return True
|
|
152
|
+
|
|
153
|
+
def fileno(self):
|
|
154
|
+
raise OSError("stop here, we only want the text")
|
|
155
|
+
|
|
156
|
+
monkeypatch.setattr(cli.sys, "stdin", Terminal())
|
|
157
|
+
cli._cli_only_requested(0.1)
|
|
158
|
+
out = capsys.readouterr().out
|
|
159
|
+
assert 'This is a normal install. Press the letter "s" within 5 seconds' in out
|
|
160
|
+
assert "command-line only install" in out
|
|
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-3.0 → compartment-3.2}/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
|