sibyl-memory-cli 0.4.0__tar.gz → 0.4.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.
- {sibyl_memory_cli-0.4.0/src/sibyl_memory_cli.egg-info → sibyl_memory_cli-0.4.1}/PKG-INFO +6 -2
- {sibyl_memory_cli-0.4.0 → sibyl_memory_cli-0.4.1}/README.md +2 -0
- {sibyl_memory_cli-0.4.0 → sibyl_memory_cli-0.4.1}/pyproject.toml +4 -2
- {sibyl_memory_cli-0.4.0 → sibyl_memory_cli-0.4.1}/src/sibyl_memory_cli/cli.py +225 -13
- {sibyl_memory_cli-0.4.0 → sibyl_memory_cli-0.4.1}/src/sibyl_memory_cli/setup.py +7 -0
- {sibyl_memory_cli-0.4.0 → sibyl_memory_cli-0.4.1/src/sibyl_memory_cli.egg-info}/PKG-INFO +6 -2
- {sibyl_memory_cli-0.4.0 → sibyl_memory_cli-0.4.1}/src/sibyl_memory_cli.egg-info/SOURCES.txt +3 -0
- {sibyl_memory_cli-0.4.0 → sibyl_memory_cli-0.4.1}/src/sibyl_memory_cli.egg-info/requires.txt +1 -1
- sibyl_memory_cli-0.4.1/tests/test_claim_and_fullid_2026_09_04.py +99 -0
- sibyl_memory_cli-0.4.1/tests/test_platform_gate_2026_09_04.py +188 -0
- sibyl_memory_cli-0.4.1/tests/test_trust_context_2026_08_25.py +78 -0
- {sibyl_memory_cli-0.4.0 → sibyl_memory_cli-0.4.1}/LICENSE +0 -0
- {sibyl_memory_cli-0.4.0 → sibyl_memory_cli-0.4.1}/setup.cfg +0 -0
- {sibyl_memory_cli-0.4.0 → sibyl_memory_cli-0.4.1}/src/sibyl_memory_cli/__init__.py +0 -0
- {sibyl_memory_cli-0.4.0 → sibyl_memory_cli-0.4.1}/src/sibyl_memory_cli/_aesthetic.py +0 -0
- {sibyl_memory_cli-0.4.0 → sibyl_memory_cli-0.4.1}/src/sibyl_memory_cli/_banner.py +0 -0
- {sibyl_memory_cli-0.4.0 → sibyl_memory_cli-0.4.1}/src/sibyl_memory_cli/migrate.py +0 -0
- {sibyl_memory_cli-0.4.0 → sibyl_memory_cli-0.4.1}/src/sibyl_memory_cli.egg-info/dependency_links.txt +0 -0
- {sibyl_memory_cli-0.4.0 → sibyl_memory_cli-0.4.1}/src/sibyl_memory_cli.egg-info/entry_points.txt +0 -0
- {sibyl_memory_cli-0.4.0 → sibyl_memory_cli-0.4.1}/src/sibyl_memory_cli.egg-info/top_level.txt +0 -0
- {sibyl_memory_cli-0.4.0 → sibyl_memory_cli-0.4.1}/tests/test_hermes_install_args.py +0 -0
- {sibyl_memory_cli-0.4.0 → sibyl_memory_cli-0.4.1}/tests/test_init_perms_guard.py +0 -0
- {sibyl_memory_cli-0.4.0 → sibyl_memory_cli-0.4.1}/tests/test_memory_cmd_2026_06_16.py +0 -0
- {sibyl_memory_cli-0.4.0 → sibyl_memory_cli-0.4.1}/tests/test_memory_tenant_2026_08_12.py +0 -0
- {sibyl_memory_cli-0.4.0 → sibyl_memory_cli-0.4.1}/tests/test_migrate.py +0 -0
- {sibyl_memory_cli-0.4.0 → sibyl_memory_cli-0.4.1}/tests/test_migrate_adversarial.py +0 -0
- {sibyl_memory_cli-0.4.0 → sibyl_memory_cli-0.4.1}/tests/test_migrate_force_2026_06_05.py +0 -0
- {sibyl_memory_cli-0.4.0 → sibyl_memory_cli-0.4.1}/tests/test_orchestrator.py +0 -0
- {sibyl_memory_cli-0.4.0 → sibyl_memory_cli-0.4.1}/tests/test_prelaunch_fixes_2026_06_25.py +0 -0
- {sibyl_memory_cli-0.4.0 → sibyl_memory_cli-0.4.1}/tests/test_quality_fixes_2026_06_30.py +0 -0
- {sibyl_memory_cli-0.4.0 → sibyl_memory_cli-0.4.1}/tests/test_setup.py +0 -0
- {sibyl_memory_cli-0.4.0 → sibyl_memory_cli-0.4.1}/tests/test_status_stores_2026_06_11.py +0 -0
- {sibyl_memory_cli-0.4.0 → sibyl_memory_cli-0.4.1}/tests/test_superpatch_2026_07_05.py +0 -0
- {sibyl_memory_cli-0.4.0 → sibyl_memory_cli-0.4.1}/tests/test_verdict_contract_2026_08_31.py +0 -0
- {sibyl_memory_cli-0.4.0 → sibyl_memory_cli-0.4.1}/tests/test_wiring_fix.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sibyl-memory-cli
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.1
|
|
4
4
|
Summary: Command-line interface for the Sibyl Memory Plugin. `sibyl init` activates, `sibyl upgrade` runs the staker / subscription flow, `sibyl status` shows current tier and DB stats, `sibyl whoami` gives a one-line account summary, `sibyl devices` lists active devices and supports per-device revoke.
|
|
5
5
|
Author-email: "SIBYL, Sibyl Labs LLC" <sibyl@sibyllabs.org>
|
|
6
6
|
License: MIT
|
|
@@ -17,10 +17,12 @@ Classifier: Programming Language :: Python :: 3.11
|
|
|
17
17
|
Classifier: Programming Language :: Python :: 3.12
|
|
18
18
|
Classifier: Environment :: Console
|
|
19
19
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
20
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
21
|
+
Classifier: Operating System :: MacOS
|
|
20
22
|
Requires-Python: >=3.10
|
|
21
23
|
Description-Content-Type: text/markdown
|
|
22
24
|
License-File: LICENSE
|
|
23
|
-
Requires-Dist: sibyl-memory-client>=0.8.
|
|
25
|
+
Requires-Dist: sibyl-memory-client>=0.8.1
|
|
24
26
|
Requires-Dist: sibyl-memory-hermes>=0.4.0
|
|
25
27
|
Requires-Dist: pyyaml<7,>=6.0
|
|
26
28
|
Provides-Extra: mcp
|
|
@@ -37,6 +39,8 @@ Command-line interface for the **Sibyl Memory Plugin**.
|
|
|
37
39
|
pip install sibyl-memory-cli
|
|
38
40
|
```
|
|
39
41
|
|
|
42
|
+
Sibyl Memory runs on Linux, macOS (Apple Silicon and Intel), and Windows through WSL2. Native Windows is not supported.
|
|
43
|
+
|
|
40
44
|
This pulls in `sibyl-memory-client` (the local SDK) and `sibyl-memory-hermes` (the Hermes provider) automatically.
|
|
41
45
|
|
|
42
46
|
## Commands
|
|
@@ -6,6 +6,8 @@ Command-line interface for the **Sibyl Memory Plugin**.
|
|
|
6
6
|
pip install sibyl-memory-cli
|
|
7
7
|
```
|
|
8
8
|
|
|
9
|
+
Sibyl Memory runs on Linux, macOS (Apple Silicon and Intel), and Windows through WSL2. Native Windows is not supported.
|
|
10
|
+
|
|
9
11
|
This pulls in `sibyl-memory-client` (the local SDK) and `sibyl-memory-hermes` (the Hermes provider) automatically.
|
|
10
12
|
|
|
11
13
|
## Commands
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "sibyl-memory-cli"
|
|
7
|
-
version = "0.4.
|
|
7
|
+
version = "0.4.1"
|
|
8
8
|
description = "Command-line interface for the Sibyl Memory Plugin. `sibyl init` activates, `sibyl upgrade` runs the staker / subscription flow, `sibyl status` shows current tier and DB stats, `sibyl whoami` gives a one-line account summary, `sibyl devices` lists active devices and supports per-device revoke."
|
|
9
9
|
authors = [{ name = "SIBYL, Sibyl Labs LLC", email = "sibyl@sibyllabs.org" }]
|
|
10
10
|
license = { text = "MIT" }
|
|
@@ -21,9 +21,11 @@ classifiers = [
|
|
|
21
21
|
"Programming Language :: Python :: 3.12",
|
|
22
22
|
"Environment :: Console",
|
|
23
23
|
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
24
|
+
"Operating System :: POSIX :: Linux",
|
|
25
|
+
"Operating System :: MacOS",
|
|
24
26
|
]
|
|
25
27
|
dependencies = [
|
|
26
|
-
"sibyl-memory-client>=0.8.
|
|
28
|
+
"sibyl-memory-client>=0.8.1",
|
|
27
29
|
"sibyl-memory-hermes>=0.4.0",
|
|
28
30
|
"pyyaml>=6.0,<7",
|
|
29
31
|
]
|
|
@@ -4,7 +4,9 @@ Stdlib only. The CLI is a thin wrapper around HTTP calls to
|
|
|
4
4
|
https://api.sibyllabs.org/api/plugin/* and the local SibylMemoryProvider.
|
|
5
5
|
|
|
6
6
|
Design pillars:
|
|
7
|
-
- Zero non-stdlib deps in this file. urllib is enough
|
|
7
|
+
- Zero HARD non-stdlib deps in this file. urllib is enough; sibyl_memory_client
|
|
8
|
+
is imported lazily where it helps (tenant resolution, TLS trust) and every
|
|
9
|
+
such import degrades gracefully when the package is absent.
|
|
8
10
|
- Credentials are written atomically at mode 0600, set at file-creation
|
|
9
11
|
time via O_CREAT|O_EXCL|O_NOFOLLOW (no chmod-after-write race).
|
|
10
12
|
- The URL parameter handed to the browser is an opaque session identifier,
|
|
@@ -20,7 +22,9 @@ import argparse
|
|
|
20
22
|
import hashlib
|
|
21
23
|
import json
|
|
22
24
|
import os
|
|
25
|
+
import platform as _platform
|
|
23
26
|
import secrets
|
|
27
|
+
import ssl
|
|
24
28
|
import sys
|
|
25
29
|
import time
|
|
26
30
|
import urllib.error
|
|
@@ -98,6 +102,94 @@ def _detect_os_family() -> str | None:
|
|
|
98
102
|
return None
|
|
99
103
|
|
|
100
104
|
|
|
105
|
+
# ---- platform support policy (2026-09-04) ------------------------------
|
|
106
|
+
#
|
|
107
|
+
# Sibyl Memory runs on Linux, macOS (Apple Silicon and Intel), and Windows
|
|
108
|
+
# through WSL2. Native Windows is not supported. WSL2 is Linux to Python and
|
|
109
|
+
# to us, so it needs no special case beyond the label.
|
|
110
|
+
#
|
|
111
|
+
# "block" refuses `sibyl init` and `sibyl setup` on native Windows. Every
|
|
112
|
+
# other command keeps working on machines that already activated there, so an
|
|
113
|
+
# existing install never loses `status`, `upgrade`, `claim`, `devices`,
|
|
114
|
+
# `logout`, `health`, `memory` or `update`.
|
|
115
|
+
NATIVE_WINDOWS_POLICY = "block"
|
|
116
|
+
NATIVE_WINDOWS_ESCAPE_ENV = "SIBYL_ALLOW_NATIVE_WINDOWS"
|
|
117
|
+
WSL_DOCS_URL = "https://docs.sibyllabs.org/memory/install#windows"
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _platform_info() -> tuple[str, str, bool]:
|
|
121
|
+
"""(os_family, human label, supported). WSL2 is Linux to Python and to us.
|
|
122
|
+
|
|
123
|
+
The os_family element is deliberately the same vocabulary
|
|
124
|
+
`_detect_os_family()` returns (`linux|macos|windows`), because telemetry
|
|
125
|
+
consumers key on it. `unknown` is the one value that function reports as
|
|
126
|
+
None instead.
|
|
127
|
+
"""
|
|
128
|
+
p = sys.platform
|
|
129
|
+
arch = _platform.machine() or "unknown"
|
|
130
|
+
if p == "darwin":
|
|
131
|
+
ver = _platform.mac_ver()[0] or ""
|
|
132
|
+
return "macos", f"macOS {ver} ({arch})".replace(" ", " "), True
|
|
133
|
+
if p.startswith("linux"):
|
|
134
|
+
rel = _platform.release().lower()
|
|
135
|
+
wsl = "microsoft" in rel or bool(os.environ.get("WSL_DISTRO_NAME"))
|
|
136
|
+
return "linux", ("Linux, WSL2" if wsl else "Linux") + f" ({arch})", True
|
|
137
|
+
if p.startswith("win"):
|
|
138
|
+
return "windows", f"Windows {_platform.release()} native ({arch})", False
|
|
139
|
+
return "unknown", p, False
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _is_wsl() -> bool:
|
|
143
|
+
if not sys.platform.startswith("linux"):
|
|
144
|
+
return False
|
|
145
|
+
return "microsoft" in _platform.release().lower() or bool(os.environ.get("WSL_DISTRO_NAME"))
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _native_windows_override() -> bool:
|
|
149
|
+
"""True when the user has explicitly opted into the unsupported path."""
|
|
150
|
+
v = (os.environ.get(NATIVE_WINDOWS_ESCAPE_ENV) or "").strip().lower()
|
|
151
|
+
return v not in ("", "0", "false", "no", "off")
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def print_platform_line(*, gate: bool = False) -> int:
|
|
155
|
+
"""Print the platform line. Returns 0 to continue, 1 to refuse.
|
|
156
|
+
|
|
157
|
+
`gate=True` is passed by `sibyl init` and `sibyl setup` only. Those are the
|
|
158
|
+
two commands that stand up a NEW install, and they are the only two the
|
|
159
|
+
native-Windows block applies to.
|
|
160
|
+
"""
|
|
161
|
+
family, label, supported = _platform_info()
|
|
162
|
+
if supported:
|
|
163
|
+
print(dim(f" platform {label} \u00b7 supported"))
|
|
164
|
+
return 0
|
|
165
|
+
|
|
166
|
+
blocked = (
|
|
167
|
+
gate
|
|
168
|
+
and family == "windows"
|
|
169
|
+
and NATIVE_WINDOWS_POLICY == "block"
|
|
170
|
+
and not _native_windows_override()
|
|
171
|
+
)
|
|
172
|
+
paint = red if blocked else yellow
|
|
173
|
+
print(paint(f" platform {label} \u00b7 not supported"))
|
|
174
|
+
|
|
175
|
+
if family != "windows":
|
|
176
|
+
print(yellow(" Sibyl Memory is built and tested on Linux and macOS. This platform is"))
|
|
177
|
+
print(yellow(" neither, so nothing here is tested or fixed. Continuing anyway."))
|
|
178
|
+
return 0
|
|
179
|
+
|
|
180
|
+
if blocked:
|
|
181
|
+
print(red(" Sibyl Memory is not supported on native Windows. Install it inside WSL2 and"))
|
|
182
|
+
print(red(f" run this command again there: {WSL_DOCS_URL}"))
|
|
183
|
+
print(red(" Already activated here? status, upgrade, claim, devices and logout keep working."))
|
|
184
|
+
print(red(f" To continue unsupported anyway, set {NATIVE_WINDOWS_ESCAPE_ENV}=1."))
|
|
185
|
+
return 1
|
|
186
|
+
|
|
187
|
+
print(yellow(" Sibyl Memory is not supported on native Windows. Run it inside WSL2:"))
|
|
188
|
+
print(yellow(f" {WSL_DOCS_URL}"))
|
|
189
|
+
print(yellow(" Continuing anyway. Nothing on this platform is tested or fixed."))
|
|
190
|
+
return 0
|
|
191
|
+
|
|
192
|
+
|
|
101
193
|
def short(token: str | None) -> str:
|
|
102
194
|
if not token:
|
|
103
195
|
return "—"
|
|
@@ -135,6 +227,24 @@ class HttpError(Exception):
|
|
|
135
227
|
self.url = url
|
|
136
228
|
|
|
137
229
|
|
|
230
|
+
def _https_context() -> ssl.SSLContext:
|
|
231
|
+
"""TLS context for every CLI network call (api.sibyllabs.org, pypi.org).
|
|
232
|
+
|
|
233
|
+
Prefers the shared certifi-backed trust context from sibyl-memory-client,
|
|
234
|
+
the fix for macOS framework-build Pythons that ship an empty OpenSSL trust
|
|
235
|
+
store (the `session-init failed (0)` / `Bind failed` / `server error: 0`
|
|
236
|
+
class of report). Degrades to the stdlib default context when the client
|
|
237
|
+
package is absent, mirroring the `_ver_lt` packaging fallback: the CLI
|
|
238
|
+
stays runnable stdlib-only, just without the Darwin trust fix. Verification
|
|
239
|
+
is fully on in both branches; nothing here ever weakens it.
|
|
240
|
+
"""
|
|
241
|
+
try:
|
|
242
|
+
from sibyl_memory_client._trust import https_context
|
|
243
|
+
except ImportError:
|
|
244
|
+
return ssl.create_default_context()
|
|
245
|
+
return https_context()
|
|
246
|
+
|
|
247
|
+
|
|
138
248
|
def http_request( # noqa: D401
|
|
139
249
|
method: str,
|
|
140
250
|
path: str,
|
|
@@ -154,7 +264,7 @@ def http_request( # noqa: D401
|
|
|
154
264
|
full_headers.update(headers)
|
|
155
265
|
req = urllib.request.Request(url, data=data, method=method, headers=full_headers)
|
|
156
266
|
try:
|
|
157
|
-
with urllib.request.urlopen(req, timeout=timeout) as resp:
|
|
267
|
+
with urllib.request.urlopen(req, timeout=timeout, context=_https_context()) as resp:
|
|
158
268
|
return json.loads(resp.read().decode("utf-8"))
|
|
159
269
|
except urllib.error.HTTPError as e:
|
|
160
270
|
try:
|
|
@@ -292,12 +402,16 @@ def cmd_init(args: argparse.Namespace) -> int:
|
|
|
292
402
|
from ._banner import print_banner
|
|
293
403
|
print_banner()
|
|
294
404
|
|
|
405
|
+
# Platform line first, so it shows on --force re-activation too.
|
|
406
|
+
if print_platform_line(gate=True) != 0:
|
|
407
|
+
return 1
|
|
408
|
+
|
|
295
409
|
cred_path = Path(args.credentials).expanduser()
|
|
296
410
|
if cred_path.exists() and not args.force:
|
|
297
411
|
existing = read_credentials(cred_path) or {}
|
|
298
412
|
print(a.section_header("already activated", subtitle="use --force to re-activate"))
|
|
299
413
|
print()
|
|
300
|
-
print(a.kv("Account",
|
|
414
|
+
print(a.kv("Account", existing.get("account_id") or "—"))
|
|
301
415
|
print(a.kv("Tier", (existing.get("tier") or "free").upper(), value_color="accent"))
|
|
302
416
|
print(a.kv("Credentials", str(cred_path)))
|
|
303
417
|
print()
|
|
@@ -345,6 +459,9 @@ def cmd_init(args: argparse.Namespace) -> int:
|
|
|
345
459
|
"pairing_code_hash": code_hash,
|
|
346
460
|
"env": {
|
|
347
461
|
"os_family": _detect_os_family(),
|
|
462
|
+
"os_version": _platform.release(),
|
|
463
|
+
"arch": _platform.machine(),
|
|
464
|
+
"wsl": _is_wsl(),
|
|
348
465
|
"install_method": "cli",
|
|
349
466
|
"client_version": _client_version(),
|
|
350
467
|
},
|
|
@@ -439,7 +556,7 @@ def cmd_init(args: argparse.Namespace) -> int:
|
|
|
439
556
|
print()
|
|
440
557
|
print(a.success_line("Activated."))
|
|
441
558
|
print()
|
|
442
|
-
print(a.kv("Account",
|
|
559
|
+
print(a.kv("Account", creds.get("account_id") or "—"))
|
|
443
560
|
print(a.kv("Tier", (creds.get("tier") or "free").upper(), value_color="accent"))
|
|
444
561
|
print(a.kv("Wallet", creds.get("wallet") or "—"))
|
|
445
562
|
print(a.kv("Email", creds.get("email") or "—"))
|
|
@@ -508,7 +625,7 @@ def cmd_upgrade(args: argparse.Namespace) -> int:
|
|
|
508
625
|
print()
|
|
509
626
|
print(a.section_header("upgrade", subtitle="lift the 5 MB free-tier cap"))
|
|
510
627
|
print()
|
|
511
|
-
print(a.kv("Account",
|
|
628
|
+
print(a.kv("Account", account_id or "—"))
|
|
512
629
|
print(a.kv("Current tier", current_tier.upper(), value_color="accent"))
|
|
513
630
|
# F3 (red-team 2026-06-17): never print the bearer to stdout (terminal
|
|
514
631
|
# scrollback / tmux / CI logs / screen-shares) — restores the invariant
|
|
@@ -672,11 +789,14 @@ def cmd_status(args: argparse.Namespace) -> int:
|
|
|
672
789
|
# Local view
|
|
673
790
|
print(a.eyebrow("local"))
|
|
674
791
|
print(a.kv("Credentials", str(cred_path)))
|
|
675
|
-
print(a.kv("Account",
|
|
792
|
+
print(a.kv("Account", creds.get("account_id") or "—"))
|
|
676
793
|
print(a.kv("Tier", (creds.get("tier") or "free").upper(), value_color="accent"))
|
|
677
794
|
print(a.kv("Wallet", creds.get("wallet") or "—"))
|
|
678
795
|
print(a.kv("Email", creds.get("email") or "—"))
|
|
679
796
|
print(a.kv("Issued", creds.get("issued_at") or "—"))
|
|
797
|
+
_family, plat_label, plat_ok = _platform_info()
|
|
798
|
+
print(a.kv("Platform", plat_label if plat_ok else f"{plat_label} \u00b7 unsupported",
|
|
799
|
+
value_color="ink" if plat_ok else "warn"))
|
|
680
800
|
|
|
681
801
|
db_path = Path(args.db).expanduser()
|
|
682
802
|
if db_path.exists():
|
|
@@ -695,11 +815,20 @@ def cmd_status(args: argparse.Namespace) -> int:
|
|
|
695
815
|
# write burst (committed pages still in memory.db-wal), so the
|
|
696
816
|
# displayed size/percentage would otherwise disagree with the gate.
|
|
697
817
|
from sibyl_memory_client.storage import db_size_bytes
|
|
818
|
+
from sibyl_memory_client._capcheck import FREE_TIER_CAP_BYTES, PAID_TIERS
|
|
698
819
|
|
|
699
820
|
size = db_size_bytes(db_path)
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
821
|
+
# Panel fix 2026-09-04: the divisor was a hardcoded pre-2026-08-06
|
|
822
|
+
# 2 MiB; the free cap is FREE_TIER_CAP_BYTES (5 MiB). And a paid
|
|
823
|
+
# tier has no cap, so no percentage is shown for it.
|
|
824
|
+
tier_now = (creds.get("tier") or "free") if creds else "free"
|
|
825
|
+
if tier_now in PAID_TIERS:
|
|
826
|
+
size_label = f"{size:,} bytes ({size / (1024 * 1024):.2f} MB · uncapped)"
|
|
827
|
+
size_color = "soft"
|
|
828
|
+
else:
|
|
829
|
+
pct = size / FREE_TIER_CAP_BYTES * 100
|
|
830
|
+
size_label = f"{size:,} bytes ({size / (1024 * 1024):.2f} MB · {pct:.1f}% of free cap)"
|
|
831
|
+
size_color = "warn" if pct > 80 else "soft"
|
|
703
832
|
print(a.kv("DB path", str(db_path)))
|
|
704
833
|
print(a.kv("DB size", size_label, value_color=size_color))
|
|
705
834
|
else:
|
|
@@ -852,16 +981,95 @@ def cmd_whoami(args: argparse.Namespace) -> int:
|
|
|
852
981
|
wallet = creds.get("wallet") if full else _mask_wallet(creds.get("wallet"))
|
|
853
982
|
|
|
854
983
|
print()
|
|
855
|
-
print(f" {a.color('account', a.INK_FAINT)} {a.bold(
|
|
984
|
+
print(f" {a.color('account', a.INK_FAINT)} {a.bold(acct or "—")} {a.dim(a.GLYPH_DOT)} {a.gradient_gold(tier)}")
|
|
856
985
|
print(f" {a.color('wallet ', a.INK_FAINT)} {a.color(wallet or '—', a.INK)}")
|
|
857
986
|
print(f" {a.color('email ', a.INK_FAINT)} {a.color(email or '—', a.INK)}")
|
|
858
|
-
|
|
859
|
-
device_line = f"sibyl-memory-cli/{_client_version()} {
|
|
987
|
+
_family, plat_label, plat_ok = _platform_info()
|
|
988
|
+
device_line = f"sibyl-memory-cli/{_client_version()} {plat_label}"
|
|
989
|
+
if not plat_ok:
|
|
990
|
+
device_line += " \u00b7 unsupported"
|
|
860
991
|
print(f" {a.color('device ', a.INK_FAINT)} {a.dim(device_line)}")
|
|
861
992
|
print()
|
|
862
993
|
return 0
|
|
863
994
|
|
|
864
995
|
|
|
996
|
+
# ---- `sibyl claim` -----------------------------------------------------
|
|
997
|
+
|
|
998
|
+
def _looks_like_checkout_code(cs: str) -> bool:
|
|
999
|
+
"""Mirror of the server's CS_ID_RE without importing re: cs_ prefix,
|
|
1000
|
+
optional test_/live_ marker, then 10..240 alphanumerics."""
|
|
1001
|
+
if not cs.startswith("cs_"):
|
|
1002
|
+
return False
|
|
1003
|
+
rest = cs[3:]
|
|
1004
|
+
for marker in ("test_", "live_"):
|
|
1005
|
+
if rest.startswith(marker):
|
|
1006
|
+
rest = rest[len(marker):]
|
|
1007
|
+
break
|
|
1008
|
+
return 10 <= len(rest) <= 240 and rest.isalnum()
|
|
1009
|
+
|
|
1010
|
+
|
|
1011
|
+
def cmd_claim(args: argparse.Namespace) -> int:
|
|
1012
|
+
"""Link a card purchase (Stripe checkout) to this account.
|
|
1013
|
+
|
|
1014
|
+
Buy-before-install: the page after checkout shows the buyer their
|
|
1015
|
+
checkout code (cs_...). This redeems it onto the account activated on
|
|
1016
|
+
this machine — email or wallet activation both work. The bearer rides
|
|
1017
|
+
along as session_token so the server verifies the caller controls the
|
|
1018
|
+
destination account, not just the receipt.
|
|
1019
|
+
"""
|
|
1020
|
+
cs = (getattr(args, "checkout_code", "") or "").strip()
|
|
1021
|
+
if not _looks_like_checkout_code(cs):
|
|
1022
|
+
print(a.warn_line("That does not look like a checkout code."))
|
|
1023
|
+
print(a.dim(" It starts with cs_ and appears on the page you landed on after paying"))
|
|
1024
|
+
print(a.dim(" (sibyllabs.org/pro/welcome). Copy the whole code."))
|
|
1025
|
+
return 1
|
|
1026
|
+
|
|
1027
|
+
creds = read_credentials(Path(args.credentials).expanduser())
|
|
1028
|
+
if not creds or not creds.get("account_id"):
|
|
1029
|
+
print(a.warn_line("Not activated."))
|
|
1030
|
+
print(a.dim(" Run `sibyl init` first, then `sibyl claim` again."))
|
|
1031
|
+
return 1
|
|
1032
|
+
account_id = creds["account_id"]
|
|
1033
|
+
|
|
1034
|
+
body: dict = {"cs": cs, "account_id": account_id}
|
|
1035
|
+
token = creds.get("bearer_token") or creds.get("session_token")
|
|
1036
|
+
if token:
|
|
1037
|
+
body["session_token"] = token
|
|
1038
|
+
|
|
1039
|
+
try:
|
|
1040
|
+
resp = http_request("POST", "/api/plugin/stripe-claim", body=body)
|
|
1041
|
+
except HttpError as e:
|
|
1042
|
+
msg = e.body.get("error") if isinstance(e.body, dict) else None
|
|
1043
|
+
print(a.warn_line(f"Claim failed ({e.status or 'network'})."))
|
|
1044
|
+
if msg:
|
|
1045
|
+
print(a.dim(f" {msg}"))
|
|
1046
|
+
if e.status == 403:
|
|
1047
|
+
print(a.dim(" This session does not match the account. Run `sibyl init`, then retry."))
|
|
1048
|
+
elif e.status == 0:
|
|
1049
|
+
print(a.dim(" Could not reach the server. Check your connection and retry."))
|
|
1050
|
+
return 1
|
|
1051
|
+
|
|
1052
|
+
tier = (resp.get("tier") or "pro").upper()
|
|
1053
|
+
print()
|
|
1054
|
+
print(a.success_line(f"Purchase linked. Tier: {tier}"))
|
|
1055
|
+
if resp.get("expires_at"):
|
|
1056
|
+
print(a.kv("Covers through", str(resp["expires_at"])))
|
|
1057
|
+
print(a.dim(" Run `sibyl status` to see it. Your agent picks it up next session."))
|
|
1058
|
+
print()
|
|
1059
|
+
|
|
1060
|
+
# Sync local state so the very next write sees the new tier.
|
|
1061
|
+
try:
|
|
1062
|
+
creds["tier"] = resp.get("tier") or "pro"
|
|
1063
|
+
write_credentials_atomic(creds, Path(args.credentials).expanduser())
|
|
1064
|
+
except Exception:
|
|
1065
|
+
pass # server is the source of truth; local hint refreshes on next access check
|
|
1066
|
+
try:
|
|
1067
|
+
invalidate_tier_cache()
|
|
1068
|
+
except Exception:
|
|
1069
|
+
pass
|
|
1070
|
+
return 0
|
|
1071
|
+
|
|
1072
|
+
|
|
865
1073
|
# ---- `sibyl devices` ---------------------------------------------------
|
|
866
1074
|
|
|
867
1075
|
def cmd_devices(args: argparse.Namespace) -> int:
|
|
@@ -1151,7 +1359,7 @@ def _pypi_latest(pkg: str, timeout: float = 4.0) -> str | None:
|
|
|
1151
1359
|
url = f"https://pypi.org/pypi/{pkg}/json"
|
|
1152
1360
|
try:
|
|
1153
1361
|
req = urllib.request.Request(url, headers={"User-Agent": f"sibyl-memory-cli/{_client_version()}"})
|
|
1154
|
-
with urllib.request.urlopen(req, timeout=timeout) as r:
|
|
1362
|
+
with urllib.request.urlopen(req, timeout=timeout, context=_https_context()) as r:
|
|
1155
1363
|
data = json.loads(r.read().decode("utf-8"))
|
|
1156
1364
|
return (data.get("info") or {}).get("version")
|
|
1157
1365
|
except Exception:
|
|
@@ -1429,6 +1637,10 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
1429
1637
|
p_up = sub.add_parser("upgrade", help="Open the upgrade flow (stake or subscribe)")
|
|
1430
1638
|
p_up.set_defaults(func=cmd_upgrade)
|
|
1431
1639
|
|
|
1640
|
+
p_claim = sub.add_parser("claim", help="Link a card purchase to this account (code from sibyllabs.org/pro/welcome)")
|
|
1641
|
+
p_claim.add_argument("checkout_code", help="The cs_... checkout code shown after paying")
|
|
1642
|
+
p_claim.set_defaults(func=cmd_claim)
|
|
1643
|
+
|
|
1432
1644
|
p_st = sub.add_parser("status", help="Show local + server tier / DB stats")
|
|
1433
1645
|
p_st.set_defaults(func=cmd_status)
|
|
1434
1646
|
|
|
@@ -817,6 +817,13 @@ def cmd_setup(args: argparse.Namespace) -> int:
|
|
|
817
817
|
print(bold("Sibyl Memory Plugin setup"))
|
|
818
818
|
print()
|
|
819
819
|
|
|
820
|
+
# Platform gate. `setup` wires a new machine, so it is one of the two
|
|
821
|
+
# commands the native-Windows block applies to (see cli.NATIVE_WINDOWS_POLICY).
|
|
822
|
+
from .cli import print_platform_line
|
|
823
|
+
if print_platform_line(gate=True) != 0:
|
|
824
|
+
return 1
|
|
825
|
+
print()
|
|
826
|
+
|
|
820
827
|
# Detection
|
|
821
828
|
if skip_present_check:
|
|
822
829
|
detected = wirers
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sibyl-memory-cli
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.1
|
|
4
4
|
Summary: Command-line interface for the Sibyl Memory Plugin. `sibyl init` activates, `sibyl upgrade` runs the staker / subscription flow, `sibyl status` shows current tier and DB stats, `sibyl whoami` gives a one-line account summary, `sibyl devices` lists active devices and supports per-device revoke.
|
|
5
5
|
Author-email: "SIBYL, Sibyl Labs LLC" <sibyl@sibyllabs.org>
|
|
6
6
|
License: MIT
|
|
@@ -17,10 +17,12 @@ Classifier: Programming Language :: Python :: 3.11
|
|
|
17
17
|
Classifier: Programming Language :: Python :: 3.12
|
|
18
18
|
Classifier: Environment :: Console
|
|
19
19
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
20
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
21
|
+
Classifier: Operating System :: MacOS
|
|
20
22
|
Requires-Python: >=3.10
|
|
21
23
|
Description-Content-Type: text/markdown
|
|
22
24
|
License-File: LICENSE
|
|
23
|
-
Requires-Dist: sibyl-memory-client>=0.8.
|
|
25
|
+
Requires-Dist: sibyl-memory-client>=0.8.1
|
|
24
26
|
Requires-Dist: sibyl-memory-hermes>=0.4.0
|
|
25
27
|
Requires-Dist: pyyaml<7,>=6.0
|
|
26
28
|
Provides-Extra: mcp
|
|
@@ -37,6 +39,8 @@ Command-line interface for the **Sibyl Memory Plugin**.
|
|
|
37
39
|
pip install sibyl-memory-cli
|
|
38
40
|
```
|
|
39
41
|
|
|
42
|
+
Sibyl Memory runs on Linux, macOS (Apple Silicon and Intel), and Windows through WSL2. Native Windows is not supported.
|
|
43
|
+
|
|
40
44
|
This pulls in `sibyl-memory-client` (the local SDK) and `sibyl-memory-hermes` (the Hermes provider) automatically.
|
|
41
45
|
|
|
42
46
|
## Commands
|
|
@@ -13,6 +13,7 @@ src/sibyl_memory_cli.egg-info/dependency_links.txt
|
|
|
13
13
|
src/sibyl_memory_cli.egg-info/entry_points.txt
|
|
14
14
|
src/sibyl_memory_cli.egg-info/requires.txt
|
|
15
15
|
src/sibyl_memory_cli.egg-info/top_level.txt
|
|
16
|
+
tests/test_claim_and_fullid_2026_09_04.py
|
|
16
17
|
tests/test_hermes_install_args.py
|
|
17
18
|
tests/test_init_perms_guard.py
|
|
18
19
|
tests/test_memory_cmd_2026_06_16.py
|
|
@@ -21,10 +22,12 @@ tests/test_migrate.py
|
|
|
21
22
|
tests/test_migrate_adversarial.py
|
|
22
23
|
tests/test_migrate_force_2026_06_05.py
|
|
23
24
|
tests/test_orchestrator.py
|
|
25
|
+
tests/test_platform_gate_2026_09_04.py
|
|
24
26
|
tests/test_prelaunch_fixes_2026_06_25.py
|
|
25
27
|
tests/test_quality_fixes_2026_06_30.py
|
|
26
28
|
tests/test_setup.py
|
|
27
29
|
tests/test_status_stores_2026_06_11.py
|
|
28
30
|
tests/test_superpatch_2026_07_05.py
|
|
31
|
+
tests/test_trust_context_2026_08_25.py
|
|
29
32
|
tests/test_verdict_contract_2026_08_31.py
|
|
30
33
|
tests/test_wiring_fix.py
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"""C2 fixes (2026-09-04): full account id on every display surface + `sibyl claim`.
|
|
2
|
+
|
|
3
|
+
Panel finding (three seats independently): the buy-before-install claim flow
|
|
4
|
+
requires the full 36-char account id, but every CLI surface printed it through
|
|
5
|
+
short() as `12345678…abcd`, making the documented flow unusable. account_id is
|
|
6
|
+
a routing identifier (rides URL querystrings and subscribe calls), not a
|
|
7
|
+
bearer secret; the F3 never-print-the-bearer invariant covers tokens.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
import pytest
|
|
14
|
+
|
|
15
|
+
from sibyl_memory_cli import cli
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
ACCT = "11111111-2222-4333-8444-555555555555"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@pytest.fixture()
|
|
22
|
+
def creds_file(tmp_path):
|
|
23
|
+
p = tmp_path / "credentials.json"
|
|
24
|
+
p.write_text(json.dumps({
|
|
25
|
+
"account_id": ACCT,
|
|
26
|
+
"tenant_id": ACCT,
|
|
27
|
+
"tier": "free",
|
|
28
|
+
"email": "buyer@example.com",
|
|
29
|
+
"wallet": None,
|
|
30
|
+
"issued_at": "2026-09-04T00:00:00Z",
|
|
31
|
+
"schema_version": 3,
|
|
32
|
+
"session_token": "22222222-3333-4444-8555-666666666666",
|
|
33
|
+
"bearer_token": "22222222-3333-4444-8555-666666666666",
|
|
34
|
+
}))
|
|
35
|
+
return p
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class _Args:
|
|
39
|
+
def __init__(self, **kw):
|
|
40
|
+
self.__dict__.update(kw)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def test_whoami_prints_full_account_id(creds_file, capsys):
|
|
44
|
+
rc = cli.cmd_whoami(_Args(credentials=str(creds_file), full=False))
|
|
45
|
+
out = capsys.readouterr().out
|
|
46
|
+
assert rc == 0
|
|
47
|
+
assert ACCT in out # full 36-char id, verbatim
|
|
48
|
+
assert "…" not in out.split("account")[1].split("\n")[0] # no ellipsis on the account line
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def test_claim_code_validation():
|
|
52
|
+
assert cli._looks_like_checkout_code("cs_test_a1B2c3D4e5F6g7H8")
|
|
53
|
+
assert cli._looks_like_checkout_code("cs_live_" + "a" * 24)
|
|
54
|
+
assert cli._looks_like_checkout_code("cs_" + "z" * 12)
|
|
55
|
+
assert not cli._looks_like_checkout_code("cs_test_short") # < 10 after marker
|
|
56
|
+
assert not cli._looks_like_checkout_code("cs_test_abc123…") # pasted ellipsis
|
|
57
|
+
assert not cli._looks_like_checkout_code("pi_123456789012") # wrong prefix
|
|
58
|
+
assert not cli._looks_like_checkout_code("")
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def test_claim_happy_path_posts_and_syncs(creds_file, capsys, monkeypatch):
|
|
62
|
+
calls = {}
|
|
63
|
+
|
|
64
|
+
def fake_http(method, path, *, body=None, **kw):
|
|
65
|
+
calls["method"], calls["path"], calls["body"] = method, path, body
|
|
66
|
+
return {"ok": True, "tier": "pro", "subscription_id": "s1", "expires_at": "2026-10-07T00:00:00Z"}
|
|
67
|
+
|
|
68
|
+
monkeypatch.setattr(cli, "http_request", fake_http)
|
|
69
|
+
monkeypatch.setattr(cli, "invalidate_tier_cache", lambda *a, **k: calls.setdefault("cache_cleared", True))
|
|
70
|
+
|
|
71
|
+
rc = cli.cmd_claim(_Args(credentials=str(creds_file), checkout_code="cs_test_a1B2c3D4e5F6g7H8"))
|
|
72
|
+
out = capsys.readouterr().out
|
|
73
|
+
assert rc == 0
|
|
74
|
+
assert calls["method"] == "POST" and calls["path"] == "/api/plugin/stripe-claim"
|
|
75
|
+
assert calls["body"]["cs"] == "cs_test_a1B2c3D4e5F6g7H8"
|
|
76
|
+
assert calls["body"]["account_id"] == ACCT
|
|
77
|
+
assert calls["body"]["session_token"] == "22222222-3333-4444-8555-666666666666" # bearer-bound
|
|
78
|
+
assert "PRO" in out
|
|
79
|
+
assert calls.get("cache_cleared") is True
|
|
80
|
+
# local tier hint synced through the atomic writer
|
|
81
|
+
assert json.loads(creds_file.read_text())["tier"] == "pro"
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def test_claim_maps_server_errors(creds_file, capsys, monkeypatch):
|
|
85
|
+
def fake_http(method, path, *, body=None, **kw):
|
|
86
|
+
raise cli.HttpError(409, {"error": "this purchase was already claimed to a different account."}, "u")
|
|
87
|
+
|
|
88
|
+
monkeypatch.setattr(cli, "http_request", fake_http)
|
|
89
|
+
rc = cli.cmd_claim(_Args(credentials=str(creds_file), checkout_code="cs_test_a1B2c3D4e5F6g7H8"))
|
|
90
|
+
out = capsys.readouterr().out
|
|
91
|
+
assert rc == 1
|
|
92
|
+
assert "different account" in out
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def test_claim_requires_activation(tmp_path, capsys):
|
|
96
|
+
rc = cli.cmd_claim(_Args(credentials=str(tmp_path / "missing.json"), checkout_code="cs_test_a1B2c3D4e5F6g7H8"))
|
|
97
|
+
out = capsys.readouterr().out
|
|
98
|
+
assert rc == 1
|
|
99
|
+
assert "sibyl init" in out
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
"""Platform support gate (2026-09-04 operator ruling).
|
|
2
|
+
|
|
3
|
+
Sibyl Memory runs on Linux, macOS (Apple Silicon and Intel), and Windows
|
|
4
|
+
through WSL2. Native Windows is not supported: `sibyl init` and `sibyl setup`
|
|
5
|
+
refuse it, every other command keeps working so existing native-Windows
|
|
6
|
+
accounts never lose `status`, `upgrade`, `claim`, `devices` or `logout`.
|
|
7
|
+
|
|
8
|
+
Six cases, and for each of them a pin on `_detect_os_family()`, whose return
|
|
9
|
+
vocabulary telemetry consumers key on and which this change must not move.
|
|
10
|
+
"""
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import platform as _platform
|
|
14
|
+
|
|
15
|
+
import pytest
|
|
16
|
+
|
|
17
|
+
from sibyl_memory_cli import cli
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _fake(monkeypatch, *, sys_platform, release="", machine="", mac_ver="", env=None):
|
|
21
|
+
monkeypatch.setattr(cli.sys, "platform", sys_platform, raising=False)
|
|
22
|
+
monkeypatch.setattr(_platform, "release", lambda: release)
|
|
23
|
+
monkeypatch.setattr(_platform, "machine", lambda: machine)
|
|
24
|
+
monkeypatch.setattr(_platform, "mac_ver", lambda: (mac_ver, ("", "", ""), ""))
|
|
25
|
+
monkeypatch.delenv("WSL_DISTRO_NAME", raising=False)
|
|
26
|
+
monkeypatch.delenv(cli.NATIVE_WINDOWS_ESCAPE_ENV, raising=False)
|
|
27
|
+
for k, v in (env or {}).items():
|
|
28
|
+
monkeypatch.setenv(k, v)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
# ---------------------------------------------------------------- supported
|
|
32
|
+
|
|
33
|
+
def test_linux(monkeypatch, capsys):
|
|
34
|
+
_fake(monkeypatch, sys_platform="linux", release="6.8.0-40-generic", machine="x86_64")
|
|
35
|
+
assert cli._platform_info() == ("linux", "Linux (x86_64)", True)
|
|
36
|
+
assert cli._detect_os_family() == "linux"
|
|
37
|
+
assert cli._is_wsl() is False
|
|
38
|
+
assert cli.print_platform_line(gate=True) == 0
|
|
39
|
+
assert "platform Linux (x86_64) · supported" in capsys.readouterr().out
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def test_wsl_detected_from_kernel_release(monkeypatch, capsys):
|
|
43
|
+
_fake(monkeypatch, sys_platform="linux",
|
|
44
|
+
release="5.15.153.1-microsoft-standard-WSL2", machine="x86_64")
|
|
45
|
+
assert cli._platform_info() == ("linux", "Linux, WSL2 (x86_64)", True)
|
|
46
|
+
assert cli._detect_os_family() == "linux"
|
|
47
|
+
assert cli._is_wsl() is True
|
|
48
|
+
assert cli.print_platform_line(gate=True) == 0
|
|
49
|
+
assert "platform Linux, WSL2 (x86_64) · supported" in capsys.readouterr().out
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def test_wsl_detected_from_env_alone(monkeypatch, capsys):
|
|
53
|
+
"""A WSL kernel that does not carry 'microsoft' in its release string still
|
|
54
|
+
exports WSL_DISTRO_NAME. Either signal is enough."""
|
|
55
|
+
_fake(monkeypatch, sys_platform="linux", release="6.6.36.6", machine="aarch64",
|
|
56
|
+
env={"WSL_DISTRO_NAME": "Ubuntu"})
|
|
57
|
+
assert cli._platform_info() == ("linux", "Linux, WSL2 (aarch64)", True)
|
|
58
|
+
assert cli._detect_os_family() == "linux"
|
|
59
|
+
assert cli._is_wsl() is True
|
|
60
|
+
assert cli.print_platform_line(gate=True) == 0
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def test_macos(monkeypatch, capsys):
|
|
64
|
+
_fake(monkeypatch, sys_platform="darwin", release="24.6.0", machine="arm64",
|
|
65
|
+
mac_ver="15.6")
|
|
66
|
+
assert cli._platform_info() == ("macos", "macOS 15.6 (arm64)", True)
|
|
67
|
+
assert cli._detect_os_family() == "macos"
|
|
68
|
+
assert cli.print_platform_line(gate=True) == 0
|
|
69
|
+
assert "platform macOS 15.6 (arm64) · supported" in capsys.readouterr().out
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
# ------------------------------------------------------------ native Windows
|
|
73
|
+
|
|
74
|
+
def test_native_windows_labels_itself_unsupported(monkeypatch):
|
|
75
|
+
_fake(monkeypatch, sys_platform="win32", release="11", machine="AMD64")
|
|
76
|
+
assert cli._platform_info() == ("windows", "Windows 11 native (AMD64)", False)
|
|
77
|
+
assert cli._detect_os_family() == "windows"
|
|
78
|
+
assert cli._is_wsl() is False
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def test_native_windows_warns_and_continues_under_warn_policy(monkeypatch, capsys):
|
|
82
|
+
_fake(monkeypatch, sys_platform="win32", release="11", machine="AMD64")
|
|
83
|
+
monkeypatch.setattr(cli, "NATIVE_WINDOWS_POLICY", "warn")
|
|
84
|
+
assert cli.print_platform_line(gate=True) == 0
|
|
85
|
+
out = capsys.readouterr().out
|
|
86
|
+
assert "platform Windows 11 native (AMD64) · not supported" in out
|
|
87
|
+
assert "Run it inside WSL2" in out
|
|
88
|
+
assert cli.WSL_DOCS_URL in out
|
|
89
|
+
assert "Continuing anyway." in out
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def test_native_windows_blocks_init_and_setup_under_block_policy(monkeypatch, capsys):
|
|
93
|
+
_fake(monkeypatch, sys_platform="win32", release="11", machine="AMD64")
|
|
94
|
+
assert cli.NATIVE_WINDOWS_POLICY == "block" # shipped policy
|
|
95
|
+
assert cli.print_platform_line(gate=True) == 1
|
|
96
|
+
out = capsys.readouterr().out
|
|
97
|
+
assert "platform Windows 11 native (AMD64) · not supported" in out
|
|
98
|
+
assert "Install it inside WSL2" in out
|
|
99
|
+
assert cli.WSL_DOCS_URL in out
|
|
100
|
+
assert "status, upgrade, claim, devices and logout keep working." in out
|
|
101
|
+
assert "SIBYL_ALLOW_NATIVE_WINDOWS=1" in out
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def test_native_windows_escape_hatch_lets_init_through(monkeypatch, capsys):
|
|
105
|
+
_fake(monkeypatch, sys_platform="win32", release="11", machine="AMD64",
|
|
106
|
+
env={"SIBYL_ALLOW_NATIVE_WINDOWS": "1"})
|
|
107
|
+
assert cli.print_platform_line(gate=True) == 0
|
|
108
|
+
assert "Continuing anyway." in capsys.readouterr().out
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def test_native_windows_never_blocks_an_ungated_command(monkeypatch, capsys):
|
|
112
|
+
"""The 76 native-Windows activations keep every command except init/setup."""
|
|
113
|
+
_fake(monkeypatch, sys_platform="win32", release="11", machine="AMD64")
|
|
114
|
+
assert cli.print_platform_line(gate=False) == 0
|
|
115
|
+
assert "Continuing anyway." in capsys.readouterr().out
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
# ------------------------------------------------------------------ unknown
|
|
119
|
+
|
|
120
|
+
def test_unknown_platform_warns_but_never_blocks(monkeypatch, capsys):
|
|
121
|
+
_fake(monkeypatch, sys_platform="freebsd14", release="14.1-RELEASE", machine="amd64")
|
|
122
|
+
assert cli._platform_info() == ("unknown", "freebsd14", False)
|
|
123
|
+
assert cli._detect_os_family() is None
|
|
124
|
+
assert cli.print_platform_line(gate=True) == 0
|
|
125
|
+
out = capsys.readouterr().out
|
|
126
|
+
assert "platform freebsd14 · not supported" in out
|
|
127
|
+
assert "built and tested on Linux and macOS" in out
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
# ------------------------------------------- session-init telemetry payload
|
|
131
|
+
|
|
132
|
+
@pytest.mark.parametrize(
|
|
133
|
+
"sys_platform,release,machine,expect_family,expect_wsl",
|
|
134
|
+
[
|
|
135
|
+
("linux", "6.8.0-40-generic", "x86_64", "linux", False),
|
|
136
|
+
("linux", "5.15.153.1-microsoft-standard-WSL2", "x86_64", "linux", True),
|
|
137
|
+
("darwin", "24.6.0", "arm64", "macos", False),
|
|
138
|
+
("win32", "11", "AMD64", "windows", False),
|
|
139
|
+
],
|
|
140
|
+
)
|
|
141
|
+
def test_session_init_env_keys(monkeypatch, sys_platform, release, machine,
|
|
142
|
+
expect_family, expect_wsl):
|
|
143
|
+
"""The three new keys the server records (section 6): os_version, arch, wsl."""
|
|
144
|
+
_fake(monkeypatch, sys_platform=sys_platform, release=release, machine=machine)
|
|
145
|
+
assert cli._detect_os_family() == expect_family
|
|
146
|
+
assert _platform.release() == release
|
|
147
|
+
assert _platform.machine() == machine
|
|
148
|
+
assert cli._is_wsl() is expect_wsl
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def test_init_sends_the_new_env_keys(tmp_path, monkeypatch):
|
|
152
|
+
"""End to end: `sibyl init` posts os_version, arch and wsl to session-init."""
|
|
153
|
+
_fake(monkeypatch, sys_platform="linux",
|
|
154
|
+
release="5.15.153.1-microsoft-standard-WSL2", machine="x86_64")
|
|
155
|
+
seen: dict = {}
|
|
156
|
+
|
|
157
|
+
def fake_http(method, path, *, body=None, timeout=15.0, headers=None):
|
|
158
|
+
if path.startswith("/api/plugin/session-init"):
|
|
159
|
+
seen.update(body["env"])
|
|
160
|
+
return {"pairing_ttl_seconds": 300}
|
|
161
|
+
if path.startswith("/api/plugin/check"):
|
|
162
|
+
return {"bound": True, "credentials": {
|
|
163
|
+
"account_id": "acct-1", "tier": "free", "bearer_token": "btok",
|
|
164
|
+
}}
|
|
165
|
+
raise AssertionError(f"unexpected call {method} {path}")
|
|
166
|
+
|
|
167
|
+
monkeypatch.setattr(cli, "http_request", fake_http)
|
|
168
|
+
monkeypatch.setattr(cli.webbrowser, "open", lambda *a, **k: True)
|
|
169
|
+
rc = cli.main(["--credentials", str(tmp_path / "credentials.json"), "init"])
|
|
170
|
+
assert rc == 0
|
|
171
|
+
assert seen["os_family"] == "linux"
|
|
172
|
+
assert seen["os_version"] == "5.15.153.1-microsoft-standard-WSL2"
|
|
173
|
+
assert seen["arch"] == "x86_64"
|
|
174
|
+
assert seen["wsl"] is True
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def test_init_refuses_native_windows_before_it_touches_the_network(tmp_path, monkeypatch):
|
|
178
|
+
_fake(monkeypatch, sys_platform="win32", release="11", machine="AMD64")
|
|
179
|
+
|
|
180
|
+
def fake_http(method, path, **kw):
|
|
181
|
+
raise AssertionError("init must refuse before calling the server")
|
|
182
|
+
|
|
183
|
+
monkeypatch.setattr(cli, "http_request", fake_http)
|
|
184
|
+
monkeypatch.setattr(cli.webbrowser, "open", lambda *a, **k: True)
|
|
185
|
+
cred = tmp_path / "credentials.json"
|
|
186
|
+
rc = cli.main(["--credentials", str(cred), "init"])
|
|
187
|
+
assert rc == 1
|
|
188
|
+
assert not cred.exists()
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""Darwin trust-store fix (2026-08-25), CLI leg: `sibyl init` on the macOS
|
|
2
|
+
python.org framework build failed every server call with
|
|
3
|
+
`Warning: session-init failed (0)` (CERTIFICATE_VERIFY_FAILED surfacing as
|
|
4
|
+
HttpError status 0). Both CLI network paths, http_request (all
|
|
5
|
+
api.sibyllabs.org calls) and _pypi_latest (update check), now pass the shared
|
|
6
|
+
certifi-backed context from sibyl_memory_client._trust, with a stdlib-default
|
|
7
|
+
fallback when the client package is absent. These tests pin the wiring at both
|
|
8
|
+
call sites and the graceful fallback."""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import ssl
|
|
12
|
+
import sys
|
|
13
|
+
import urllib.request
|
|
14
|
+
|
|
15
|
+
from sibyl_memory_cli import cli
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class _Resp:
|
|
19
|
+
def __init__(self, body: bytes = b"{}"):
|
|
20
|
+
self._body = body
|
|
21
|
+
def read(self):
|
|
22
|
+
return self._body
|
|
23
|
+
def __enter__(self):
|
|
24
|
+
return self
|
|
25
|
+
def __exit__(self, *a):
|
|
26
|
+
return False
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _shared_context():
|
|
30
|
+
from sibyl_memory_client import _trust
|
|
31
|
+
return _trust.https_context()
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def test_http_request_passes_trust_context(monkeypatch):
|
|
35
|
+
captured = {}
|
|
36
|
+
|
|
37
|
+
def fake_urlopen(req, timeout=None, context=None):
|
|
38
|
+
captured["context"] = context
|
|
39
|
+
return _Resp(b"{\"ok\": true}")
|
|
40
|
+
|
|
41
|
+
monkeypatch.setattr(urllib.request, "urlopen", fake_urlopen)
|
|
42
|
+
out = cli.http_request("GET", "/api/plugin/whoami")
|
|
43
|
+
assert out == {"ok": True}
|
|
44
|
+
assert captured["context"] is _shared_context()
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def test_pypi_latest_passes_trust_context(monkeypatch):
|
|
48
|
+
captured = {}
|
|
49
|
+
|
|
50
|
+
def fake_urlopen(req, timeout=None, context=None):
|
|
51
|
+
captured["context"] = context
|
|
52
|
+
return _Resp(b"{\"info\": {\"version\": \"9.9.9\"}}")
|
|
53
|
+
|
|
54
|
+
monkeypatch.setattr(urllib.request, "urlopen", fake_urlopen)
|
|
55
|
+
assert cli._pypi_latest("sibyl-memory-cli") == "9.9.9"
|
|
56
|
+
assert captured["context"] is _shared_context()
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def test_fallback_without_client_package(monkeypatch):
|
|
60
|
+
# sys.modules[name] = None makes the from-import raise ImportError.
|
|
61
|
+
monkeypatch.setitem(sys.modules, "sibyl_memory_client._trust", None)
|
|
62
|
+
ctx = cli._https_context() # must not raise
|
|
63
|
+
assert isinstance(ctx, ssl.SSLContext)
|
|
64
|
+
assert ctx.check_hostname is True
|
|
65
|
+
assert ctx.verify_mode == ssl.CERT_REQUIRED
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def test_fallback_still_serves_http_request(monkeypatch):
|
|
69
|
+
monkeypatch.setitem(sys.modules, "sibyl_memory_client._trust", None)
|
|
70
|
+
captured = {}
|
|
71
|
+
|
|
72
|
+
def fake_urlopen(req, timeout=None, context=None):
|
|
73
|
+
captured["context"] = context
|
|
74
|
+
return _Resp()
|
|
75
|
+
|
|
76
|
+
monkeypatch.setattr(urllib.request, "urlopen", fake_urlopen)
|
|
77
|
+
assert cli.http_request("GET", "/api/plugin/whoami") == {}
|
|
78
|
+
assert isinstance(captured["context"], ssl.SSLContext)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sibyl_memory_cli-0.4.0 → sibyl_memory_cli-0.4.1}/src/sibyl_memory_cli.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{sibyl_memory_cli-0.4.0 → sibyl_memory_cli-0.4.1}/src/sibyl_memory_cli.egg-info/entry_points.txt
RENAMED
|
File without changes
|
{sibyl_memory_cli-0.4.0 → sibyl_memory_cli-0.4.1}/src/sibyl_memory_cli.egg-info/top_level.txt
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
|