From 503f88cb1b7a85841e90e6a554723c560144e2f9 Mon Sep 17 00:00:00 2001 From: CryptoJym Date: Sun, 27 Sep 2026 19:19:13 -0600 Subject: [PATCH 1/5] Add opt-in scoped remote memory lifecycle client --- RELEASE-INVENTORY.json | 36 ++- docs/BLUEPRINT.md | 19 ++ installer/cli.py | 25 ++ installer/health.py | 36 ++- installer/onboarding.py | 17 ++ installer/remote_memory.py | 364 ++++++++++++++++++++++++++ installer/test_remote_memory.py | 447 ++++++++++++++++++++++++++++++++ memory/bin/mem0-fleet-configure | 58 +++-- 8 files changed, 964 insertions(+), 38 deletions(-) create mode 100644 installer/remote_memory.py create mode 100644 installer/test_remote_memory.py diff --git a/RELEASE-INVENTORY.json b/RELEASE-INVENTORY.json index 1bd1565..ad6a537 100644 --- a/RELEASE-INVENTORY.json +++ b/RELEASE-INVENTORY.json @@ -1,6 +1,6 @@ { "excluded_self": "RELEASE-INVENTORY.json", - "file_count": 400, + "file_count": 402, "files": [ { "bytes": 1914, @@ -808,9 +808,9 @@ "sha256": "e59dbdbab95ab3749451d539f8bca3462af6bb79ff6924fec031bc15acefbb77" }, { - "bytes": 6629, + "bytes": 10117, "path": "docs/BLUEPRINT.md", - "sha256": "264355fa8af08b6c6f529a35e497da9da746540d11457d2fdccf95fffe27aa98" + "sha256": "7cb56803faad3f9e40971e1cc5a76cdf958249dbefda171ae2faa11b1bff3b9b" }, { "bytes": 14235, @@ -1008,9 +1008,9 @@ "sha256": "4a9edbc2d7704f8d88eb4d797c7d50f411533152cfb44da6b6eae45acc43fe9e" }, { - "bytes": 9714, + "bytes": 11587, "path": "installer/cli.py", - "sha256": "cebd7eacfcb53efe76b666bef26beda8a789afb80dc1f2e6f3b53d170056743f" + "sha256": "eb784acd24ac37af252c6212313edc2496eae21a97d4ac1bbb5a85e7f2fc5431" }, { "bytes": 10302, @@ -1048,9 +1048,9 @@ "sha256": "b8573095ab48abf8bede9582452e598afd7118078f6a571d5ee86832f58a7826" }, { - "bytes": 19270, + "bytes": 20895, "path": "installer/health.py", - "sha256": "88b70d35ff8e029a90c0ad70a80871a8aa51aff2098e56acdea7213ca9670c2e" + "sha256": "f0433a51c99ccf7ceb848ce063945998ccc1014dcafe59373cc4f1a493a9c040" }, { "bytes": 14377, @@ -1088,15 +1088,20 @@ "sha256": "0d34acd4457a0de2457a2e0b1ce9d29f31e1a4d9dc2db69e358ccae79f9da79c" }, { - "bytes": 20705, + "bytes": 22256, "path": "installer/onboarding.py", - "sha256": "02860640403068e6184562161f8e452cbce31746371697b461be16a371edd281" + "sha256": "b8bae6cd2e3f971d40c95410d54866fe75d08aafa209fdc75bfd643417271468" }, { "bytes": 28468, "path": "installer/release_guard.py", "sha256": "ee32045bf09da38d41f85eb175e9073b04da6f1a388f983c8f3ae81b8bd16684" }, + { + "bytes": 19665, + "path": "installer/remote_memory.py", + "sha256": "36273fa78c7078205a13da8730387275b29b83c3c1fbcb24b322895aa042f795" + }, { "bytes": 352, "path": "installer/requirements.in", @@ -1167,6 +1172,11 @@ "path": "installer/test_release_guard.py", "sha256": "846c401865704f0cd8a3b62971a957ed12067059db33ea05b09103783d074dc2" }, + { + "bytes": 26250, + "path": "installer/test_remote_memory.py", + "sha256": "0cc4fe64df27fbe81216410bee5d43744f4c05315d2524af9b996c16cffbfb2f" + }, { "bytes": 1547, "path": "installer/test_service_limits.py", @@ -1263,9 +1273,9 @@ "sha256": "26d85a5bcec6c6eeb657cf7402e0a899f548f4a81627a3acc54b7cd018f8f51c" }, { - "bytes": 45554, + "bytes": 46802, "path": "memory/bin/mem0-fleet-configure", - "sha256": "6c837bfb9a8288d3ab242971313a2a6b3b629d72db52b4c010608bf72fd85b65" + "sha256": "31d0d4773e40625f878462e50ba689ea8b553d80e78c3ac19b60558cbb4c794d" }, { "bytes": 123922, @@ -2003,7 +2013,7 @@ "sha256": "cd4bdb4529012e0cfcd38e059215f9ea433b8ee1fa636276b36c6515e7949e28" } ], - "inventory_sha256": "927bc4b7fc93fcc354eea91f24337b98d289d616292f38fab72c87a95eb85cac", + "inventory_sha256": "4fb095bf761e5597f5ece8baf0a6e30f11cc91d4623d3a8afce15b0e0f9409c1", "schema": "borg-public-inventory/v1", - "total_bytes": 88047043 + "total_bytes": 88102743 } diff --git a/docs/BLUEPRINT.md b/docs/BLUEPRINT.md index 33215f9..ec18032 100644 --- a/docs/BLUEPRINT.md +++ b/docs/BLUEPRINT.md @@ -31,6 +31,25 @@ The shell installer needs Python 3 on PATH to validate blueprint input before bo The complete source and locked runtime package remain installed for either profile. Selection controls configured services and owner setup, not package trimming. Beads without Inbox prepares the native coordination configuration that its custody wrapper needs, without starting an Inbox service. Tools-only Codex gets the isolated BORG MCP connection with zero memory lifecycle hooks. Its watchdog probes authenticated connector liveness; absent memory is not treated as a working memory service. +An owner may explicitly opt a tools node into the existing remote Mem0 lifecycle service after the BORG primary profile and pinned Codex runtime are installed. This is a sidecar binding, not a blueprint change or a local memory service. The owner or fleet controller must first verify the native machine identity and the existing private HTTPS `/mcp` Hub route. Stage a fresh token inside this BORG home with a principal bound to its instance UUID: + +```sh +borg memory-client stage --home /absolute/private/borg-home \ + --machine exact-native-machine-id --hub-machine exact-hub-machine-id \ + --endpoint https://verified-private-hub.example/mcp \ + --principal borg-life-exact-native-machine-id-BORG-INSTANCE-UUID \ + --read-scope personal:owner --read-scope team:project --read-scope ops \ + --write-scope personal:owner +borg memory-client check --home /absolute/private/borg-home +borg memory-client enable --home /absolute/private/borg-home +``` + +Use the actual `personal:` scope and the exact `instance_id` from this home's private installation configuration. Stage prints only the token SHA-256 digest and binding metadata. Grant that digest to the named principal on the existing Hub with the exact sorted read scopes and personal write scope, using the Hub's native grant authority; no token value or provider profile is copied. `enable` first checks `memory_whoami` against the binding, then uses the bundled configurator's native `config/read`, `hooks/list` and versioned `config/batchWrite` to install and trust the four lifecycle hooks in this home's isolated primary Codex profile. It preserves unrelated hooks and their trust; a native policy or permission refusal stays a refusal. Run `check` again after native approval if needed. The generated hooks bind `BORG_HOME` explicitly so a direct isolated Codex launch uses the same owned route. The default tools profile remains hook-free until this explicit step. + +`borg doctor` reports `remote_memory_client` only for a staged sidecar. `VERIFIED` means the current scoped route authenticated and all four native hooks were discovered and trusted; it does **not** mean a conversation was captured or recalled. `lifecycle_e2e` remains `NOT_VERIFIED` until a separate real conversation canary proves capture, recall and replay on the Hub. A missing grant, changed binding, unavailable route, or native hook refusal cannot be reported as ready. The opt-in does not turn on the local Mem0, graph or model services and does not change Inbox hooks. + +For an existing tools installation whose `bin/borg` predates `memory-client`, stage the complete reviewed source release under the **same** owner's `BORG_HOME/tools/releases/<40-character-reviewed-commit>`. Preserve its original release inventory and allowlist, owner-controlled regular files and directory modes; do not put a symlink or mutable checkout at that path. Run `python3 -B BORG_HOME/tools/releases//borg.py memory-client stage|check|enable --home BORG_HOME` with the same stage arguments above. The command accepts only that exact owned release location or the intact installed `BORG_HOME/app`; it verifies the release inventory and uses the release's bundled configurator with the existing owned Codex profile, driver and route. This source-only recovery leaves the installed app, its source manifest, runtime, services, account and Inbox state untouched. It is not an in-place app upgrade or a substitute for native conversation acceptance. + Grok and Claude binaries are not supplied by checking their boxes. Their provider entries remain disabled until the owner configures and qualifies their own runtime. Onboarding documents dedicated profiles and native sign-in, plus the bundled launch-bus/provider limitations. The installer does not enable automatic provider authentication. Prepared configuration does not prove a running service, provider account or usable quota. Run `borg onboard` for selected setup instructions and `borg doctor` for observed readiness. `local_services_ready` covers selected native installation services. `ready` remains false when a selected external/research/operational capability has no automatic acceptance probe; `selected_setup` names those unverified choices. Follow the documented owner checks. Neither a checkbox nor a source directory proves those capabilities operational. diff --git a/installer/cli.py b/installer/cli.py index 33c1efb..86d2795 100644 --- a/installer/cli.py +++ b/installer/cli.py @@ -61,6 +61,15 @@ def parser() -> argparse.ArgumentParser: fleet.add_argument("--instance-id") fleet.add_argument("--label") fleet.add_argument("--role", action="append", default=[]) + memory_client = commands.add_parser("memory-client", help="stage and verify this tools node's scoped remote memory lifecycle client") + memory_client.add_argument("operation", choices=["stage", "check", "enable"]) + memory_client.add_argument("--home", type=Path, default=Path(os.environ.get("BORG_HOME", Path.home() / ".borg"))) + memory_client.add_argument("--machine") + memory_client.add_argument("--hub-machine") + memory_client.add_argument("--endpoint") + memory_client.add_argument("--principal") + memory_client.add_argument("--read-scope", action="append", default=[]) + memory_client.add_argument("--write-scope") return cli @@ -124,6 +133,22 @@ def main(argv: list[str] | None = None) -> int: elif args.command == "fleet": from installer.fleet import manage print(json.dumps(manage(doc, args), indent=2)) + elif args.command == "memory-client": + from installer import remote_memory + if args.operation == "stage": + if not all((args.machine, args.hub_machine, args.endpoint, args.principal, + args.read_scope, args.write_scope)): + raise ValueError("memory-client stage requires machine, hub-machine, endpoint, principal, read-scope and write-scope") + result = remote_memory.stage(doc, machine=args.machine, hub_machine=args.hub_machine, + endpoint=args.endpoint, principal=args.principal, read_scopes=args.read_scope, + write_scope=args.write_scope) + else: + if any((args.machine, args.hub_machine, args.endpoint, args.principal, + args.read_scope, args.write_scope)): + raise ValueError("memory-client check/enable use the existing instance-bound stage; no route arguments") + result = remote_memory.check(doc) if args.operation == "check" else remote_memory.enable(doc) + print(json.dumps(result, sort_keys=True)) + return 0 if args.operation != "enable" or result["state"] == "VERIFIED" else 1 elif args.command == "start": from installer import services result = services.start(doc, args.components or None) diff --git a/installer/health.py b/installer/health.py index 2ef5b9e..0f316ae 100644 --- a/installer/health.py +++ b/installer/health.py @@ -263,9 +263,23 @@ def status(doc: dict) -> dict: "connector": "authenticated", "inbox": "authenticated", "models": "digest_verified", "capture_hooks": "registered_and_trusted", "brain": "cycle_verified", "watchdog": "running", "graph_llm": "identity_verified"} + remote_opt_in = False if "conductor" in enabled and not blueprint.full(doc): - components["codex_client"] = tools_client_health(doc) - expected["codex_client"] = "configured_without_capture" + from installer import remote_memory + binding = root / "mem0/data/remote-client.json" + remote_opt_in = binding.exists() or binding.is_symlink() + remote = None + if remote_opt_in: + try: + remote = remote_memory.check(doc) + components["remote_memory_client"] = { + key: remote[key] for key in ("state", "route_authenticated", "native_trust_verified", "lifecycle_e2e")} + except (OSError, ValueError, RuntimeError, KeyError, TypeError): + components["remote_memory_client"] = {"state": "INVALID_BINDING", + "route_authenticated": False, "native_trust_verified": False, + "lifecycle_e2e": "NOT_VERIFIED"} + components["codex_client"] = tools_client_health(doc, remote=remote) + expected["codex_client"] = "configured_with_remote_capture" if remote_opt_in else "configured_without_capture" if doc.get("blueprint") and blueprint.selected(doc, "beads"): try: probe = subprocess.run([str(root / "bin/bd"), "list", "--limit", "1", "--json"], env=env, @@ -291,14 +305,17 @@ def status(doc: dict) -> dict: if item["id"] not in {"codex", "inbox", "beads"}] result["selected_setup"] = owner_setup result["ready"] = (result["local_services_ready"] and not owner_setup + and (not remote_opt_in or components["remote_memory_client"]["state"] == "VERIFIED") and ("conductor" not in enabled or components["provider_login"]["state"] == "authenticated_and_pinned")) - result["state"] = ("ready" if result["ready"] else "selected_setup_required" if result["local_services_ready"] and owner_setup + result["state"] = ("ready" if result["ready"] else "remote_memory_setup_required" if result["local_services_ready"] + and remote_opt_in and components["remote_memory_client"]["state"] != "VERIFIED" else + "selected_setup_required" if result["local_services_ready"] and owner_setup else "provider_sign_in_required" if result["local_services_ready"] else "setup_incomplete") return result -def tools_client_health(doc: dict) -> dict: - """Check live isolated Codex configuration; tools nodes must have no memory hooks.""" +def tools_client_health(doc: dict, *, remote: dict | None = None) -> dict: + """Check the live isolated client, including only explicitly staged remote capture.""" root = Path(doc["home"]) try: payload = {"method": "config/read", "params": {"includeLayers": True}, "timeoutMs": 8000} @@ -315,6 +332,13 @@ def tools_client_health(doc: dict) -> dict: stale = any(" hook --home " in hook.get("command", "") for groups in client.get("hooks", {}).values() if isinstance(groups, list) for group in groups for hook in group.get("hooks", [])) - return {"state": "configured_without_capture" if matched and not stale else "incomplete"} + fleet = any("mem0-fleet-hook" in hook.get("command", "") + for groups in client.get("hooks", {}).values() if isinstance(groups, list) + for group in groups for hook in group.get("hooks", [])) + if not matched or stale: + return {"state": "incomplete"} + if remote is not None: + return {"state": "configured_with_remote_capture" if remote.get("native_trust_verified") is True else "incomplete"} + return {"state": "incomplete" if fleet else "configured_without_capture"} except (OSError, ValueError, KeyError, TypeError): return {"state": "unavailable"} diff --git a/installer/onboarding.py b/installer/onboarding.py index ec97ebf..d63545b 100644 --- a/installer/onboarding.py +++ b/installer/onboarding.py @@ -153,6 +153,23 @@ def command(*args: str, purpose: str, requires: list[str] | None = None) -> dict command("doctor", purpose="Verify native hook registration and trust"), install_command], ["The receipt describes an earlier configuration action, not today's hook or MCP readiness.", "Installer client setup targets only conductors/primary/profile, never a shared global profile."])) + if doc.get("blueprint") and not blueprint.full(doc) and blueprint.selected(doc, "codex"): + binding_path = root / "mem0/data/remote-client.json" + if binding_path.exists() or binding_path.is_symlink(): + binding, binding_read = _metadata(root, "mem0/data/remote-client.json") + bound = (binding_read == "observed" and binding.get("schema") == "borg-memory-client/v1" + and binding.get("home") == str(root) and binding.get("owner") == doc["owner"] + and binding.get("instance_id") == doc["instance_id"] + and binding.get("profile") == profile) + steps.append(_step("remote-memory-client", "Verify the opt-in remote memory lifecycle client", + "staged" if bound else "incomplete", + {"binding": binding_read, "instance_matches": bound, "lifecycle_e2e": "NOT_VERIFIED"}, + ["Exact scoped Hub grant, authenticated route, four trusted native hooks, and a real capture/recall/replay canary"], + [command("memory-client", "check", purpose="Verify the scoped route and native trusted hooks"), + command("memory-client", "enable", purpose="Enable four hooks through the native Codex CAS configurator", + requires=["A matching hash-only Hub grant and native profile write approval"])], + ["A staged token digest never proves the Hub grant or route.", + "Trusted configuration does not prove conversation capture, recall or replay."])) native_commands = [] runtime = conductor.get("runtime", {}) node = runtime.get("nodeBin") if isinstance(runtime, dict) else None diff --git a/installer/remote_memory.py b/installer/remote_memory.py new file mode 100644 index 0000000..a005163 --- /dev/null +++ b/installer/remote_memory.py @@ -0,0 +1,364 @@ +"""Explicit, instance-owned remote Mem0 lifecycle client for tools nodes. + +The default tools blueprint remains hook-free. This module stages a new badge +under the BORG home, verifies its Hub identity, then delegates every Codex +config/trust mutation to the bundled native configurator. +""" +from __future__ import annotations + +import hashlib +import json +import os +from pathlib import Path +import re +import secrets +import stat +import subprocess +from urllib.parse import urlsplit + +from installer import blueprint, config, services + + +SCHEMA = "borg-memory-client/v1" +SOURCE_ROOT = Path(__file__).absolute().parent.parent +# hooks/list `eventName` and the configurator's trust receipt use these +# lower-camel native values, even though config.toml keys use PascalCase. +EVENTS = {"sessionStart", "userPromptSubmit", "stop", "sessionEnd"} +MAX_OUTPUT = 1024 * 1024 +MACHINE = re.compile(r"[a-z][a-z0-9-]{0,79}\Z") +PRINCIPAL = re.compile(r"[A-Za-z0-9][A-Za-z0-9._-]{0,127}\Z") + + +class NativeRefusal(RuntimeError): + """A native Codex operation refused; its raw output must not escape.""" + + +class NativeUncertain(NativeRefusal): + """A timed-out native operation may have committed; never replay it here.""" + + +def _paths(doc: dict) -> dict[str, Path]: + root = Path(doc["home"]) + return {"root": root, "data": root / "mem0/data", + "manifest": root / "mem0/data/remote-client.json", + "token": root / "mem0/data/fleet-token", + "endpoint": root / "mem0/data/fleet-endpoint", + "profile": root / "conductors/primary/profile", + "config": root / "conductors/primary/profile/config.toml", + "helper": SOURCE_ROOT / "memory/bin/mem0-fleet-configure", + "curl": root / "mem0/bin/mem0-mcp-curl", + "driver": root / "mem0/bin/mem0-fleet-hook", + "source_curl": SOURCE_ROOT / "memory/bin/mem0-mcp-curl", + "source_driver": SOURCE_ROOT / "memory/bin/mem0-fleet-hook", + "codex": root / "runtime/npm/node_modules/.bin/codex"} + + +def _owned_regular(path: Path, *, private: bool = False, limit: int = 1024 * 1024) -> bytes: + config.absolute_root(path.parent) + fd = os.open(path, os.O_RDONLY | os.O_NOFOLLOW) + with os.fdopen(fd, "rb") as handle: + info = os.fstat(handle.fileno()) + if (not stat.S_ISREG(info.st_mode) or info.st_uid != os.getuid() + or info.st_nlink != 1 or info.st_mode & (0o077 if private else 0o022) + or info.st_size > limit): + raise ValueError("BORG memory client path is not an owned regular file") + return handle.read(limit + 1) + + +def _url(raw: str) -> str: + if not isinstance(raw, str) or raw != raw.strip() or re.search(r"\s", raw): + raise ValueError("Memory endpoint must be an explicit credential-free HTTPS MCP URL") + try: + parsed = urlsplit(raw) + hostname = parsed.hostname + except ValueError as exc: + raise ValueError("Invalid memory endpoint") from exc + if (parsed.scheme != "https" or not hostname or parsed.username or parsed.password + or parsed.path != "/mcp" or parsed.query or parsed.fragment or not parsed.netloc): + raise ValueError("Memory endpoint must be an explicit credential-free HTTPS MCP URL") + try: + if parsed.port is not None and not 1 <= parsed.port <= 65535: + raise ValueError + except ValueError as exc: + raise ValueError("Invalid memory endpoint port") from exc + return raw + + +def _verify_source(doc: dict) -> None: + """Accept only this home's intact app or a complete owner-staged release.""" + root = Path(doc["home"]) + source = config.absolute_root(SOURCE_ROOT) + if source == root / "app": + from installer import installation + if Path(installation.__file__).absolute().parent.parent != source: + raise ValueError("BORG installed source identity differs") + installation.preflight(doc) + return + release_parent = root / "tools/releases" + if source.parent != release_parent or not re.fullmatch(r"[0-9a-f]{40}", source.name): + raise ValueError("Memory client source must be this BORG home's reviewed release") + for directory in (root / "tools", release_parent, source): + config.managed_directory(directory) + info = directory.lstat() + if not stat.S_ISDIR(info.st_mode) or info.st_uid != os.getuid() or info.st_mode & 0o022: + raise ValueError("Memory client release directory is not owner-controlled") + from installer import release_guard + inventory = source / "RELEASE-INVENTORY.json" + allowlist = source / "RELEASE-ALLOWLIST.json" + _owned_regular(inventory, limit=128 * 1024) + _owned_regular(allowlist, limit=128 * 1024) + try: + approved = release_guard._load_inventory(inventory) + if approved.get("excluded_self") != "RELEASE-INVENTORY.json": + raise ValueError("Memory client release inventory has no exact self-exclusion") + files, findings = release_guard.scan(source, skip={"RELEASE-INVENTORY.json"}, + allowlist_path=allowlist) + findings += release_guard._compare_inventory(files, approved) + except release_guard.GuardConfigurationError: + raise ValueError("Memory client release inventory is invalid") from None + if findings: + raise ValueError("Memory client release failed its exact source inventory") + expected = {"borg.py", "installer/cli.py", "installer/remote_memory.py", + "memory/bin/mem0-fleet-configure", "memory/bin/mem0-fleet-hook", + "memory/bin/mem0-mcp-curl"} + if not expected <= {str(row["path"]) for row in files}: + raise ValueError("Memory client release is missing a required source file") + for entry in source.rglob("*"): + info = entry.lstat() + if (info.st_uid != os.getuid() or info.st_mode & 0o022 + or (stat.S_ISREG(info.st_mode) and info.st_nlink != 1)): + raise ValueError("Memory client release ownership differs") + + +def _identity(doc: dict) -> dict[str, Path]: + paths = _paths(doc) + root = paths["root"] + config.absolute_root(root) + if not doc.get("blueprint") or blueprint.full(doc) or not blueprint.selected(doc, "codex"): + raise ValueError("Remote memory client requires a Codex tools blueprint") + _verify_source(doc) + for name in ("data", "profile"): + path = paths[name] + config.managed_directory(path) + info = path.lstat() + if not stat.S_ISDIR(info.st_mode) or info.st_uid != os.getuid() or info.st_mode & 0o077: + raise ValueError("BORG memory client directory is not owner-only") + _owned_regular(paths["config"], private=True) + conductor = config.read_private(root / "conductors/config.json") + primary = [row for row in conductor.get("conductors", []) if isinstance(row, dict) and row.get("id") == "primary"] + if (conductor.get("owner") != doc["owner"] or conductor.get("instance_id") != doc["instance_id"] + or conductor.get("borgHome") != str(root) or len(primary) != 1 + or primary[0].get("codexHome") != str(paths["profile"])): + raise ValueError("BORG primary conductor identity differs") + for name in ("helper", "curl", "driver", "source_curl", "source_driver"): + _owned_regular(paths[name], limit=2 * 1024 * 1024) + if any(not os.access(paths[name], os.X_OK) for name in ("helper", "curl", "driver")): + raise ValueError("BORG memory client executables are not available") + for name in ("curl", "driver"): + installed = hashlib.sha256(_owned_regular(paths[name], limit=2 * 1024 * 1024)).digest() + bundled = hashlib.sha256(_owned_regular(paths["source_" + name], limit=2 * 1024 * 1024)).digest() + if installed != bundled: + raise ValueError("Installed memory client runtime differs from the reviewed release") + binary = paths["codex"].resolve(strict=True) + if (not binary.is_relative_to(root / "runtime/npm") or not binary.is_file() + or binary.stat().st_uid != os.getuid() or binary.stat().st_mode & 0o022): + raise ValueError("Pinned BORG Codex binary differs") + runtime = conductor.get("runtime") + node_bin = runtime.get("nodeBin") if isinstance(runtime, dict) else None + if not isinstance(node_bin, str) or not Path(node_bin).is_absolute(): + raise ValueError("Pinned BORG Node runtime differs") + node = Path(node_bin).resolve(strict=True) + if (not node.is_relative_to(root / "runtime/node") or not node.is_file() + or node.stat().st_uid != os.getuid() or node.stat().st_mode & 0o022): + raise ValueError("Pinned BORG Node runtime differs") + paths["node"] = node + return paths + + +def _expected(doc: dict, *, machine: str, hub_machine: str, endpoint: str, + principal: str, read_scopes: list[str], write_scope: str) -> dict: + if not isinstance(machine, str) or not MACHINE.fullmatch(machine): + raise ValueError("Invalid native machine ID") + if not isinstance(hub_machine, str) or not MACHINE.fullmatch(hub_machine) or hub_machine == machine: + raise ValueError("Invalid distinct Hub machine ID") + owner_scope = doc["memory"]["default_scope"] + allowed = {owner_scope, "team:project", "ops"} + if (not isinstance(read_scopes, list) or not read_scopes + or any(not isinstance(scope, str) or scope not in allowed for scope in read_scopes) + or len(set(read_scopes)) != len(read_scopes) + or owner_scope not in read_scopes or write_scope != owner_scope): + raise ValueError("Remote memory scopes must stay within the owner/project/ops policy") + expected_principal = f"borg-life-{machine}-{doc['instance_id']}" + if not isinstance(principal, str) or not PRINCIPAL.fullmatch(principal) or principal != expected_principal: + raise ValueError("Remote memory principal must bind this machine and BORG instance") + return {"schema": SCHEMA, "instance_id": doc["instance_id"], "owner": doc["owner"], + "home": doc["home"], "profile": str(Path(doc["home"]) / "conductors/primary/profile"), + "machine": machine, "hub_machine": hub_machine, "endpoint": _url(endpoint), + "principal": principal, "read_scopes": sorted(read_scopes), "write_scope": write_scope} + + +def _read_manifest(doc: dict, paths: dict[str, Path]) -> dict | None: + path = paths["manifest"] + if not path.exists() and not path.is_symlink(): + return None + manifest = config.read_private(path) + if not isinstance(manifest, dict) or set(manifest) != { + "schema", "instance_id", "owner", "home", "profile", "machine", "hub_machine", + "endpoint", "principal", "read_scopes", "write_scope", "token_sha256"}: + raise ValueError("Remote memory client manifest is invalid") + expected = _expected(doc, machine=manifest["machine"], hub_machine=manifest["hub_machine"], + endpoint=manifest["endpoint"], principal=manifest["principal"], + read_scopes=manifest["read_scopes"], write_scope=manifest["write_scope"]) + if any(manifest.get(key) != value for key, value in expected.items()): + raise ValueError("Remote memory client manifest belongs to another BORG instance") + if not isinstance(manifest["token_sha256"], str) or not re.fullmatch(r"[0-9a-f]{64}", manifest["token_sha256"]): + raise ValueError("Remote memory client token digest is invalid") + token = _owned_regular(paths["token"], private=True, limit=256).strip() + if (not re.fullmatch(rb"[A-Za-z0-9_+/=-]{40,128}", token) + or hashlib.sha256(token).hexdigest() != manifest["token_sha256"]): + raise ValueError("Remote memory client token differs") + stored_endpoint = _owned_regular(paths["endpoint"], private=True, limit=2048).decode("utf-8").strip() + if stored_endpoint != manifest["endpoint"]: + raise ValueError("Remote memory client endpoint differs") + return manifest + + +def _safe_result(manifest: dict, state: str, *, route: bool = False, native: bool = False) -> dict: + return {**manifest, "state": state, "route_authenticated": route, + "native_trust_verified": native, "lifecycle_e2e": "NOT_VERIFIED"} + + +def stage(doc: dict, *, machine: str, hub_machine: str, endpoint: str, + principal: str, read_scopes: list[str], write_scope: str) -> dict: + paths = _identity(doc) + expected = _expected(doc, machine=machine, hub_machine=hub_machine, endpoint=endpoint, + principal=principal, read_scopes=read_scopes, write_scope=write_scope) + existing = _read_manifest(doc, paths) + if existing is not None: + if any(existing.get(key) != value for key, value in expected.items()): + raise ValueError("Existing remote memory binding differs; preserve and reconcile it") + return _safe_result(existing, "STAGED_NEEDS_GRANT") + if any(paths[name].exists() or paths[name].is_symlink() for name in ("token", "endpoint")): + raise ValueError("Unrecognized remote memory route exists; preserve and reconcile it") + token = secrets.token_urlsafe(36) + digest = hashlib.sha256(token.encode()).hexdigest() + config.write_private(paths["token"], token + "\n") + config.write_private(paths["endpoint"], endpoint + "\n") + manifest = {**expected, "token_sha256": digest} + config.write_private(paths["manifest"], json.dumps(manifest, sort_keys=True) + "\n") + return _safe_result(manifest, "STAGED_NEEDS_GRANT") + + +def _environment(doc: dict, paths: dict[str, Path], manifest: dict) -> dict[str, str]: + env = {**os.environ, **services.service_environment(doc)} + for key in tuple(env): + if key.startswith("MEM0_") or key in {"BORG_POLICY_FILE", "BORG_MEMORY_ENDPOINT", "MEMORY_CODEX_BIN"}: + env.pop(key, None) + env.update({"BORG_HOME": doc["home"], "BORG_LOCAL_MACHINE_ID": manifest["hub_machine"], + "MEM0_FLEET_BASE": str(paths["root"] / "mem0"), + "MEM0_FLEET_TOKEN_FILE": str(paths["token"]), + "MEM0_FLEET_ENDPOINT_FILE": str(paths["endpoint"]), + "MEM0_MACHINE": manifest["machine"], "MEM0_HARNESS": "codex", + "CODEX_HOME": str(paths["profile"]), + "PATH": str(paths["node"].parent) + os.pathsep + env.get("PATH", os.defpath)}) + return env + + +def _run(args: list[str], env: dict[str, str], *, timeout: int) -> dict: + try: + result = subprocess.run(args, env=env, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, + text=True, timeout=timeout, check=False) + except subprocess.TimeoutExpired: + raise NativeUncertain("Native memory client operation timed out; inspect native state before retrying") from None + except OSError: + raise NativeRefusal("Native memory client operation unavailable") from None + if result.returncode != 0 or len(result.stdout) > MAX_OUTPUT: + raise NativeRefusal("Native memory client operation refused; inspect the private native receipt") + try: + value = json.loads(result.stdout) + except (ValueError, TypeError): + raise NativeRefusal("Native memory client returned an unreadable receipt") from None + if not isinstance(value, dict): + raise NativeRefusal("Native memory client returned an invalid receipt") + return value + + +def _whoami(doc: dict, paths: dict[str, Path], manifest: dict) -> bool: + try: + value = _run([str(paths["curl"]), "memory_whoami", "{}"], + _environment(doc, paths, manifest), timeout=30) + except NativeRefusal: + return False + return (value.get("principal") == manifest["principal"] + and isinstance(value.get("allowed_scopes"), list) + and set(value["allowed_scopes"]) == set(manifest["read_scopes"]) + and len(value["allowed_scopes"]) == len(manifest["read_scopes"]) + and value.get("write_scope") == manifest["write_scope"]) + + +def _plan(doc: dict, paths: dict[str, Path], manifest: dict, *, apply: bool) -> tuple[dict, bool]: + args = [str(paths["helper"]), "--machine", manifest["machine"], "--codex", + "--codex-home", str(paths["profile"]), "--codex-bin", str(paths["codex"])] + if not apply: + args.append("--check") + receipt = _run(args, _environment(doc, paths, manifest), timeout=120 if apply else 60) + codex = receipt.get("codex") + profiles = codex.get("profiles") if isinstance(codex, dict) else None + if (receipt.get("status") != ("PASS" if apply else "CHECK") or receipt.get("machine") != manifest["machine"] + or not isinstance(profiles, list) or len(profiles) != 1 or codex.get("selected") != 1 + or not isinstance(profiles[0], dict) or profiles[0].get("path") != str(paths["config"])): + raise NativeRefusal("Native memory configurator receipt differs from the owned primary profile") + profile = profiles[0] + trust = profile.get("trust") + if not isinstance(trust, dict): + raise NativeRefusal("Native memory configurator omitted hook trust evidence") + trusted = {value.get("event") for key, value in trust.items() + if key != "state_upsert_required" and isinstance(value, dict) + and value.get("status_after" if apply else "status_before") == "trusted"} + exact_hooks = (len(trust) == len(EVENTS) + (0 if apply else 1) + and len({key for key in trust if key != "state_upsert_required"}) == len(EVENTS)) + ready = (exact_hooks and codex.get("changed") == 0 and profile.get("changed_fields") == [] + and profile.get("missing_fields") == [] + and trust.get("state_upsert_required") is False and trusted == EVENTS) if not apply else ( + exact_hooks and profile.get("unrelated_trust_preserved") is True and trusted == EVENTS) + return receipt, ready + + +def check(doc: dict) -> dict: + paths = _identity(doc) + manifest = _read_manifest(doc, paths) + if manifest is None: + return {"schema": SCHEMA, "state": "ABSENT", "instance_id": doc["instance_id"], + "route_authenticated": False, "native_trust_verified": False, + "lifecycle_e2e": "NOT_VERIFIED"} + route = _whoami(doc, paths, manifest) + try: + _, trusted = _plan(doc, paths, manifest, apply=False) + except NativeUncertain: + return _safe_result(manifest, "NATIVE_UNCERTAIN", route=route) + except NativeRefusal: + return _safe_result(manifest, "NATIVE_REFUSAL", route=route) + return _safe_result(manifest, "VERIFIED" if route and trusted else "NEEDS_GRANT" if not route else "NEEDS_HOOKS", + route=route, native=trusted) + + +def enable(doc: dict) -> dict: + paths = _identity(doc) + manifest = _read_manifest(doc, paths) + if manifest is None: + raise ValueError("Stage the owned remote memory client before enabling it") + if not _whoami(doc, paths, manifest): + return _safe_result(manifest, "NEEDS_GRANT") + try: + _, trusted = _plan(doc, paths, manifest, apply=False) + if not trusted: + _, applied = _plan(doc, paths, manifest, apply=True) + if not applied: + raise NativeRefusal("Native memory configurator did not trust all owned hooks") + _, trusted = _plan(doc, paths, manifest, apply=False) + except NativeUncertain: + return _safe_result(manifest, "NATIVE_UNCERTAIN", route=True) + except NativeRefusal: + return _safe_result(manifest, "NATIVE_REFUSAL", route=True) + return _safe_result(manifest, "VERIFIED" if trusted else "NEEDS_HOOKS", + route=True, native=trusted) diff --git a/installer/test_remote_memory.py b/installer/test_remote_memory.py new file mode 100644 index 0000000..65d77ac --- /dev/null +++ b/installer/test_remote_memory.py @@ -0,0 +1,447 @@ +"""Opt-in tools memory must bind one owned instance and native trust.""" +from __future__ import annotations + +import contextlib +import hashlib +import io +import json +import os +from pathlib import Path +import runpy +import shutil +import subprocess +import tempfile +import unittest +from unittest.mock import patch +from types import SimpleNamespace + +from installer import blueprint, cli, clients, config, health, onboarding, release_guard, remote_memory, services + + +FIXTURE = Path(__file__).with_name("fixtures") / "blueprint-v1.json" + + +class RemoteMemoryTests(unittest.TestCase): + def setUp(self): + temporary = tempfile.TemporaryDirectory() + self.addCleanup(temporary.cleanup) + self.root = Path(temporary.name).resolve() / "borg home" + value = json.loads(FIXTURE.read_text()) + value["machines"][0].update(profile="tools", components=["codex"], integrations=[]) + with patch.object(blueprint, "runtime_platform", return_value="macos-arm64"): + self.doc = config.initialize(self.root, "james", blueprint_selection={ + "input": value, "machine_id": "node-1"}) + self.profile = self.root / "conductors/primary/profile" + self.profile.mkdir(mode=0o700) + config.write_private(self.profile / "config.toml", "# owned profile\n") + node = self._file("runtime/node/bin/node", executable=True) + self._file("runtime/npm/node_modules/.bin/codex", executable=True) + self._file("mem0/bin/mem0-fleet-configure", executable=True) + source = Path(__file__).resolve().parents[1] + for name in ("mem0-fleet-hook", "mem0-mcp-curl"): + target = self.root / "mem0/bin" / name + shutil.copy2(source / "memory/bin" / name, target) + target.chmod(0o700) + config.write_private(self.root / "conductors/config.json", json.dumps({ + "owner": "james", "instance_id": self.doc["instance_id"], "borgHome": str(self.root), + "runtime": {"nodeBin": str(node)}, + "conductors": [{"id": "primary", "codexHome": str(self.profile)}]})) + self.args = {"machine": "studio-d63163377e6a", "hub_machine": "studio0", + "endpoint": "https://studio0.tail.example/mcp", + "principal": f"borg-life-studio-d63163377e6a-{self.doc['instance_id']}", + "read_scopes": ["ops", "personal:james", "team:project"], + "write_scope": "personal:james"} + self.env_patch = patch.object(services, "service_environment", return_value={"PATH": "/usr/bin:/bin"}) + self.env_patch.start() + self.addCleanup(self.env_patch.stop) + self.source_patch = patch.object(remote_memory, "_verify_source") + self.source_patch.start() + self.addCleanup(self.source_patch.stop) + + def _file(self, relative: str, *, executable: bool = False) -> Path: + path = self.root / relative + path.parent.mkdir(mode=0o700, parents=True, exist_ok=True) + path.write_text("#!/bin/sh\nexit 0\n") + path.chmod(0o700 if executable else 0o600) + return path + + def stage(self): + return remote_memory.stage(self.doc, **self.args) + + def _reviewed_release(self) -> Path: + source = Path(__file__).resolve().parents[1] + release = self.root / "tools/releases" / ("a" * 40) + for relative in ("borg.py", "installer/cli.py", "installer/remote_memory.py", + "memory/bin/mem0-fleet-configure", "memory/bin/mem0-fleet-hook", + "memory/bin/mem0-mcp-curl"): + target = release / relative + target.parent.mkdir(mode=0o700, parents=True, exist_ok=True) + shutil.copy2(source / relative, target) + (release / "RELEASE-ALLOWLIST.json").write_text(json.dumps({ + "schema": release_guard.ALLOWLIST_SCHEMA, "entries": []})) + adapters = [] + for name in sorted(("graphiti-extraction-qwen3-1.7b", "graphiti-extraction-qwen3-4b", + "capture-extraction-qwen3-4b")): + relative = f"adapters/{name}/adapters.safetensors" + payload = ("fixture:" + name).encode() + target = release / relative + target.parent.mkdir(mode=0o700, parents=True, exist_ok=True) + target.write_bytes(payload) + adapters.append({"name": name, "weights_present": True, "active": False, + "weights": {"path": relative, "bytes": len(payload), + "sha256": hashlib.sha256(payload).hexdigest()}}) + (release / "adapters/MANIFEST.json").write_text(json.dumps({ + "schema": "borg-adapters/v2", "distribution": {"weights_included": True}, + "adapters": adapters})) + for directory in (self.root / "tools", self.root / "tools/releases", release, *release.rglob("*")): + if directory.is_dir(): + directory.chmod(0o700) + files, findings = release_guard.scan(release, skip={"RELEASE-INVENTORY.json"}, + allowlist_path=release / "RELEASE-ALLOWLIST.json") + self.assertEqual(findings, []) + inventory = release_guard._inventory_document(files, "RELEASE-INVENTORY.json") + (release / "RELEASE-INVENTORY.json").write_text(json.dumps(inventory)) + (release / "RELEASE-INVENTORY.json").chmod(0o600) + return release + + def _receipt(self, *, apply: bool, trusted: bool) -> dict: + # These values come from native hooks/list eventName and the bundled + # configurator receipt, not the PascalCase config.toml hook keys. + trust = {f"key-{event}": {"event": event, + "status_after" if apply else "status_before": "trusted"} + for event in ("sessionStart", "userPromptSubmit", "stop", "sessionEnd")} if trusted else {} + if not apply: + trust["state_upsert_required"] = not trusted + profile = {"path": str(self.profile / "config.toml"), "trust": trust, + "changed_fields": [] if trusted else ["hooks.SessionStart"], + "missing_fields": [] if trusted else ["hooks.SessionStart"], + "unrelated_trust_preserved": True} + return {"status": "PASS" if apply else "CHECK", "machine": self.args["machine"], + "codex": {"selected": 1, "changed": 0 if trusted else 1, + "profiles": [profile]}} + + def test_stage_is_private_idempotent_and_never_prints_token(self): + first = self.stage() + second = self.stage() + self.assertEqual(first, second) + self.assertEqual(first["state"], "STAGED_NEEDS_GRANT") + token_path = self.root / "mem0/data/fleet-token" + token = token_path.read_text().strip() + self.assertEqual(hashlib.sha256(token.encode()).hexdigest(), first["token_sha256"]) + self.assertNotIn(token, json.dumps(first)) + self.assertEqual(token_path.stat().st_mode & 0o777, 0o600) + self.assertEqual((self.root / "mem0/data/remote-client.json").stat().st_mode & 0o777, 0o600) + self.assertEqual(first["read_scopes"], ["ops", "personal:james", "team:project"]) + self.assertEqual((self.profile / "config.toml").read_text(), "# owned profile\n") + + def test_stage_refuses_foreign_route_and_out_of_policy_scope(self): + for changed in ({"endpoint": "http://127.0.0.1:8795/mcp"}, + {"endpoint": "https://user:pass@studio0.tail.example/mcp"}, + {"read_scopes": ["*"]}, {"write_scope": "team:project"}, + {"principal": "someone-else"}): + with self.subTest(changed=changed), self.assertRaises(ValueError): + remote_memory.stage(self.doc, **(self.args | changed)) + self.assertFalse((self.root / "mem0/data/fleet-token").exists()) + self.stage() + with self.assertRaisesRegex(ValueError, "binding differs"): + remote_memory.stage(self.doc, **(self.args | {"endpoint": "https://other.tail.example/mcp"})) + manifest = self.root / "mem0/data/remote-client.json" + manifest.unlink() + with self.assertRaisesRegex(ValueError, "Unrecognized remote memory route"): + self.stage() + + def test_check_requires_exact_whoami_and_four_native_trusted_hooks(self): + self.stage() + def operation(args, env, *, timeout): + if Path(args[0]).name == "mem0-mcp-curl": + return {"principal": self.args["principal"], "allowed_scopes": self.args["read_scopes"], + "write_scope": self.args["write_scope"]} + return self._receipt(apply=False, trusted=True) + with patch.object(remote_memory, "_run", side_effect=operation): + result = remote_memory.check(self.doc) + self.assertEqual(result["state"], "VERIFIED") + self.assertTrue(result["route_authenticated"]) + self.assertTrue(result["native_trust_verified"]) + self.assertEqual(result["lifecycle_e2e"], "NOT_VERIFIED") + self.assertEqual(remote_memory.EVENTS, {"sessionStart", "userPromptSubmit", "stop", "sessionEnd"}) + with patch.object(remote_memory, "_run", side_effect=lambda args, env, timeout: ( + {"principal": "foreign", "allowed_scopes": self.args["read_scopes"], + "write_scope": self.args["write_scope"]} if Path(args[0]).name == "mem0-mcp-curl" + else self._receipt(apply=False, trusted=True))): + self.assertEqual(remote_memory.check(self.doc)["state"], "NEEDS_GRANT") + + def test_enable_requires_grant_before_native_write_and_reports_refusal(self): + self.stage() + calls = [] + def no_grant(args, env, *, timeout): + calls.append(args) + return {"principal": "foreign"} + with patch.object(remote_memory, "_run", side_effect=no_grant): + result = remote_memory.enable(self.doc) + self.assertEqual(result["state"], "NEEDS_GRANT") + self.assertEqual(len(calls), 1) + self.assertEqual(Path(calls[0][0]).name, "mem0-mcp-curl") + def refused(args, env, *, timeout): + if Path(args[0]).name == "mem0-mcp-curl": + return {"principal": self.args["principal"], "allowed_scopes": self.args["read_scopes"], + "write_scope": self.args["write_scope"]} + if "--check" not in args: + raise remote_memory.NativeRefusal("native 403") + return self._receipt(apply=False, trusted=False) + with patch.object(remote_memory, "_run", side_effect=refused): + result = remote_memory.enable(self.doc) + self.assertEqual(result["state"], "NATIVE_REFUSAL") + self.assertNotIn("403", json.dumps(result)) + + def test_enable_uses_native_check_apply_readback_and_keeps_e2e_unverified(self): + self.stage() + calls = [] + def operation(args, env, *, timeout): + calls.append(args) + if Path(args[0]).name == "mem0-mcp-curl": + return {"principal": self.args["principal"], "allowed_scopes": self.args["read_scopes"], + "write_scope": self.args["write_scope"]} + if "--check" not in args: + return self._receipt(apply=True, trusted=True) + return self._receipt(apply=False, trusted=len(calls) > 3) + with patch.object(remote_memory, "_run", side_effect=operation): + result = remote_memory.enable(self.doc) + self.assertEqual(result["state"], "VERIFIED") + self.assertEqual(result["lifecycle_e2e"], "NOT_VERIFIED") + self.assertEqual(["--check" in args for args in calls[1:]], [True, False, True]) + self.assertEqual((self.profile / "config.toml").read_text(), "# owned profile\n") + + def test_timed_out_apply_is_uncertain_and_never_replayed_in_same_call(self): + self.stage() + calls = [] + def operation(args, env, *, timeout): + calls.append(args) + if Path(args[0]).name == "mem0-mcp-curl": + return {"principal": self.args["principal"], "allowed_scopes": self.args["read_scopes"], + "write_scope": self.args["write_scope"]} + if "--check" in args: + return self._receipt(apply=False, trusted=False) + raise remote_memory.NativeUncertain("timed out after native CAS") + with patch.object(remote_memory, "_run", side_effect=operation): + result = remote_memory.enable(self.doc) + self.assertEqual(result["state"], "NATIVE_UNCERTAIN") + self.assertEqual(["--check" in args for args in calls[1:]], [True, False]) + with patch.object(remote_memory.subprocess, "run", side_effect=subprocess.TimeoutExpired("native", 1)): + with self.assertRaises(remote_memory.NativeUncertain): + remote_memory._run(["native"], {}, timeout=1) + + def test_staged_binding_is_rejected_if_file_or_runtime_changes(self): + self.stage() + endpoint = self.root / "mem0/data/fleet-endpoint" + endpoint.write_text("https://foreign.example/mcp\n") + with self.assertRaisesRegex(ValueError, "endpoint differs"): + remote_memory.check(self.doc) + endpoint.write_text(self.args["endpoint"] + "\n") + endpoint.chmod(0o644) + with self.assertRaises(ValueError): + remote_memory.check(self.doc) + endpoint.chmod(0o600) + conductor_path = self.root / "conductors/config.json" + conductor = config.read_private(conductor_path) + conductor["instance_id"] = "another" + config.write_private(conductor_path, json.dumps(conductor), replace=True) + with self.assertRaisesRegex(ValueError, "identity differs"): + remote_memory.check(self.doc) + + def test_installed_driver_and_curl_must_match_reviewed_source(self): + self._file("mem0/bin/mem0-mcp-curl", executable=True) + with self.assertRaisesRegex(ValueError, "runtime differs"): + self.stage() + + def test_source_only_release_uses_bundled_helper_and_refuses_drift_or_redirect(self): + release = self._reviewed_release() + self.source_patch.stop() + with patch.object(remote_memory, "SOURCE_ROOT", release): + staged = self.stage() + self.assertEqual(staged["state"], "STAGED_NEEDS_GRANT") + self.assertEqual(remote_memory._paths(self.doc)["helper"], release / "memory/bin/mem0-fleet-configure") + self.assertNotEqual((self.root / "mem0/bin/mem0-fleet-configure").read_bytes(), + (release / "memory/bin/mem0-fleet-configure").read_bytes()) + helper = release / "memory/bin/mem0-fleet-configure" + original = helper.read_bytes() + helper.write_bytes(original + b"\n# drift\n") + with self.assertRaisesRegex(ValueError, "inventory"): + remote_memory.check(self.doc) + helper.write_bytes(original) + helper.chmod(0o700) + link = release / "installer/cli.py" + link.unlink() + link.symlink_to(release / "borg.py") + with self.assertRaises(ValueError): + remote_memory.check(self.doc) + link.unlink() + shutil.copy2(Path(__file__).resolve().parents[1] / "installer/cli.py", link) + release.chmod(0o777) + with self.assertRaisesRegex(ValueError, "owner-controlled"): + remote_memory.check(self.doc) + foreign = self.root.parent / "foreign-source" + foreign.mkdir(mode=0o700) + with patch.object(remote_memory, "SOURCE_ROOT", foreign), self.assertRaisesRegex(ValueError, "reviewed release"): + remote_memory.check(self.doc) + + def test_provisioning_environment_discards_route_test_and_policy_overrides(self): + self.stage() + paths = remote_memory._identity(self.doc) + manifest = remote_memory._read_manifest(self.doc, paths) + with patch.dict(os.environ, {"MEM0_FLEET_TEST_CAPTURE_JSON": '{"principal":"foreign"}', + "MEM0_MCP_CURL_HOOK": "/foreign/driver", + "MEM0_FLEET_ENDPOINT": "https://foreign.example/mcp", + "BORG_POLICY_FILE": "/foreign/policy", + "MEMORY_CODEX_BIN": "/foreign/codex"}): + env = remote_memory._environment(self.doc, paths, manifest) + for name in ("MEM0_FLEET_TEST_CAPTURE_JSON", "MEM0_MCP_CURL_HOOK", "MEM0_FLEET_ENDPOINT", + "BORG_POLICY_FILE", "MEMORY_CODEX_BIN"): + self.assertNotIn(name, env) + self.assertEqual(env["CODEX_HOME"], str(self.profile)) + self.assertEqual(env["MEM0_MACHINE"], self.args["machine"]) + self.assertEqual(env["BORG_LOCAL_MACHINE_ID"], self.args["hub_machine"]) + + def test_onboarding_mentions_opt_in_without_reading_token_or_claiming_acceptance(self): + self.stage() + result = onboarding.plan(self.doc) + step = next(row for row in result["steps"] if row["id"] == "remote-memory-client") + self.assertEqual(step["state"], "staged") + self.assertEqual(step["observed"]["lifecycle_e2e"], "NOT_VERIFIED") + self.assertNotIn((self.root / "mem0/data/fleet-token").read_text().strip(), json.dumps(result)) + self.assertEqual([command["argv"][1] for command in step["commands"]], ["memory-client", "memory-client"]) + + def test_cli_stage_and_check_expose_only_safe_json(self): + args = ["memory-client", "stage", "--home", str(self.root), "--machine", self.args["machine"], + "--hub-machine", self.args["hub_machine"], "--endpoint", self.args["endpoint"], + "--principal", self.args["principal"], "--write-scope", self.args["write_scope"]] + for scope in self.args["read_scopes"]: + args += ["--read-scope", scope] + output = io.StringIO() + with contextlib.redirect_stdout(output): + self.assertEqual(cli.main(args), 0) + receipt = json.loads(output.getvalue()) + self.assertEqual(receipt["schema"], remote_memory.SCHEMA) + self.assertNotIn((self.root / "mem0/data/fleet-token").read_text().strip(), output.getvalue()) + + def test_generated_native_hooks_bind_home_and_exact_old_form_stays_reviewed(self): + native = runpy.run_path(str(Path(__file__).resolve().parents[1] / "memory/bin/mem0-fleet-configure")) + with patch.dict(os.environ, {"BORG_LOCAL_MACHINE_ID": self.args["hub_machine"]}): + for event, mode in (("SessionStart", "prime"), ("UserPromptSubmit", "start"), + ("Stop", "end"), ("SessionEnd", "end")): + with self.subTest(event=event): + command = native["command_for"](self.root, self.args["machine"], "codex", mode) + old = native["command_for"](self.root, self.args["machine"], "codex", mode, + include_home=False) + self.assertIn("BORG_HOME=", command) + self.assertIn("MEM0_FLEET_TOKEN_FILE=", command) + self.assertIn("MEM0_FLEET_ENDPOINT_FILE=", command) + self.assertIn("MEM0_FLEET_ENDPOINT=", command) + self.assertIn("MEM0_FLEET_BASE=", command) + self.assertIn("env -u MEM0_FLEET_TEST_CAPTURE_JSON -u MEM0_FLEET_TEST_SEARCH_JSON", command) + self.assertNotIn("BORG_LOCAL_MACHINE_ID=", command) + self.assertNotIn("BORG_HOME=", old) + self.assertTrue(native["reviewed_hook"](command, event, "codex", self.root, self.args["machine"])) + self.assertTrue(native["reviewed_hook"](old, event, "codex", self.root, self.args["machine"])) + self.assertFalse(native["reviewed_hook"](command + " ; curl foreign", event, + "codex", self.root, self.args["machine"])) + + def test_actual_bundled_check_receipt_uses_native_event_names(self): + self.stage() + native = runpy.run_path(str(Path(__file__).resolve().parents[1] / "memory/bin/mem0-fleet-configure")) + hooks = {"state": {}} + rows = [] + names = {"SessionStart": "sessionStart", "UserPromptSubmit": "userPromptSubmit", + "Stop": "stop", "SessionEnd": "sessionEnd"} + with patch.dict(os.environ, {"BORG_LOCAL_MACHINE_ID": self.args["hub_machine"]}): + for event, mode in (("SessionStart", "prime"), ("UserPromptSubmit", "start"), + ("Stop", "end"), ("SessionEnd", "end")): + command = native["command_for"](self.root, self.args["machine"], "codex", mode) + child = {"type": "command", "command": command, + "timeout": 3 if event in {"SessionStart", "Stop", "SessionEnd"} else 20} + if event == "UserPromptSubmit": + child["additionalContextLimit"] = 900 + group = {"hooks": [child]} + if event == "SessionStart": + group["matcher"] = "startup|resume|clear|compact" + hooks[event] = [group] + key = f"key-{event}" + digest = "sha256:" + format(len(rows) + 1, "064x") + hooks["state"][key] = {"trusted_hash": digest} + rows.append({"eventName": names[event], "sourcePath": str(self.profile / "config.toml"), + "command": command, "matcher": group.get("matcher", ""), + "key": key, "currentHash": digest, "trustStatus": "trusted"}) + class RPC: + def call(self, method, args): + if method == "config/read": + return {"layers": [{"name": {"type": "user", "file": str(self.profile_path)}, + "config": {"hooks": hooks}, "version": "v1"}]} + if method == "hooks/list": + return {"data": [{"hooks": rows}]} + raise AssertionError(method) + def close(self): + pass + RPC.profile_path = self.profile / "config.toml" + native["codex_plan"].__globals__["NativeRPC"] = lambda *a: RPC() + profile_receipt = native["codex_plan"](self.profile / "config.toml", self.profile, + self.root, self.args["machine"], False, "codex") + self.assertEqual({row["event"] for key, row in profile_receipt["trust"].items() + if key != "state_upsert_required"}, set(names.values())) + self.assertFalse(profile_receipt["trust"]["state_upsert_required"]) + wrapped = {"status": "CHECK", "machine": self.args["machine"], + "codex": {"selected": 1, "changed": 0, "profiles": [profile_receipt]}} + paths = remote_memory._identity(self.doc) + manifest = remote_memory._read_manifest(self.doc, paths) + with patch.object(remote_memory, "_run", return_value=wrapped): + _, trusted = remote_memory._plan(self.doc, paths, manifest, apply=False) + self.assertTrue(trusted, {"changed": profile_receipt["changed_fields"], + "missing": profile_receipt.get("missing_fields"), + "trust_count": len(profile_receipt["trust"])}) + + def test_tools_health_never_calls_unstaged_fleet_hooks_capture_free(self): + mcp = {"command": str(self.root / "mem0/venv/bin/python"), + "args": [str(self.root / "app/borg.py"), "mcp-stdio", "--home", str(self.root)]} + client = {"mcp_servers": {"borg": mcp}, "hooks": {}} + def native_response(): + body = json.dumps({"result": {"config": client}}).encode() + return SimpleNamespace(open=lambda *a, **k: io.BytesIO(body)) + with patch.object(health, "conductor_headers", return_value={}), \ + patch.object(health.urllib.request, "build_opener", side_effect=lambda *a: native_response()): + self.assertEqual(health.tools_client_health(self.doc)["state"], "configured_without_capture") + client["hooks"] = {"Stop": [{"hooks": [{"command": "env MEM0_MACHINE=studio mem0-fleet-hook end"}]}]} + self.assertEqual(health.tools_client_health(self.doc)["state"], "incomplete") + self.assertEqual(health.tools_client_health(self.doc, remote={"native_trust_verified": True})["state"], + "configured_with_remote_capture") + + def test_tools_client_rerun_preserves_remote_and_inbox_hooks(self): + self.stage() + root = self.root + mcp = {"command": str(root / "mem0/venv/bin/python"), + "args": [str(root / "app/borg.py"), "mcp-stdio", "--home", str(root)], + "startup_timeout_sec": 30, "tool_timeout_sec": 120} + hooks = {"Stop": [{"hooks": [{"command": "inbox-native report"}, + {"command": f"env BORG_HOME={root} {root}/mem0/bin/mem0-fleet-hook end"}]}], + "state": {"inbox-key": {"trusted_hash": "sha256:inbox"}, + "memory-key": {"trusted_hash": "sha256:memory"}}} + current = {"mcp_servers": {"borg": mcp}, "hooks": hooks} + writes = [] + class RPC: + def call(self, method, args): + if method == "config/batchWrite": + writes.extend(args["edits"]) + return {"status": "ok"} + return {} + def close(self): + pass + native = SimpleNamespace(NativeRPC=lambda *a: RPC(), + raw_user_layer=lambda *a: {"config": current, "version": "v1"}, + hooks_data=lambda *a: []) + with patch.object(clients.importlib.machinery.SourceFileLoader, "load_module", return_value=native): + receipt = clients.configure_clients(self.doc) + self.assertEqual(receipt["hooks"], 0) + self.assertEqual([row["keyPath"] for row in writes], ["mcp_servers.borg", "hooks.state"]) + self.assertEqual(writes[1]["value"], hooks["state"]) + self.assertEqual(current["hooks"], hooks) + + +if __name__ == "__main__": + unittest.main() diff --git a/memory/bin/mem0-fleet-configure b/memory/bin/mem0-fleet-configure index 1bf3dbd..fd08d15 100755 --- a/memory/bin/mem0-fleet-configure +++ b/memory/bin/mem0-fleet-configure @@ -79,7 +79,7 @@ def shell_value(value: str | Path) -> str: return shlex.quote(str(value)) -def command_for(home: Path, machine: str, harness: str, mode: str) -> str: +def command_for(home: Path, machine: str, harness: str, mode: str, *, include_home: bool = True) -> str: if is_local_machine(machine): token = home / "mem0/data/mcp-token" endpoint = os.environ.get("BORG_MEMORY_ENDPOINT") @@ -96,8 +96,20 @@ def command_for(home: Path, machine: str, harness: str, mode: str) -> str: f"MEM0_FLEET_TOKEN_FILE={shell_value(token)}", f"MEM0_FLEET_ENDPOINT_FILE={shell_value(endpoint)}", ] + if include_home: + # The driver checks this override before the file. Empty it so a + # direct Codex launch cannot redirect the owned token to a stale + # inherited endpoint. + env.append("MEM0_FLEET_ENDPOINT=") + env.append(f"MEM0_FLEET_BASE={shell_value(home / 'mem0')}") + # Hooks can also run from a direct CODEX_HOME launch, outside the BORG + # conductor's inherited environment. Bind the driver to its owned home. + if include_home: + env.append(f"BORG_HOME={shell_value(home)}") env.extend([f"MEM0_MACHINE={shell_value(machine)}", f"MEM0_HARNESS={harness}"]) - return "env " + " ".join(env) + " " + shell_value(installed_driver(home)) + " " + mode + clean_test_env = ("-u MEM0_FLEET_TEST_CAPTURE_JSON -u MEM0_FLEET_TEST_SEARCH_JSON " + if include_home and not is_local_machine(machine) else "") + return "env " + clean_test_env + " ".join(env) + " " + shell_value(installed_driver(home)) + " " + mode def claude_guard(command: str) -> str: @@ -114,7 +126,11 @@ def unguard(command: str) -> str: def is_memory_hook(value: Any) -> bool: - return isinstance(value, dict) and isinstance(value.get("command"), str) and bool(re.search(r"(?:^|[\s/])mem0-[A-Za-z0-9_-]+(?:\s|$)", unguard(value["command"]))) + # A private BORG home may contain spaces. shlex.quote then leaves a closing + # quote directly after the executable name; still classify it for strict + # native review instead of appending an ambiguous second writer. + return isinstance(value, dict) and isinstance(value.get("command"), str) and bool( + re.search(r"(?:^|[\s/'\"])mem0-[A-Za-z0-9_-]+(?:[\s/'\";]|$)", unguard(value["command"]))) RECALL_EVENTS = ("SessionStart", "UserPromptSubmit") @@ -230,14 +246,17 @@ def reviewed_recall(command: str, event: str, harness: str, home: Path, machine: prefix = ["env", f"MEM0_MACHINE={machine}", f"MEM0_HARNESS={harness}"] if same_command_tokens(tokens, prefix + [str(installed_driver(home)), mode]): return True - generated = command_for(home, machine, harness, mode) - if harness == "claude": - generated = claude_guard(generated) - generated_tokens = command_tokens(generated) - if generated_tokens is None: - return False - expected_tokens, expected_guard = generated_tokens - return same_command_tokens(tokens, expected_tokens) and guarded == expected_guard + for include_home in (True, False): + generated = command_for(home, machine, harness, mode, include_home=include_home) + if harness == "claude": + generated = claude_guard(generated) + generated_tokens = command_tokens(generated) + if generated_tokens is None: + continue + expected_tokens, expected_guard = generated_tokens + if same_command_tokens(tokens, expected_tokens) and guarded == expected_guard: + return True + return False def reviewed_capture(command: str, event: str, harness: str, home: Path, machine: str) -> bool: @@ -253,14 +272,15 @@ def reviewed_capture(command: str, event: str, harness: str, home: Path, machine if harness != "claude" and guarded: return False - generated = command_for(home, machine, harness, "end") - if harness == "claude": - generated = claude_guard(generated) - generated_tokens = command_tokens(generated) - if generated_tokens is not None: - expected_tokens, expected_guard = generated_tokens - if same_command_tokens(tokens, expected_tokens) and guarded == expected_guard: - return True + for include_home in (True, False): + generated = command_for(home, machine, harness, "end", include_home=include_home) + if harness == "claude": + generated = claude_guard(generated) + generated_tokens = command_tokens(generated) + if generated_tokens is not None: + expected_tokens, expected_guard = generated_tokens + if same_command_tokens(tokens, expected_tokens) and guarded == expected_guard: + return True if harness in {"claude", "codex"} and not is_local_machine(machine) and capture_skip: driver_default = [ From fc2afc9237859424917efb2295e433cf148a3d10 Mon Sep 17 00:00:00 2001 From: CryptoJym Date: Sun, 27 Sep 2026 19:49:26 -0600 Subject: [PATCH 2/5] Load remote memory clients without local model identities --- RELEASE-INVENTORY.json | 40 +++++++----- docs/BLUEPRINT.md | 2 +- installer/remote_memory.py | 51 ++++++++++++--- installer/test_remote_memory.py | 68 +++++++++++++++++++- memory/bin/borg_client_config.py | 83 +++++++++++++++++++++++++ memory/bin/mem0-fleet-configure | 42 +++++++++++-- memory/bin/mem0-fleet-hook | 8 +-- memory/tests/test_borg_client_config.py | 81 ++++++++++++++++++++++++ memory/tests/test_mem0_fleet_hook.py | 19 +++++- 9 files changed, 357 insertions(+), 37 deletions(-) create mode 100644 memory/bin/borg_client_config.py create mode 100644 memory/tests/test_borg_client_config.py diff --git a/RELEASE-INVENTORY.json b/RELEASE-INVENTORY.json index ad6a537..7251aea 100644 --- a/RELEASE-INVENTORY.json +++ b/RELEASE-INVENTORY.json @@ -1,6 +1,6 @@ { "excluded_self": "RELEASE-INVENTORY.json", - "file_count": 402, + "file_count": 404, "files": [ { "bytes": 1914, @@ -808,9 +808,9 @@ "sha256": "e59dbdbab95ab3749451d539f8bca3462af6bb79ff6924fec031bc15acefbb77" }, { - "bytes": 10117, + "bytes": 10417, "path": "docs/BLUEPRINT.md", - "sha256": "7cb56803faad3f9e40971e1cc5a76cdf958249dbefda171ae2faa11b1bff3b9b" + "sha256": "2849f1fc711105fd21eb4f583af558b95fd15a1b66093b3401487c0f218d8b29" }, { "bytes": 14235, @@ -1098,9 +1098,9 @@ "sha256": "ee32045bf09da38d41f85eb175e9073b04da6f1a388f983c8f3ae81b8bd16684" }, { - "bytes": 19665, + "bytes": 21674, "path": "installer/remote_memory.py", - "sha256": "36273fa78c7078205a13da8730387275b29b83c3c1fbcb24b322895aa042f795" + "sha256": "ac1eaaa2731d82bb0dc8e12e7df8c0bb3a23dc9ccb7b044cc0522f737330c74f" }, { "bytes": 352, @@ -1173,9 +1173,9 @@ "sha256": "846c401865704f0cd8a3b62971a957ed12067059db33ea05b09103783d074dc2" }, { - "bytes": 26250, + "bytes": 30809, "path": "installer/test_remote_memory.py", - "sha256": "0cc4fe64df27fbe81216410bee5d43744f4c05315d2524af9b996c16cffbfb2f" + "sha256": "b4d5dcc55ca3103e293041b8a73c7428187463c8174fa4925e4890dc7a74bc89" }, { "bytes": 1547, @@ -1197,6 +1197,11 @@ "path": "installer/web.py", "sha256": "b5605179c9629848b535aff4a22e840bff7d462443ba28435b0b469dfc64a925" }, + { + "bytes": 3316, + "path": "memory/bin/borg_client_config.py", + "sha256": "b3fb3319aa32d870475b3cd3716f3749babebe7aad887f7330e792e9b0103826" + }, { "bytes": 8775, "path": "memory/bin/borg_config.py", @@ -1273,14 +1278,14 @@ "sha256": "26d85a5bcec6c6eeb657cf7402e0a899f548f4a81627a3acc54b7cd018f8f51c" }, { - "bytes": 46802, + "bytes": 48738, "path": "memory/bin/mem0-fleet-configure", - "sha256": "31d0d4773e40625f878462e50ba689ea8b553d80e78c3ac19b60558cbb4c794d" + "sha256": "73b237a5126db3e5ffee195806714bd7eb9467b3db479377407ddb7e1d87f1d8" }, { - "bytes": 123922, + "bytes": 123899, "path": "memory/bin/mem0-fleet-hook", - "sha256": "dd950dd7dfeb9fc5d9b206a8655b41017d8e721031dc7610a3750c12d1433b59" + "sha256": "85b2f3688e32a2f42ea1df6e4afbd64f76d71f5db6b02571faf2318195a6918d" }, { "bytes": 22952, @@ -1522,6 +1527,11 @@ "path": "memory/tests/borg_test_support.py", "sha256": "ff928f3e63f28f3fe668bd499172ad20cc8cea71a884a0ae76cf87c1f083ecec" }, + { + "bytes": 3697, + "path": "memory/tests/test_borg_client_config.py", + "sha256": "91fefd794d8d3dfa01c8a708e95717ad47bd4fa6850a050a3ce0aac6d100199e" + }, { "bytes": 5921, "path": "memory/tests/test_borg_config.py", @@ -1543,9 +1553,9 @@ "sha256": "2c299e59ec5cef6b77475fb7b345a5288f6f9670af8400cc3b32c64a3288a643" }, { - "bytes": 6110, + "bytes": 6898, "path": "memory/tests/test_mem0_fleet_hook.py", - "sha256": "b82f9fa200c6de9ad9e973d54046c7d6d21c357f721877ee9c0fe631f6e0e7f0" + "sha256": "a33fbd0e23931651f71e2375265eeae23bfc009db83d1f5a4c9c6b5eacee31dc" }, { "bytes": 6709, @@ -2013,7 +2023,7 @@ "sha256": "cd4bdb4529012e0cfcd38e059215f9ea433b8ee1fa636276b36c6515e7949e28" } ], - "inventory_sha256": "4fb095bf761e5597f5ece8baf0a6e30f11cc91d4623d3a8afce15b0e0f9409c1", + "inventory_sha256": "649a86407326566c03af03cb724442c3b1bd25687542807bba5f70b5ab33a684", "schema": "borg-public-inventory/v1", - "total_bytes": 88102743 + "total_bytes": 88119325 } diff --git a/docs/BLUEPRINT.md b/docs/BLUEPRINT.md index ec18032..c0a1650 100644 --- a/docs/BLUEPRINT.md +++ b/docs/BLUEPRINT.md @@ -48,7 +48,7 @@ Use the actual `personal:` scope and the exact `instance_id` from th `borg doctor` reports `remote_memory_client` only for a staged sidecar. `VERIFIED` means the current scoped route authenticated and all four native hooks were discovered and trusted; it does **not** mean a conversation was captured or recalled. `lifecycle_e2e` remains `NOT_VERIFIED` until a separate real conversation canary proves capture, recall and replay on the Hub. A missing grant, changed binding, unavailable route, or native hook refusal cannot be reported as ready. The opt-in does not turn on the local Mem0, graph or model services and does not change Inbox hooks. -For an existing tools installation whose `bin/borg` predates `memory-client`, stage the complete reviewed source release under the **same** owner's `BORG_HOME/tools/releases/<40-character-reviewed-commit>`. Preserve its original release inventory and allowlist, owner-controlled regular files and directory modes; do not put a symlink or mutable checkout at that path. Run `python3 -B BORG_HOME/tools/releases//borg.py memory-client stage|check|enable --home BORG_HOME` with the same stage arguments above. The command accepts only that exact owned release location or the intact installed `BORG_HOME/app`; it verifies the release inventory and uses the release's bundled configurator with the existing owned Codex profile, driver and route. This source-only recovery leaves the installed app, its source manifest, runtime, services, account and Inbox state untouched. It is not an in-place app upgrade or a substitute for native conversation acceptance. +For an existing tools installation whose `bin/borg` predates `memory-client`, stage the complete reviewed source release under the **same** owner's `BORG_HOME/tools/releases/<40-character-reviewed-commit>`. Preserve its original release inventory and allowlist, owner-controlled regular files and directory modes; do not put a symlink or mutable checkout at that path. Run `python3 -B BORG_HOME/tools/releases//borg.py memory-client stage|check|enable --home BORG_HOME` with the same stage arguments above. The controller must verify the exact release inventory and ownership **before** executing that source; the client checks the inventory again. The bundled configurator pins its sibling hook driver in the trusted native command, and the client uses the release's sibling curl for route checks. This remote client resolves only its owned BORG home and state path; it needs no local model identity or local extraction service. The original installed driver and curl remain byte-matched to their existing app manifest and are not replaced. This source-only recovery leaves the installed app, its source manifest, runtime, services, account and Inbox state untouched. It is not an in-place app upgrade or a substitute for native conversation acceptance. Grok and Claude binaries are not supplied by checking their boxes. Their provider entries remain disabled until the owner configures and qualifies their own runtime. Onboarding documents dedicated profiles and native sign-in, plus the bundled launch-bus/provider limitations. The installer does not enable automatic provider authentication. Prepared configuration does not prove a running service, provider account or usable quota. diff --git a/installer/remote_memory.py b/installer/remote_memory.py index a005163..b967825 100644 --- a/installer/remote_memory.py +++ b/installer/remote_memory.py @@ -39,6 +39,10 @@ class NativeUncertain(NativeRefusal): def _paths(doc: dict) -> dict[str, Path]: root = Path(doc["home"]) + staged = (SOURCE_ROOT.parent == root / "tools/releases" + and re.fullmatch(r"[0-9a-f]{40}", SOURCE_ROOT.name) is not None) + installed_curl = root / "mem0/bin/mem0-mcp-curl" + installed_driver = root / "mem0/bin/mem0-fleet-hook" return {"root": root, "data": root / "mem0/data", "manifest": root / "mem0/data/remote-client.json", "token": root / "mem0/data/fleet-token", @@ -46,8 +50,9 @@ def _paths(doc: dict) -> dict[str, Path]: "profile": root / "conductors/primary/profile", "config": root / "conductors/primary/profile/config.toml", "helper": SOURCE_ROOT / "memory/bin/mem0-fleet-configure", - "curl": root / "mem0/bin/mem0-mcp-curl", - "driver": root / "mem0/bin/mem0-fleet-hook", + "curl": SOURCE_ROOT / "memory/bin/mem0-mcp-curl" if staged else installed_curl, + "driver": SOURCE_ROOT / "memory/bin/mem0-fleet-hook" if staged else installed_driver, + "installed_curl": installed_curl, "installed_driver": installed_driver, "source_curl": SOURCE_ROOT / "memory/bin/mem0-mcp-curl", "source_driver": SOURCE_ROOT / "memory/bin/mem0-fleet-hook", "codex": root / "runtime/npm/node_modules/.bin/codex"} @@ -119,7 +124,7 @@ def _verify_source(doc: dict) -> None: if findings: raise ValueError("Memory client release failed its exact source inventory") expected = {"borg.py", "installer/cli.py", "installer/remote_memory.py", - "memory/bin/mem0-fleet-configure", "memory/bin/mem0-fleet-hook", + "memory/bin/borg_client_config.py", "memory/bin/mem0-fleet-configure", "memory/bin/mem0-fleet-hook", "memory/bin/mem0-mcp-curl"} if not expected <= {str(row["path"]) for row in files}: raise ValueError("Memory client release is missing a required source file") @@ -130,6 +135,29 @@ def _verify_source(doc: dict) -> None: raise ValueError("Memory client release ownership differs") +def _verify_installed_sidecars(paths: dict[str, Path]) -> None: + """A source-only client never overwrites the immutable installed app.""" + app = paths["root"] / "app" + config.managed_directory(app) + info = app.lstat() + if not stat.S_ISDIR(info.st_mode) or info.st_uid != os.getuid() or info.st_mode & 0o022: + raise ValueError("Installed BORG app directory differs") + try: + manifest = json.loads(_owned_regular(app / "source-manifest.json", limit=128 * 1024)) + except (ValueError, TypeError): + raise ValueError("Installed BORG app manifest is invalid") from None + if not isinstance(manifest, dict): + raise ValueError("Installed BORG app manifest is invalid") + for name in ("curl", "driver"): + relative = "memory/bin/mem0-mcp-curl" if name == "curl" else "memory/bin/mem0-fleet-hook" + expected = manifest.get(relative) + if not isinstance(expected, str) or not re.fullmatch(r"[0-9a-f]{64}", expected): + raise ValueError("Installed memory sidecar manifest is incomplete") + for path in (app / relative, paths["installed_" + name]): + if hashlib.sha256(_owned_regular(path, limit=2 * 1024 * 1024)).hexdigest() != expected: + raise ValueError("Installed memory sidecar differs from its app manifest") + + def _identity(doc: dict) -> dict[str, Path]: paths = _paths(doc) root = paths["root"] @@ -150,15 +178,19 @@ def _identity(doc: dict) -> dict[str, Path]: or conductor.get("borgHome") != str(root) or len(primary) != 1 or primary[0].get("codexHome") != str(paths["profile"])): raise ValueError("BORG primary conductor identity differs") - for name in ("helper", "curl", "driver", "source_curl", "source_driver"): + for name in ("helper", "curl", "driver", "source_curl", "source_driver", + "installed_curl", "installed_driver"): _owned_regular(paths[name], limit=2 * 1024 * 1024) if any(not os.access(paths[name], os.X_OK) for name in ("helper", "curl", "driver")): raise ValueError("BORG memory client executables are not available") - for name in ("curl", "driver"): - installed = hashlib.sha256(_owned_regular(paths[name], limit=2 * 1024 * 1024)).digest() - bundled = hashlib.sha256(_owned_regular(paths["source_" + name], limit=2 * 1024 * 1024)).digest() - if installed != bundled: - raise ValueError("Installed memory client runtime differs from the reviewed release") + if paths["driver"] != paths["installed_driver"]: + _verify_installed_sidecars(paths) + else: + for name in ("curl", "driver"): + installed = hashlib.sha256(_owned_regular(paths[name], limit=2 * 1024 * 1024)).digest() + bundled = hashlib.sha256(_owned_regular(paths["source_" + name], limit=2 * 1024 * 1024)).digest() + if installed != bundled: + raise ValueError("Installed memory client runtime differs from the reviewed release") binary = paths["codex"].resolve(strict=True) if (not binary.is_relative_to(root / "runtime/npm") or not binary.is_file() or binary.stat().st_uid != os.getuid() or binary.stat().st_mode & 0o022): @@ -305,6 +337,7 @@ def _plan(doc: dict, paths: dict[str, Path], manifest: dict, *, apply: bool) -> codex = receipt.get("codex") profiles = codex.get("profiles") if isinstance(codex, dict) else None if (receipt.get("status") != ("PASS" if apply else "CHECK") or receipt.get("machine") != manifest["machine"] + or receipt.get("driver_path") != str(paths["driver"]) or not isinstance(profiles, list) or len(profiles) != 1 or codex.get("selected") != 1 or not isinstance(profiles[0], dict) or profiles[0].get("path") != str(paths["config"])): raise NativeRefusal("Native memory configurator receipt differs from the owned primary profile") diff --git a/installer/test_remote_memory.py b/installer/test_remote_memory.py index 65d77ac..942b8f0 100644 --- a/installer/test_remote_memory.py +++ b/installer/test_remote_memory.py @@ -42,6 +42,15 @@ def setUp(self): target = self.root / "mem0/bin" / name shutil.copy2(source / "memory/bin" / name, target) target.chmod(0o700) + app = self.root / "app" + installed_manifest = {} + for name in ("mem0-fleet-hook", "mem0-mcp-curl"): + relative = "memory/bin/" + name + target = app / relative + target.parent.mkdir(mode=0o700, parents=True, exist_ok=True) + shutil.copy2(self.root / "mem0/bin" / name, target) + installed_manifest[relative] = hashlib.sha256(target.read_bytes()).hexdigest() + config.write_private(app / "source-manifest.json", json.dumps(installed_manifest)) config.write_private(self.root / "conductors/config.json", json.dumps({ "owner": "james", "instance_id": self.doc["instance_id"], "borgHome": str(self.root), "runtime": {"nodeBin": str(node)}, @@ -72,7 +81,8 @@ def _reviewed_release(self) -> Path: source = Path(__file__).resolve().parents[1] release = self.root / "tools/releases" / ("a" * 40) for relative in ("borg.py", "installer/cli.py", "installer/remote_memory.py", - "memory/bin/mem0-fleet-configure", "memory/bin/mem0-fleet-hook", + "memory/bin/borg_client_config.py", "memory/bin/mem0-fleet-configure", + "memory/bin/mem0-fleet-hook", "memory/bin/mem0-mcp-curl"): target = release / relative target.parent.mkdir(mode=0o700, parents=True, exist_ok=True) @@ -117,6 +127,7 @@ def _receipt(self, *, apply: bool, trusted: bool) -> dict: "missing_fields": [] if trusted else ["hooks.SessionStart"], "unrelated_trust_preserved": True} return {"status": "PASS" if apply else "CHECK", "machine": self.args["machine"], + "driver_path": str(remote_memory._paths(self.doc)["driver"]), "codex": {"selected": 1, "changed": 0 if trusted else 1, "profiles": [profile]}} @@ -169,6 +180,13 @@ def operation(args, env, *, timeout): "write_scope": self.args["write_scope"]} if Path(args[0]).name == "mem0-mcp-curl" else self._receipt(apply=False, trusted=True))): self.assertEqual(remote_memory.check(self.doc)["state"], "NEEDS_GRANT") + def wrong_driver(args, env, *, timeout): + if Path(args[0]).name == "mem0-mcp-curl": + return {"principal": self.args["principal"], "allowed_scopes": self.args["read_scopes"], + "write_scope": self.args["write_scope"]} + return {**self._receipt(apply=False, trusted=True), "driver_path": "/foreign/driver"} + with patch.object(remote_memory, "_run", side_effect=wrong_driver): + self.assertEqual(remote_memory.check(self.doc)["state"], "NATIVE_REFUSAL") def test_enable_requires_grant_before_native_write_and_reports_refusal(self): self.stage() @@ -260,8 +278,16 @@ def test_source_only_release_uses_bundled_helper_and_refuses_drift_or_redirect(s staged = self.stage() self.assertEqual(staged["state"], "STAGED_NEEDS_GRANT") self.assertEqual(remote_memory._paths(self.doc)["helper"], release / "memory/bin/mem0-fleet-configure") + self.assertEqual(remote_memory._paths(self.doc)["driver"], release / "memory/bin/mem0-fleet-hook") + self.assertEqual(remote_memory._paths(self.doc)["curl"], release / "memory/bin/mem0-mcp-curl") self.assertNotEqual((self.root / "mem0/bin/mem0-fleet-configure").read_bytes(), (release / "memory/bin/mem0-fleet-configure").read_bytes()) + native = runpy.run_path(str(release / "memory/bin/mem0-fleet-configure")) + with patch.dict(os.environ, {"BORG_LOCAL_MACHINE_ID": self.args["hub_machine"]}): + command = native["command_for"](self.root, self.args["machine"], "codex", "end") + self.assertIn(str(release / "memory/bin/mem0-fleet-hook"), command) + legacy = native["command_for"](self.root, self.args["machine"], "codex", "end", include_home=False) + self.assertIn(str(self.root / "mem0/bin/mem0-fleet-hook"), legacy) helper = release / "memory/bin/mem0-fleet-configure" original = helper.read_bytes() helper.write_bytes(original + b"\n# drift\n") @@ -284,6 +310,45 @@ def test_source_only_release_uses_bundled_helper_and_refuses_drift_or_redirect(s with patch.object(remote_memory, "SOURCE_ROOT", foreign), self.assertRaisesRegex(ValueError, "reviewed release"): remote_memory.check(self.doc) + def test_source_only_client_preserves_old_installed_sidecars_but_refuses_drift(self): + release = self._reviewed_release() + self.source_patch.stop() + with patch.object(remote_memory, "SOURCE_ROOT", release): + self.stage() + self.assertEqual(remote_memory._paths(self.doc)["driver"], release / "memory/bin/mem0-fleet-hook") + installed = self.root / "mem0/bin/mem0-fleet-hook" + installed.write_bytes(installed.read_bytes() + b"\n# unreviewed change\n") + with self.assertRaisesRegex(ValueError, "sidecar differs"): + remote_memory.check(self.doc) + + def test_source_only_check_refuses_trusted_old_driver_hook(self): + release = self._reviewed_release() + native = runpy.run_path(str(release / "memory/bin/mem0-fleet-configure")) + with patch.dict(os.environ, {"BORG_LOCAL_MACHINE_ID": self.args["hub_machine"]}): + old = native["command_for"](self.root, self.args["machine"], "codex", "prime", include_home=False) + hook = {"type": "command", "command": old, "timeout": 3} + hooks = {"SessionStart": [{"matcher": "startup|resume|clear|compact", "hooks": [hook]}], + "state": {"old-key": {"trusted_hash": "sha256:" + "a" * 64}}} + row = {"eventName": "sessionStart", "sourcePath": str(self.profile / "config.toml"), + "command": old, "matcher": "startup|resume|clear|compact", + "key": "old-key", "currentHash": "sha256:" + "a" * 64, + "trustStatus": "trusted"} + class RPC: + def call(self, method, args): + if method == "config/read": + return {"layers": [{"name": {"type": "user", "file": str(self.profile_path)}, + "config": {"hooks": hooks}, "version": "v1"}]} + if method == "hooks/list": + return {"data": [{"hooks": [row]}]} + raise AssertionError(method) + def close(self): + pass + RPC.profile_path = self.profile / "config.toml" + native["codex_plan"].__globals__["NativeRPC"] = lambda *args: RPC() + with self.assertRaisesRegex(Exception, "not pinned to the reviewed staged driver"): + native["codex_plan"](self.profile / "config.toml", self.profile, + self.root, self.args["machine"], False, "codex") + def test_provisioning_environment_discards_route_test_and_policy_overrides(self): self.stage() paths = remote_memory._identity(self.doc) @@ -388,6 +453,7 @@ def close(self): if key != "state_upsert_required"}, set(names.values())) self.assertFalse(profile_receipt["trust"]["state_upsert_required"]) wrapped = {"status": "CHECK", "machine": self.args["machine"], + "driver_path": str(self.root / "mem0/bin/mem0-fleet-hook"), "codex": {"selected": 1, "changed": 0, "profiles": [profile_receipt]}} paths = remote_memory._identity(self.doc) manifest = remote_memory._read_manifest(self.doc, paths) diff --git a/memory/bin/borg_client_config.py b/memory/bin/borg_client_config.py new file mode 100644 index 0000000..bedfb72 --- /dev/null +++ b/memory/bin/borg_client_config.py @@ -0,0 +1,83 @@ +"""Minimal owned-home resolver for a remote fleet client. + +The fleet hook never runs a local extractor, embedder, graph, or Mem0 server. +Keep those model requirements in borg_config.py, which still validates the full +server; importing a remote client must not invent local model identities. +""" + +from __future__ import annotations + +import json +import os +from pathlib import Path +import re +import stat +import uuid + + +class ClientConfigError(ValueError): + """The client route does not belong to this owner and BORG home.""" + + +def _directory(path: Path, *, owner_only: bool = True) -> None: + try: + info = path.lstat() + except OSError as exc: + raise ClientConfigError("BORG client directory is unavailable") from exc + if (not stat.S_ISDIR(info.st_mode) or info.st_uid != os.getuid() + or info.st_mode & (0o077 if owner_only else 0o022)): + raise ClientConfigError("BORG client directory is not owner-controlled") + + +def _home() -> Path: + raw = os.environ.get("BORG_HOME", "") + path = Path(raw).expanduser() + if not raw or not path.is_absolute() or path == Path("/"): + raise ClientConfigError("BORG_HOME must be an absolute owned home") + try: + if path.resolve(strict=True) != path or any(parent.is_symlink() for parent in (path, *path.parents)): + raise ClientConfigError("BORG_HOME has a symlink or noncanonical parent") + except OSError as exc: + raise ClientConfigError("BORG_HOME is unavailable") from exc + _directory(path) + return path + + +def _document(home: Path) -> dict: + path = home / "config.json" + try: + fd = os.open(path, os.O_RDONLY | os.O_NOFOLLOW) + with os.fdopen(fd, "rb") as stream: + info = os.fstat(stream.fileno()) + if (not stat.S_ISREG(info.st_mode) or info.st_uid != os.getuid() + or info.st_nlink != 1 or info.st_mode & 0o077 or info.st_size > 1024 * 1024): + raise ClientConfigError("BORG client config is not an owned private file") + doc = json.loads(stream.read(1024 * 1024 + 1)) + except (OSError, ValueError) as exc: + raise ClientConfigError("BORG client config is unavailable or invalid") from exc + if not isinstance(doc, dict): + raise ClientConfigError("BORG client config must be an object") + return doc + + +def mem0_root() -> Path: + """Resolve only this instance's client state root, without server models.""" + home = _home() + doc = _document(home) + owner = doc.get("owner") + instance = doc.get("instance_id") + try: + canonical_instance = str(uuid.UUID(instance)) == instance + except (ValueError, TypeError, AttributeError): + canonical_instance = False + if (doc.get("schema") != "borg-install/v1" or doc.get("home") != str(home) + or not isinstance(owner, str) or not re.fullmatch(r"[a-z][a-z0-9_-]{0,47}", owner) + or not canonical_instance + or (os.environ.get("BORG_OWNER_ID") not in (None, owner))): + raise ClientConfigError("BORG client instance or owner differs") + root = home / "mem0" + _directory(root) + raw_base = os.environ.get("MEM0_FLEET_BASE") + if raw_base is not None and raw_base != str(root): + raise ClientConfigError("MEM0_FLEET_BASE must be this BORG home's mem0 root") + return root diff --git a/memory/bin/mem0-fleet-configure b/memory/bin/mem0-fleet-configure index fd08d15..cba8054 100755 --- a/memory/bin/mem0-fleet-configure +++ b/memory/bin/mem0-fleet-configure @@ -71,8 +71,31 @@ def explicit_owned_dir(raw: str, allowed: list[Path], label: str) -> Path: raise ProvisionError(f"{label} is outside owned roots") -def installed_driver(home: Path) -> Path: - return home / "mem0/bin/mem0-fleet-hook" +def installed_driver(home: Path, *, legacy: bool = False) -> Path: + """Use the reviewed sibling client only from this home's staged release. + + The older installed command remains recognizable for native preimage + review, but a new source-only tools enrollment pins the bundled driver. + """ + original = home / "mem0/bin/mem0-fleet-hook" + if legacy: + return original + script = Path(__file__).absolute() + source = script.parents[2] + if (source.parent != home / "tools/releases" + or not re.fullmatch(r"[0-9a-f]{40}", source.name)): + return original + if script != source / "memory/bin/mem0-fleet-configure" or script.resolve() != script: + raise ProvisionError("Bundled memory helper source path differs") + driver = script.with_name("mem0-fleet-hook") + try: + info = driver.lstat() + except OSError as exc: + raise ProvisionError("Bundled memory driver is unavailable") from exc + if (not stat.S_ISREG(info.st_mode) or info.st_uid != os.getuid() + or info.st_nlink != 1 or info.st_mode & 0o022 or not info.st_mode & 0o111): + raise ProvisionError("Bundled memory driver is not an owned executable") + return driver def shell_value(value: str | Path) -> str: @@ -109,7 +132,7 @@ def command_for(home: Path, machine: str, harness: str, mode: str, *, include_ho env.extend([f"MEM0_MACHINE={shell_value(machine)}", f"MEM0_HARNESS={harness}"]) clean_test_env = ("-u MEM0_FLEET_TEST_CAPTURE_JSON -u MEM0_FLEET_TEST_SEARCH_JSON " if include_home and not is_local_machine(machine) else "") - return "env " + clean_test_env + " ".join(env) + " " + shell_value(installed_driver(home)) + " " + mode + return "env " + clean_test_env + " ".join(env) + " " + shell_value(installed_driver(home, legacy=not include_home)) + " " + mode def claude_guard(command: str) -> str: @@ -244,7 +267,7 @@ def reviewed_recall(command: str, event: str, harness: str, home: Path, machine: return True if not is_local_machine(machine): prefix = ["env", f"MEM0_MACHINE={machine}", f"MEM0_HARNESS={harness}"] - if same_command_tokens(tokens, prefix + [str(installed_driver(home)), mode]): + if same_command_tokens(tokens, prefix + [str(installed_driver(home, legacy=True)), mode]): return True for include_home in (True, False): generated = command_for(home, machine, harness, mode, include_home=include_home) @@ -287,7 +310,7 @@ def reviewed_capture(command: str, event: str, harness: str, home: Path, machine "env", f"MEM0_MACHINE={machine}", f"MEM0_HARNESS={harness}", - str(installed_driver(home)), + str(installed_driver(home, legacy=True)), "end", ] if same_command_tokens(tokens, driver_default): @@ -804,6 +827,15 @@ def codex_plan(config: Path, codex_home: Path, user_home: Path, machine: str, ap row = rows[0] if not reviewed_hook(str(row.get("command", "")), event, "codex", user_home, machine): raise ProvisionError(f"unreviewed native {event} command") + if installed_driver(user_home) != installed_driver(user_home, legacy=True): + # A source-only client may recognize an old owned hook while + # reviewing the preimage, but cannot report it as ready: that + # hook would still execute the old model-dependent driver. + expected = command_tokens(command_for(user_home, machine, "codex", event_mode(event) or "")) + actual = command_tokens(str(row.get("command", ""))) + if (expected is None or actual is None or expected[1] != actual[1] + or not same_command_tokens(actual[0], expected[0])): + raise ProvisionError("native BORG hook is not pinned to the reviewed staged driver") if event == "SessionStart" and "compact" not in str(row.get("matcher", "")) and (apply or "hooks.SessionStart" not in hook_changes): raise ProvisionError("native SessionStart recall omits compact") selected.append(row) diff --git a/memory/bin/mem0-fleet-hook b/memory/bin/mem0-fleet-hook index a0b228a..d3b32b1 100755 --- a/memory/bin/mem0-fleet-hook +++ b/memory/bin/mem0-fleet-hook @@ -47,13 +47,13 @@ from pathlib import Path from typing import Any, Callable from zoneinfo import ZoneInfo -CONFIG = importlib.machinery.SourceFileLoader( - "borg_config_fleet_hook", str(Path(__file__).resolve().parent / "borg_config.py") -).load_module().CONFIG +CLIENT_CONFIG = importlib.machinery.SourceFileLoader( + "borg_client_config_fleet_hook", str(Path(__file__).resolve().parent / "borg_client_config.py") +).load_module() os.umask(0o077) -BASE = Path(os.environ.get("MEM0_FLEET_BASE", str(CONFIG.mem0_root))) +BASE = CLIENT_CONFIG.mem0_root() DATA = BASE / "data" STATE = DATA / "hook-runs" / "fleet" LOG = DATA / "fleet-hook.log" diff --git a/memory/tests/test_borg_client_config.py b/memory/tests/test_borg_client_config.py new file mode 100644 index 0000000..b3617eb --- /dev/null +++ b/memory/tests/test_borg_client_config.py @@ -0,0 +1,81 @@ +"""The real fleet driver and curl load on a tools home with no local models.""" + +from __future__ import annotations + +import json +import os +from pathlib import Path +import subprocess +import sys +import tempfile +import unittest + + +BIN = Path(__file__).resolve().parents[1] / "bin" + + +class ClientConfigSubprocessTests(unittest.TestCase): + def setUp(self): + temporary = tempfile.TemporaryDirectory() + self.addCleanup(temporary.cleanup) + self.home = Path(temporary.name).resolve() / "tools home" + self.home.mkdir(mode=0o700) + (self.home / "mem0").mkdir(mode=0o700) + self.config = self.home / "config.json" + self.document = {"schema": "borg-install/v1", "home": str(self.home), + "owner": "james", "instance_id": "00000000-0000-4000-8000-000000000001"} + self.save() + self.env = {key: value for key, value in os.environ.items() + if not key.startswith("MEM0_") and key not in {"BORG_HOME", "BORG_OWNER_ID"}} + self.env.update({"BORG_HOME": str(self.home), "BORG_OWNER_ID": "james", + "MEM0_FLEET_BASE": str(self.home / "mem0"), + "MEM0_FLEET_TOKEN_FILE": str(self.home / "mem0/data/fleet-token"), + "MEM0_MACHINE": "studio-test", "MEM0_HARNESS": "codex", + "PYTHONDONTWRITEBYTECODE": "1"}) + + def save(self): + self.config.write_text(json.dumps(self.document)) + self.config.chmod(0o600) + + def call(self, name: str, *args: str, env: dict | None = None): + return subprocess.run([sys.executable, "-B", str(BIN / name), *args], + input="{}", env=env or self.env, capture_output=True, + text=True, timeout=20, check=False) + + def test_real_driver_and_curl_load_without_local_model_ids(self): + self.assertNotIn("BORG_EXTRACTION_MODEL_ID", self.document) + self.assertNotIn("BORG_EMBED_MODEL_ID", self.document) + replay = self.call("mem0-fleet-hook", "replay") + self.assertEqual(replay.returncode, 0, replay.stderr) + environment = {**self.env, "MEM0_FLEET_TEST_CAPTURE_JSON": json.dumps({ + "principal": "bounded-test", "allowed_scopes": ["personal:james"], + "write_scope": "personal:james"})} + who = self.call("mem0-mcp-curl", "memory_whoami", "{}", env=environment) + self.assertEqual(who.returncode, 0, who.stderr) + self.assertEqual(json.loads(who.stdout)["principal"], "bounded-test") + + def test_wrong_owner_home_base_or_symlink_refuses_before_client_load(self): + cases = [] + self.document["owner"] = "someone-else" + self.save() + cases.append(self.call("mem0-fleet-hook", "replay")) + self.document["owner"] = "james" + self.document["home"] = str(self.home.parent / "foreign-home") + self.save() + cases.append(self.call("mem0-fleet-hook", "replay")) + self.document["home"] = str(self.home) + self.save() + cases.append(self.call("mem0-fleet-hook", "replay", env={**self.env, + "MEM0_FLEET_BASE": str(self.home / "foreign-base")})) + alias = self.home.parent / "alias" + alias.symlink_to(self.home, target_is_directory=True) + cases.append(self.call("mem0-mcp-curl", "memory_whoami", "{}", env={**self.env, + "BORG_HOME": str(alias), "MEM0_FLEET_TEST_CAPTURE_JSON": "{}"})) + for result in cases: + with self.subTest(stderr=result.stderr[:120]): + self.assertNotEqual(result.returncode, 0) + self.assertNotIn("bounded-test", result.stdout) + + +if __name__ == "__main__": + unittest.main() diff --git a/memory/tests/test_mem0_fleet_hook.py b/memory/tests/test_mem0_fleet_hook.py index 32def94..0591ca5 100644 --- a/memory/tests/test_mem0_fleet_hook.py +++ b/memory/tests/test_mem0_fleet_hook.py @@ -15,7 +15,18 @@ class FleetHookTests(unittest.TestCase): def setUp(self): self.tmp = tempfile.TemporaryDirectory() - os.environ["MEM0_FLEET_BASE"] = self.tmp.name + self.previous = {name: os.environ.get(name) for name in + ("BORG_HOME", "BORG_OWNER_ID", "MEM0_FLEET_BASE", "MEM0_MACHINE", + "MEM0_HARNESS", "MEM0_FLEET_ENDPOINT")} + home = Path(self.tmp.name).resolve() + (home / "mem0").mkdir(mode=0o700) + config = home / "config.json" + config.write_text(json.dumps({"schema": "borg-install/v1", "home": str(home), + "owner": "example-owner", "instance_id": "00000000-0000-4000-8000-000000000001"})) + config.chmod(0o600) + os.environ["BORG_HOME"] = str(home) + os.environ["BORG_OWNER_ID"] = "example-owner" + os.environ["MEM0_FLEET_BASE"] = str(home / "mem0") os.environ["MEM0_MACHINE"] = "test-studio" os.environ["MEM0_HARNESS"] = "codex" os.environ["MEM0_FLEET_ENDPOINT"] = "http://127.0.0.1:18765/mcp" @@ -23,7 +34,11 @@ def setUp(self): self.mod = importlib.machinery.SourceFileLoader(name, str(SCRIPT)).load_module() def tearDown(self): - os.environ.pop("MEM0_FLEET_ENDPOINT", None) + for name, value in self.previous.items(): + if value is None: + os.environ.pop(name, None) + else: + os.environ[name] = value self.tmp.cleanup() def payload(self, **extra): From 10cacc0fc2099de429410a473d0644d16ba3a1e4 Mon Sep 17 00:00:00 2001 From: CryptoJym Date: Sun, 27 Sep 2026 20:00:55 -0600 Subject: [PATCH 3/5] Keep source-only memory hooks bytecode-free --- RELEASE-INVENTORY.json | 12 ++++----- memory/bin/mem0-fleet-hook | 6 +++++ memory/tests/test_borg_client_config.py | 36 +++++++++++++++++++++++++ 3 files changed, 48 insertions(+), 6 deletions(-) diff --git a/RELEASE-INVENTORY.json b/RELEASE-INVENTORY.json index 7251aea..427e4b9 100644 --- a/RELEASE-INVENTORY.json +++ b/RELEASE-INVENTORY.json @@ -1283,9 +1283,9 @@ "sha256": "73b237a5126db3e5ffee195806714bd7eb9467b3db479377407ddb7e1d87f1d8" }, { - "bytes": 123899, + "bytes": 124225, "path": "memory/bin/mem0-fleet-hook", - "sha256": "85b2f3688e32a2f42ea1df6e4afbd64f76d71f5db6b02571faf2318195a6918d" + "sha256": "b005e1022ca34dedaea5ac58c4974e7a7f9d58fb5a1bb22971ed7cb85de3bb3b" }, { "bytes": 22952, @@ -1528,9 +1528,9 @@ "sha256": "ff928f3e63f28f3fe668bd499172ad20cc8cea71a884a0ae76cf87c1f083ecec" }, { - "bytes": 3697, + "bytes": 5801, "path": "memory/tests/test_borg_client_config.py", - "sha256": "91fefd794d8d3dfa01c8a708e95717ad47bd4fa6850a050a3ce0aac6d100199e" + "sha256": "fe98472396725fdcc256b54a783de896dfa35dbb626744cb5a564ee667254a3c" }, { "bytes": 5921, @@ -2023,7 +2023,7 @@ "sha256": "cd4bdb4529012e0cfcd38e059215f9ea433b8ee1fa636276b36c6515e7949e28" } ], - "inventory_sha256": "649a86407326566c03af03cb724442c3b1bd25687542807bba5f70b5ab33a684", + "inventory_sha256": "f37b7365bf0fa5fb1cf67f9586ae3f463ba2c4ecaf8dfa15e4b4d4c2bd07c053", "schema": "borg-public-inventory/v1", - "total_bytes": 88119325 + "total_bytes": 88121755 } diff --git a/memory/bin/mem0-fleet-hook b/memory/bin/mem0-fleet-hook index d3b32b1..619d923 100755 --- a/memory/bin/mem0-fleet-hook +++ b/memory/bin/mem0-fleet-hook @@ -47,6 +47,12 @@ from pathlib import Path from typing import Any, Callable from zoneinfo import ZoneInfo +# Native hooks may run directly from an immutable tools/releases/ +# tree, outside the installer CLI's -B process. The sibling client config +# and selector are loaded below with importlib; never create __pycache__ in +# that reviewed release during an ordinary capture or recall hook. +sys.dont_write_bytecode = True + CLIENT_CONFIG = importlib.machinery.SourceFileLoader( "borg_client_config_fleet_hook", str(Path(__file__).resolve().parent / "borg_client_config.py") ).load_module() diff --git a/memory/tests/test_borg_client_config.py b/memory/tests/test_borg_client_config.py index b3617eb..62a3ef9 100644 --- a/memory/tests/test_borg_client_config.py +++ b/memory/tests/test_borg_client_config.py @@ -5,6 +5,7 @@ import json import os from pathlib import Path +import shutil import subprocess import sys import tempfile @@ -54,6 +55,41 @@ def test_real_driver_and_curl_load_without_local_model_ids(self): self.assertEqual(who.returncode, 0, who.stderr) self.assertEqual(json.loads(who.stdout)["principal"], "bounded-test") + def test_native_hook_and_curl_leave_reviewed_release_without_bytecode(self): + """Direct shebang entry points do not inherit the installer's -B flag.""" + source_bin = self.home.parent / "reviewed-release" / "memory" / "bin" + source_bin.mkdir(parents=True) + for name in ("borg_client_config.py", "memory_selection.py", + "mem0-fleet-hook", "mem0-mcp-curl"): + shutil.copy2(BIN / name, source_bin / name) + environment = dict(self.env) + environment.pop("PYTHONDONTWRITEBYTECODE", None) + environment.pop("PYTHONPYCACHEPREFIX", None) + environment["MEM0_FLEET_TEST_SEARCH_JSON"] = "[]" + payload = {"session_id": "direct-hook-test", "turn_id": "turn-1", + "prompt": "What is the Alder paper lantern color?", "cwd": str(self.home)} + start = subprocess.run( + [sys.executable, str(source_bin / "mem0-fleet-hook"), "start"], + input=json.dumps(payload), env=environment, capture_output=True, + text=True, timeout=20, check=False, + ) + self.assertEqual(start.returncode, 0, start.stderr) + log = self.home / "mem0/data/fleet-hook.log" + events = [json.loads(line) for line in log.read_text().splitlines()] + self.assertTrue(any(row.get("event") == "start" and row.get("status") == "PASS_EMPTY" + for row in events), events) + environment["MEM0_FLEET_TEST_CAPTURE_JSON"] = json.dumps({ + "principal": "bounded-test", "allowed_scopes": ["personal:james"], + "write_scope": "personal:james"}) + who = subprocess.run( + [sys.executable, str(source_bin / "mem0-mcp-curl"), "memory_whoami", "{}"], + env=environment, capture_output=True, text=True, timeout=20, check=False, + ) + self.assertEqual(who.returncode, 0, who.stderr) + self.assertEqual(json.loads(who.stdout)["principal"], "bounded-test") + self.assertEqual(list(source_bin.rglob("__pycache__")), []) + self.assertEqual(list(source_bin.rglob("*.pyc")), []) + def test_wrong_owner_home_base_or_symlink_refuses_before_client_load(self): cases = [] self.document["owner"] = "someone-else" From ce0ffd0e51b597bcb96a572c4bd6c1d5e99519b0 Mon Sep 17 00:00:00 2001 From: CryptoJym Date: Sun, 27 Sep 2026 20:56:55 -0600 Subject: [PATCH 4/5] Schedule bounded replay for opted-in remote memory clients --- RELEASE-INVENTORY.json | 16 +-- docs/BLUEPRINT.md | 2 +- installer/remote_memory.py | 226 ++++++++++++++++++++++++++++++-- installer/test_remote_memory.py | 200 ++++++++++++++++++++++++++++ 4 files changed, 427 insertions(+), 17 deletions(-) diff --git a/RELEASE-INVENTORY.json b/RELEASE-INVENTORY.json index 427e4b9..08907c3 100644 --- a/RELEASE-INVENTORY.json +++ b/RELEASE-INVENTORY.json @@ -808,9 +808,9 @@ "sha256": "e59dbdbab95ab3749451d539f8bca3462af6bb79ff6924fec031bc15acefbb77" }, { - "bytes": 10417, + "bytes": 11166, "path": "docs/BLUEPRINT.md", - "sha256": "2849f1fc711105fd21eb4f583af558b95fd15a1b66093b3401487c0f218d8b29" + "sha256": "453a51fc58f48571aa23013909fb13577a7bf19c4abf9aec2b7cd5a366a0d787" }, { "bytes": 14235, @@ -1098,9 +1098,9 @@ "sha256": "ee32045bf09da38d41f85eb175e9073b04da6f1a388f983c8f3ae81b8bd16684" }, { - "bytes": 21674, + "bytes": 32169, "path": "installer/remote_memory.py", - "sha256": "ac1eaaa2731d82bb0dc8e12e7df8c0bb3a23dc9ccb7b044cc0522f737330c74f" + "sha256": "5768e7ee85041ef18a9533fddd3105d10de69f2913c517470c0f93b9aff0a8a4" }, { "bytes": 352, @@ -1173,9 +1173,9 @@ "sha256": "846c401865704f0cd8a3b62971a957ed12067059db33ea05b09103783d074dc2" }, { - "bytes": 30809, + "bytes": 43520, "path": "installer/test_remote_memory.py", - "sha256": "b4d5dcc55ca3103e293041b8a73c7428187463c8174fa4925e4890dc7a74bc89" + "sha256": "092706418ef908f343cc6fbfe34f2e3b0b9b2105e2af823b92be457a2a4d11aa" }, { "bytes": 1547, @@ -2023,7 +2023,7 @@ "sha256": "cd4bdb4529012e0cfcd38e059215f9ea433b8ee1fa636276b36c6515e7949e28" } ], - "inventory_sha256": "f37b7365bf0fa5fb1cf67f9586ae3f463ba2c4ecaf8dfa15e4b4d4c2bd07c053", + "inventory_sha256": "d868cbc45786a5898c353d174c415242c2554703818985a644eb12986e76173e", "schema": "borg-public-inventory/v1", - "total_bytes": 88121755 + "total_bytes": 88145710 } diff --git a/docs/BLUEPRINT.md b/docs/BLUEPRINT.md index c0a1650..3c0d757 100644 --- a/docs/BLUEPRINT.md +++ b/docs/BLUEPRINT.md @@ -46,7 +46,7 @@ borg memory-client enable --home /absolute/private/borg-home Use the actual `personal:` scope and the exact `instance_id` from this home's private installation configuration. Stage prints only the token SHA-256 digest and binding metadata. Grant that digest to the named principal on the existing Hub with the exact sorted read scopes and personal write scope, using the Hub's native grant authority; no token value or provider profile is copied. `enable` first checks `memory_whoami` against the binding, then uses the bundled configurator's native `config/read`, `hooks/list` and versioned `config/batchWrite` to install and trust the four lifecycle hooks in this home's isolated primary Codex profile. It preserves unrelated hooks and their trust; a native policy or permission refusal stays a refusal. Run `check` again after native approval if needed. The generated hooks bind `BORG_HOME` explicitly so a direct isolated Codex launch uses the same owned route. The default tools profile remains hook-free until this explicit step. -`borg doctor` reports `remote_memory_client` only for a staged sidecar. `VERIFIED` means the current scoped route authenticated and all four native hooks were discovered and trusted; it does **not** mean a conversation was captured or recalled. `lifecycle_e2e` remains `NOT_VERIFIED` until a separate real conversation canary proves capture, recall and replay on the Hub. A missing grant, changed binding, unavailable route, or native hook refusal cannot be reported as ready. The opt-in does not turn on the local Mem0, graph or model services and does not change Inbox hooks. +`borg doctor` reports `remote_memory_client` only for a staged sidecar. On macOS, `memory-client enable` also registers one instance-owned user LaunchAgent for bounded periodic replay of retained capture requests. It uses the reviewed driver, owned Python and private token/endpoint **file paths**, with a five-minute interval and caps of four requests and 30 seconds per pass. It does not put credential values in the job, modify the global miner roster, or restart other services. An absent job reports `NEEDS_REPLAY`/`replay_scheduler_state=MISSING`; an exact loaded job reports `VERIFIED`/`VERIFIED`. A foreign or stale job refuses without replacement, and a timed-out native registration remains `NATIVE_UNCERTAIN` until manually inspected. Unsupported platforms report `REPLAY_UNSUPPORTED`/`UNSUPPORTED`. `VERIFIED` means the scoped route authenticated, all four native hooks were trusted, and this exact scheduler is loaded; it does **not** mean a conversation was captured or recalled. `lifecycle_e2e` remains `NOT_VERIFIED` until a separate real conversation canary proves capture, recall and replay on the Hub. A missing grant, changed binding, unavailable route, or native hook refusal cannot be reported as ready. The opt-in does not turn on the local Mem0, graph or model services and does not change Inbox hooks. For an existing tools installation whose `bin/borg` predates `memory-client`, stage the complete reviewed source release under the **same** owner's `BORG_HOME/tools/releases/<40-character-reviewed-commit>`. Preserve its original release inventory and allowlist, owner-controlled regular files and directory modes; do not put a symlink or mutable checkout at that path. Run `python3 -B BORG_HOME/tools/releases//borg.py memory-client stage|check|enable --home BORG_HOME` with the same stage arguments above. The controller must verify the exact release inventory and ownership **before** executing that source; the client checks the inventory again. The bundled configurator pins its sibling hook driver in the trusted native command, and the client uses the release's sibling curl for route checks. This remote client resolves only its owned BORG home and state path; it needs no local model identity or local extraction service. The original installed driver and curl remain byte-matched to their existing app manifest and are not replaced. This source-only recovery leaves the installed app, its source manifest, runtime, services, account and Inbox state untouched. It is not an in-place app upgrade or a substitute for native conversation acceptance. diff --git a/installer/remote_memory.py b/installer/remote_memory.py index b967825..b083940 100644 --- a/installer/remote_memory.py +++ b/installer/remote_memory.py @@ -10,6 +10,9 @@ import json import os from pathlib import Path +import platform +import plistlib +import pwd import re import secrets import stat @@ -255,9 +258,11 @@ def _read_manifest(doc: dict, paths: dict[str, Path]) -> dict | None: return manifest -def _safe_result(manifest: dict, state: str, *, route: bool = False, native: bool = False) -> dict: +def _safe_result(manifest: dict, state: str, *, route: bool = False, native: bool = False, + replay: str = "NOT_VERIFIED") -> dict: return {**manifest, "state": state, "route_authenticated": route, - "native_trust_verified": native, "lifecycle_e2e": "NOT_VERIFIED"} + "native_trust_verified": native, "replay_scheduler_state": replay, + "lifecycle_e2e": "NOT_VERIFIED"} def stage(doc: dict, *, machine: str, hub_machine: str, endpoint: str, @@ -296,6 +301,195 @@ def _environment(doc: dict, paths: dict[str, Path], manifest: dict) -> dict[str, return env +REPLAY_INTERVAL_SECONDS = 300 +REPLAY_ITEM_CAP = 4 +REPLAY_WALL_SECONDS = 30 + + +def _launch_agents_dir(home: Path | None = None) -> Path: + # HOME is an environment input to the CLI; the native UID database binds + # the user domain and the only LaunchAgents directory we may create in it. + try: + user_home = home or Path(pwd.getpwuid(os.getuid()).pw_dir) + except KeyError: + raise NativeRefusal("Native user home is unavailable for replay") from None + directory = user_home / "Library/LaunchAgents" + for parent in (user_home, directory.parent, directory): + if not parent.exists() and not parent.is_symlink(): + if parent == directory: + continue + raise NativeRefusal("Native LaunchAgent parent is unavailable") + info = parent.lstat() + if (not stat.S_ISDIR(info.st_mode) or info.st_uid != os.getuid() + or info.st_mode & 0o022): + raise NativeRefusal("Native LaunchAgent directory is not owner-controlled") + return directory + + +def _replay_definition(doc: dict, paths: dict[str, Path], manifest: dict) -> tuple[Path, bytes, dict]: + """One opt-in user job; the installed app and all other services stay untouched.""" + if platform.system() != "Darwin": + raise NativeRefusal("Periodic remote memory replay is unsupported on this platform") + root = paths["root"] + try: + python = config.managed_python(root, root / "mem0/venv/bin/python") + except (OSError, ValueError): + raise NativeRefusal("Pinned BORG Python runtime is unavailable for replay") from None + label = services.label(doc, "memory-replay") + directory = _launch_agents_dir() + environment = { + "BORG_HOME": doc["home"], "BORG_OWNER_ID": doc["owner"], + "BORG_LOCAL_MACHINE_ID": manifest["hub_machine"], + "MEM0_FLEET_BASE": str(root / "mem0"), + "MEM0_FLEET_TOKEN_FILE": str(paths["token"]), + "MEM0_FLEET_ENDPOINT_FILE": str(paths["endpoint"]), + "MEM0_FLEET_ENDPOINT": "", + "MEM0_MACHINE": manifest["machine"], "MEM0_HARNESS": "codex", + "MEM0_REPLAY_ITEM_CAP": str(REPLAY_ITEM_CAP), + "MEM0_REPLAY_WALL_SECONDS": str(REPLAY_WALL_SECONDS), + "PYTHONDONTWRITEBYTECODE": "1", + "PATH": str(Path(python).parent) + ":/usr/bin:/bin", + } + specification = { + "Label": label, + "ProgramArguments": ["/usr/bin/env", "-u", "MEM0_FLEET_TEST_CAPTURE_JSON", + "-u", "MEM0_FLEET_TEST_SEARCH_JSON", str(python), "-B", + str(paths["driver"]), "replay"], + "WorkingDirectory": str(root), + "EnvironmentVariables": environment, + "RunAtLoad": True, "StartInterval": REPLAY_INTERVAL_SECONDS, + "ProcessType": "Background", "LowPriorityIO": True, "Nice": 5, + "Umask": 0o077, + "StandardOutPath": "/dev/null", "StandardErrorPath": "/dev/null", + } + return directory / (label + ".plist"), plistlib.dumps(specification), specification + + +def _launchctl(args: list[str], *, timeout: int = 15) -> subprocess.CompletedProcess: + try: + result = subprocess.run(["/bin/launchctl", *args], stdout=subprocess.PIPE, + stderr=subprocess.DEVNULL, text=True, timeout=timeout, check=False) + except subprocess.TimeoutExpired: + raise NativeUncertain("Native memory replay registration timed out; inspect the exact user job") from None + except OSError: + raise NativeRefusal("Native memory replay registration is unavailable") from None + if len(result.stdout) > 128 * 1024: + raise NativeRefusal("Native memory replay job response is too large") + return result + + +def _launchctl_fields(raw: str) -> tuple[dict[str, str], dict[str, list[str]]]: + """Parse only the small, stable fields needed to reject a stale loaded job.""" + fields: dict[str, str] = {} + blocks: dict[str, list[str]] = {} + lines = raw.splitlines() + index = 0 + while index < len(lines): + line = lines[index] + if line.startswith("\t") and not line.startswith("\t\t") and line.endswith(" = {"): + name = line.strip()[:-4] + values = [] + index += 1 + while index < len(lines) and lines[index] != "\t}": + if not lines[index].startswith("\t\t"): + raise NativeRefusal("Native memory replay job response is malformed") + values.append(lines[index].strip()) + index += 1 + if index == len(lines) or name in blocks: + raise NativeRefusal("Native memory replay job response is malformed") + blocks[name] = values + elif line.startswith("\t") and not line.startswith("\t\t") and " = " in line: + name, value = line.strip().split(" = ", 1) + if name in fields: + raise NativeRefusal("Native memory replay job response is malformed") + fields[name] = value + index += 1 + return fields, blocks + + +def _replay_state(doc: dict, paths: dict[str, Path], manifest: dict) -> str: + target, expected, specification = _replay_definition(doc, paths, manifest) + domain = f"gui/{os.getuid()}" + loaded = _launchctl(["print", domain + "/" + specification["Label"]]) + if loaded.returncode not in {0, 113}: + raise NativeRefusal("Native memory replay job could not be inspected") + if target.exists() or target.is_symlink(): + try: + observed = _owned_regular(target, private=True, limit=64 * 1024) + except (OSError, ValueError): + raise NativeRefusal("Existing native memory replay job is not an owned private file") from None + if observed != expected: + raise NativeRefusal("Existing native memory replay job differs; preserve it") + elif loaded.returncode == 0: + raise NativeRefusal("Loaded native memory replay job has no owned definition") + else: + return "MISSING" + if loaded.returncode == 113: + return "MISSING" + if not loaded.stdout.startswith(domain + "/" + specification["Label"] + " = {\n"): + raise NativeRefusal("Loaded native memory replay job identity differs") + fields, blocks = _launchctl_fields(loaded.stdout) + if any(fields.get(key) != value for key, value in { + "path": str(target), "type": "LaunchAgent", + "program": specification["ProgramArguments"][0], + "working directory": specification["WorkingDirectory"], + "run interval": str(REPLAY_INTERVAL_SECONDS) + " seconds"}.items()): + raise NativeRefusal("Loaded native memory replay job differs from its owned definition") + if "runatload" not in (fields.get("properties") or "").split(" | "): + raise NativeRefusal("Loaded native memory replay job did not retain RunAtLoad") + if blocks.get("arguments") != specification["ProgramArguments"]: + raise NativeRefusal("Loaded native memory replay command differs") + observed_env = {} + for row in blocks.get("environment", []): + match = re.fullmatch(r"([A-Z][A-Z0-9_]*) => ?(.*)", row) + if match is None: + raise NativeRefusal("Loaded native memory replay environment is malformed") + key, value = match.groups() + if key in observed_env: + raise NativeRefusal("Loaded native memory replay environment is malformed") + observed_env[key] = value + expected_env = specification["EnvironmentVariables"] + if (any(observed_env.get(key) != value for key, value in expected_env.items()) + or set(observed_env) - set(expected_env) - {"XPC_SERVICE_NAME"}): + raise NativeRefusal("Loaded native memory replay environment differs") + return "VERIFIED" + + +def _enable_replay(doc: dict, paths: dict[str, Path], manifest: dict) -> str: + state = _replay_state(doc, paths, manifest) + if state == "VERIFIED": + return state + target, body, specification = _replay_definition(doc, paths, manifest) + if not target.parent.exists(): + try: + target.parent.mkdir(mode=0o700) + except OSError: + raise NativeRefusal("Native LaunchAgent directory could not be created") from None + _launch_agents_dir() + if not target.exists() and not target.is_symlink(): + try: + config.write_private(target, body.decode("utf-8")) + except (OSError, ValueError): + raise NativeRefusal("Native memory replay definition could not be created") from None + try: + if _owned_regular(target, private=True, limit=64 * 1024) != body: + raise NativeRefusal("Existing native memory replay job differs; preserve it") + except (OSError, ValueError): + raise NativeRefusal("Existing native memory replay job is not an owned private file") from None + # A definite bootstrap refusal leaves the exact owned definition for an + # explicit retry. A timeout is uncertain and is never retried in this call. + result = _launchctl(["bootstrap", f"gui/{os.getuid()}", str(target)], timeout=30) + if result.returncode != 0: + raise NativeRefusal("Native memory replay bootstrap refused") + try: + verified = _replay_state(doc, paths, manifest) == "VERIFIED" + except (NativeRefusal, NativeUncertain): + verified = False + if not verified: + raise NativeUncertain("Native memory replay bootstrap completed without exact readback; inspect the user job") + return "VERIFIED" + + def _run(args: list[str], env: dict[str, str], *, timeout: int) -> dict: try: result = subprocess.run(args, env=env, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, @@ -362,8 +556,8 @@ def check(doc: dict) -> dict: manifest = _read_manifest(doc, paths) if manifest is None: return {"schema": SCHEMA, "state": "ABSENT", "instance_id": doc["instance_id"], - "route_authenticated": False, "native_trust_verified": False, - "lifecycle_e2e": "NOT_VERIFIED"} + "route_authenticated": False, "native_trust_verified": False, + "replay_scheduler_state": "NOT_VERIFIED", "lifecycle_e2e": "NOT_VERIFIED"} route = _whoami(doc, paths, manifest) try: _, trusted = _plan(doc, paths, manifest, apply=False) @@ -371,8 +565,20 @@ def check(doc: dict) -> dict: return _safe_result(manifest, "NATIVE_UNCERTAIN", route=route) except NativeRefusal: return _safe_result(manifest, "NATIVE_REFUSAL", route=route) - return _safe_result(manifest, "VERIFIED" if route and trusted else "NEEDS_GRANT" if not route else "NEEDS_HOOKS", - route=route, native=trusted) + if not route or not trusted: + return _safe_result(manifest, "NEEDS_GRANT" if not route else "NEEDS_HOOKS", + route=route, native=trusted) + if platform.system() != "Darwin": + return _safe_result(manifest, "REPLAY_UNSUPPORTED", route=True, native=True, + replay="UNSUPPORTED") + try: + replay = _replay_state(doc, paths, manifest) + except NativeUncertain: + return _safe_result(manifest, "NATIVE_UNCERTAIN", route=True, native=True) + except NativeRefusal: + return _safe_result(manifest, "NATIVE_REFUSAL", route=True, native=True) + return _safe_result(manifest, "VERIFIED" if replay == "VERIFIED" else "NEEDS_REPLAY", + route=True, native=True, replay=replay) def enable(doc: dict) -> dict: @@ -382,6 +588,9 @@ def enable(doc: dict) -> dict: raise ValueError("Stage the owned remote memory client before enabling it") if not _whoami(doc, paths, manifest): return _safe_result(manifest, "NEEDS_GRANT") + if platform.system() != "Darwin": + return _safe_result(manifest, "REPLAY_UNSUPPORTED", route=True, + replay="UNSUPPORTED") try: _, trusted = _plan(doc, paths, manifest, apply=False) if not trusted: @@ -389,9 +598,10 @@ def enable(doc: dict) -> dict: if not applied: raise NativeRefusal("Native memory configurator did not trust all owned hooks") _, trusted = _plan(doc, paths, manifest, apply=False) + replay = _enable_replay(doc, paths, manifest) if trusted else "NOT_VERIFIED" except NativeUncertain: return _safe_result(manifest, "NATIVE_UNCERTAIN", route=True) except NativeRefusal: return _safe_result(manifest, "NATIVE_REFUSAL", route=True) - return _safe_result(manifest, "VERIFIED" if trusted else "NEEDS_HOOKS", - route=True, native=trusted) + return _safe_result(manifest, "VERIFIED" if trusted and replay == "VERIFIED" else "NEEDS_HOOKS", + route=True, native=trusted, replay=replay) diff --git a/installer/test_remote_memory.py b/installer/test_remote_memory.py index 942b8f0..f172523 100644 --- a/installer/test_remote_memory.py +++ b/installer/test_remote_memory.py @@ -66,6 +66,12 @@ def setUp(self): self.source_patch = patch.object(remote_memory, "_verify_source") self.source_patch.start() self.addCleanup(self.source_patch.stop) + self.replay_check_patch = patch.object(remote_memory, "_replay_state", return_value="VERIFIED") + self.replay_check_patch.start() + self.addCleanup(self.replay_check_patch.stop) + self.replay_enable_patch = patch.object(remote_memory, "_enable_replay", return_value="VERIFIED") + self.replay_enable_patch.start() + self.addCleanup(self.replay_enable_patch.stop) def _file(self, relative: str, *, executable: bool = False) -> Path: path = self.root / relative @@ -131,6 +137,200 @@ def _receipt(self, *, apply: bool, trusted: bool) -> dict: "codex": {"selected": 1, "changed": 0 if trusted else 1, "profiles": [profile]}} + def _replay_fixture(self): + self.replay_check_patch.stop() + self.replay_enable_patch.stop() + native = self._file("runtime/python/bin/python3", executable=True) + link = self.root / "mem0/venv/bin/python" + link.parent.mkdir(mode=0o700, parents=True, exist_ok=True) + link.symlink_to(native) + user_home = self.root.parent / "native user" + (user_home / "Library/LaunchAgents").mkdir(mode=0o700, parents=True) + return user_home / "Library/LaunchAgents" + + def _native_operation(self, args, env, *, timeout): + if Path(args[0]).name == "mem0-mcp-curl": + return {"principal": self.args["principal"], "allowed_scopes": self.args["read_scopes"], + "write_scope": self.args["write_scope"]} + return self._receipt(apply="--check" not in args, trusted=True) + + @staticmethod + def _loaded_replay(target, spec): + label = spec["Label"] + rows = [f"gui/{os.getuid()}/{label} = {{", "\tactive count = 0", + f"\tpath = {target}", "\ttype = LaunchAgent", "\tstate = not running", + f"\tprogram = {spec['ProgramArguments'][0]}", "\targuments = {"] + rows += ["\t\t" + value for value in spec["ProgramArguments"]] + rows += ["\t}", f"\tworking directory = {spec['WorkingDirectory']}", + "\tenvironment = {"] + rows += [f"\t\t{key} => {value}" for key, value in spec["EnvironmentVariables"].items()] + rows += ["\t}", f"\trun interval = {spec['StartInterval']} seconds", + "\tproperties = runatload | low priority i/o", "}"] + return "\n".join(rows) + "\n" + + def test_periodic_replay_registers_once_and_never_claims_e2e(self): + directory = self._replay_fixture() + self.stage() + loaded = False + bootstraps = [] + paths = remote_memory._identity(self.doc) + manifest = remote_memory._read_manifest(self.doc, paths) + with patch.object(remote_memory, "_launch_agents_dir", return_value=directory), \ + patch.object(remote_memory.platform, "system", return_value="Darwin"): + target, body, spec = remote_memory._replay_definition(self.doc, paths, manifest) + token = (self.root / "mem0/data/fleet-token").read_text().strip() + self.assertNotIn(token, body.decode()) + self.assertEqual(spec["ProgramArguments"], ["/usr/bin/env", "-u", + "MEM0_FLEET_TEST_CAPTURE_JSON", "-u", "MEM0_FLEET_TEST_SEARCH_JSON", + str(self.root / "mem0/venv/bin/python"), "-B", str(paths["driver"]), "replay"]) + self.assertEqual(spec["EnvironmentVariables"]["MEM0_MACHINE"], self.args["machine"]) + self.assertEqual(spec["EnvironmentVariables"]["MEM0_FLEET_ENDPOINT"], "") + self.assertEqual(spec["StartInterval"], 300) + def launchctl(args, *, timeout=15): + nonlocal loaded + if args[0] == "print": + return subprocess.CompletedProcess(args, 0 if loaded else 113, + self._loaded_replay(target, spec) if loaded else "") + bootstraps.append(args) + loaded = True + return subprocess.CompletedProcess(args, 0, "") + with patch.object(remote_memory, "_launchctl", side_effect=launchctl), \ + patch.object(remote_memory, "_run", side_effect=self._native_operation): + before = remote_memory.check(self.doc) + self.assertEqual((before["state"], before["replay_scheduler_state"]), + ("NEEDS_REPLAY", "MISSING")) + enabled = remote_memory.enable(self.doc) + self.assertEqual(enabled["state"], "VERIFIED") + self.assertEqual(enabled["replay_scheduler_state"], "VERIFIED") + self.assertEqual(enabled["lifecycle_e2e"], "NOT_VERIFIED") + self.assertEqual(remote_memory.check(self.doc)["state"], "VERIFIED") + self.assertEqual(remote_memory.enable(self.doc)["state"], "VERIFIED") + self.assertEqual(len(bootstraps), 1) + self.assertEqual(target.read_bytes(), body) + self.assertEqual(target.stat().st_mode & 0o777, 0o600) + + def test_replay_rejects_foreign_stale_or_symlinked_job_and_loaded_collision(self): + directory = self._replay_fixture() + self.stage() + paths = remote_memory._identity(self.doc) + manifest = remote_memory._read_manifest(self.doc, paths) + with patch.object(remote_memory, "_launch_agents_dir", return_value=directory), \ + patch.object(remote_memory.platform, "system", return_value="Darwin"): + target, body, spec = remote_memory._replay_definition(self.doc, paths, manifest) + target.write_bytes(body + b"\n") + target.chmod(0o600) + with patch.object(remote_memory, "_launchctl", return_value=subprocess.CompletedProcess([], 113, "")): + with self.assertRaisesRegex(remote_memory.NativeRefusal, "differs"): + remote_memory._enable_replay(self.doc, paths, manifest) + self.assertEqual(target.read_bytes(), body + b"\n") + target.unlink() + target.symlink_to(self.root / "config.json") + with patch.object(remote_memory, "_launchctl", return_value=subprocess.CompletedProcess([], 113, "")): + with self.assertRaisesRegex(remote_memory.NativeRefusal, "owned private"): + remote_memory._replay_state(self.doc, paths, manifest) + target.unlink() + with patch.object(remote_memory, "_launchctl", return_value=subprocess.CompletedProcess([], 0, + self._loaded_replay(target, spec))): + with self.assertRaisesRegex(remote_memory.NativeRefusal, "no owned definition"): + remote_memory._replay_state(self.doc, paths, manifest) + target.write_bytes(body); target.chmod(0o600) + wrong = dict(spec, ProgramArguments=[*spec["ProgramArguments"][:-2], "/foreign/driver", "replay"]) + with patch.object(remote_memory, "_launchctl", return_value=subprocess.CompletedProcess([], 0, + self._loaded_replay(target, wrong))): + with self.assertRaisesRegex(remote_memory.NativeRefusal, "command differs"): + remote_memory._enable_replay(self.doc, paths, manifest) + wrong_environment = dict(spec, EnvironmentVariables={**spec["EnvironmentVariables"], + "MEM0_MACHINE": "another-native-machine"}) + with patch.object(remote_memory, "_launchctl", return_value=subprocess.CompletedProcess([], 0, + self._loaded_replay(target, wrong_environment))): + with self.assertRaisesRegex(remote_memory.NativeRefusal, "environment differs"): + remote_memory._replay_state(self.doc, paths, manifest) + unexpected_environment = dict(spec, EnvironmentVariables={**spec["EnvironmentVariables"], + "MEM0_FLEET_TEST_CAPTURE_JSON": "foreign-override"}) + with patch.object(remote_memory, "_launchctl", return_value=subprocess.CompletedProcess([], 0, + self._loaded_replay(target, unexpected_environment))): + with self.assertRaisesRegex(remote_memory.NativeRefusal, "environment differs"): + remote_memory._replay_state(self.doc, paths, manifest) + + def test_replay_release_driver_is_pinned_and_launch_agent_directory_is_owned(self): + directory = self._replay_fixture() + self.stage() + release = self.root / "tools/releases" / ("f" * 40) + with patch.object(remote_memory, "SOURCE_ROOT", release): + paths = remote_memory._paths(self.doc) + self.assertEqual(paths["driver"], release / "memory/bin/mem0-fleet-hook") + manifest = remote_memory._read_manifest(self.doc, paths) + with patch.object(remote_memory, "_launch_agents_dir", return_value=directory), \ + patch.object(remote_memory.platform, "system", return_value="Darwin"): + _, _, spec = remote_memory._replay_definition(self.doc, paths, manifest) + self.assertIn(str(paths["driver"]), spec["ProgramArguments"]) + self.assertNotIn(str(paths["installed_driver"]), spec["ProgramArguments"]) + self.assertEqual(remote_memory._launch_agents_dir(directory.parents[1]), directory) + directory.chmod(0o777) + with self.assertRaisesRegex(remote_memory.NativeRefusal, "owner-controlled"): + remote_memory._launch_agents_dir(directory.parents[1]) + directory.chmod(0o700) + directory.rmdir() + directory.symlink_to(self.root / "mem0") + with self.assertRaisesRegex(remote_memory.NativeRefusal, "owner-controlled"): + remote_memory._launch_agents_dir(directory.parents[1]) + + def test_replay_definite_bootstrap_failure_can_retry_exact_file_timeout_stays_uncertain(self): + directory = self._replay_fixture() + self.stage() + paths = remote_memory._identity(self.doc) + manifest = remote_memory._read_manifest(self.doc, paths) + with patch.object(remote_memory, "_launch_agents_dir", return_value=directory), \ + patch.object(remote_memory.platform, "system", return_value="Darwin"): + target, body, spec = remote_memory._replay_definition(self.doc, paths, manifest) + loaded = False + attempts = 0 + def launchctl(args, *, timeout=15): + nonlocal loaded, attempts + if args[0] == "print": + return subprocess.CompletedProcess(args, 0 if loaded else 113, + self._loaded_replay(target, spec) if loaded else "") + attempts += 1 + if attempts == 1: + return subprocess.CompletedProcess(args, 5, "") + if attempts == 2: + raise remote_memory.NativeUncertain("fixture timeout") + loaded = True + return subprocess.CompletedProcess(args, 0, "") + with patch.object(remote_memory, "_launchctl", side_effect=launchctl): + with self.assertRaises(remote_memory.NativeRefusal): + remote_memory._enable_replay(self.doc, paths, manifest) + self.assertEqual(target.read_bytes(), body) + with self.assertRaises(remote_memory.NativeUncertain): + remote_memory._enable_replay(self.doc, paths, manifest) + self.assertEqual(attempts, 2) + self.assertEqual(remote_memory._enable_replay(self.doc, paths, manifest), "VERIFIED") + self.assertEqual(attempts, 3) + + def test_replay_success_without_exact_native_readback_is_uncertain(self): + directory = self._replay_fixture() + self.stage() + paths = remote_memory._identity(self.doc) + manifest = remote_memory._read_manifest(self.doc, paths) + def launchctl(args, *, timeout=15): + return subprocess.CompletedProcess(args, 113 if args[0] == "print" else 0, "") + with patch.object(remote_memory, "_launch_agents_dir", return_value=directory), \ + patch.object(remote_memory.platform, "system", return_value="Darwin"), \ + patch.object(remote_memory, "_launchctl", side_effect=launchctl): + with self.assertRaises(remote_memory.NativeUncertain): + remote_memory._enable_replay(self.doc, paths, manifest) + + def test_non_macos_replay_is_explicit_hold_before_native_enable(self): + self.stage() + with patch.object(remote_memory.platform, "system", return_value="Linux"), \ + patch.object(remote_memory, "_run", side_effect=self._native_operation) as operation: + result = remote_memory.check(self.doc) + self.assertEqual((result["state"], result["replay_scheduler_state"]), + ("REPLAY_UNSUPPORTED", "UNSUPPORTED")) + count = operation.call_count + self.assertEqual(remote_memory.enable(self.doc)["state"], "REPLAY_UNSUPPORTED") + self.assertEqual(operation.call_count, count + 1) # authenticated whoami only + def test_stage_is_private_idempotent_and_never_prints_token(self): first = self.stage() second = self.stage() From 96a388c7bedc74362a8dd9d367194c422c2ff02d Mon Sep 17 00:00:00 2001 From: CryptoJym Date: Sun, 27 Sep 2026 21:13:26 -0600 Subject: [PATCH 5/5] Verify loaded replay output paths and isolate Python imports --- RELEASE-INVENTORY.json | 20 +++++++++--------- docs/BLUEPRINT.md | 2 +- installer/remote_memory.py | 7 +++++-- installer/test_remote_memory.py | 28 ++++++++++++++++++++++++- memory/tests/test_borg_client_config.py | 17 +++++++++++++++ 5 files changed, 60 insertions(+), 14 deletions(-) diff --git a/RELEASE-INVENTORY.json b/RELEASE-INVENTORY.json index 08907c3..4a1d51c 100644 --- a/RELEASE-INVENTORY.json +++ b/RELEASE-INVENTORY.json @@ -808,9 +808,9 @@ "sha256": "e59dbdbab95ab3749451d539f8bca3462af6bb79ff6924fec031bc15acefbb77" }, { - "bytes": 11166, + "bytes": 11367, "path": "docs/BLUEPRINT.md", - "sha256": "453a51fc58f48571aa23013909fb13577a7bf19c4abf9aec2b7cd5a366a0d787" + "sha256": "7315efe823b1dc7a3efaffd494c630a7b94b620c15faf87d7d56d07649108e1f" }, { "bytes": 14235, @@ -1098,9 +1098,9 @@ "sha256": "ee32045bf09da38d41f85eb175e9073b04da6f1a388f983c8f3ae81b8bd16684" }, { - "bytes": 32169, + "bytes": 32347, "path": "installer/remote_memory.py", - "sha256": "5768e7ee85041ef18a9533fddd3105d10de69f2913c517470c0f93b9aff0a8a4" + "sha256": "ffa6c816ef476863523c96223a83d246b5bda0e92ec9e9cb311fecdd80df9832" }, { "bytes": 352, @@ -1173,9 +1173,9 @@ "sha256": "846c401865704f0cd8a3b62971a957ed12067059db33ea05b09103783d074dc2" }, { - "bytes": 43520, + "bytes": 45233, "path": "installer/test_remote_memory.py", - "sha256": "092706418ef908f343cc6fbfe34f2e3b0b9b2105e2af823b92be457a2a4d11aa" + "sha256": "451874a5d6147a64bbee7dd4630a89b0e3654107b648e1f62330811279ecec93" }, { "bytes": 1547, @@ -1528,9 +1528,9 @@ "sha256": "ff928f3e63f28f3fe668bd499172ad20cc8cea71a884a0ae76cf87c1f083ecec" }, { - "bytes": 5801, + "bytes": 6792, "path": "memory/tests/test_borg_client_config.py", - "sha256": "fe98472396725fdcc256b54a783de896dfa35dbb626744cb5a564ee667254a3c" + "sha256": "57d0d87bc6adf7b4f90cb19a84d7d393c95f2a363bc811f0a3cf0bfbd0714474" }, { "bytes": 5921, @@ -2023,7 +2023,7 @@ "sha256": "cd4bdb4529012e0cfcd38e059215f9ea433b8ee1fa636276b36c6515e7949e28" } ], - "inventory_sha256": "d868cbc45786a5898c353d174c415242c2554703818985a644eb12986e76173e", + "inventory_sha256": "dcf01dcb0b8747ad96d829d624ce98c9ed9a9c7adb0e5a0f42f0f0cf89237d5e", "schema": "borg-public-inventory/v1", - "total_bytes": 88145710 + "total_bytes": 88148793 } diff --git a/docs/BLUEPRINT.md b/docs/BLUEPRINT.md index 3c0d757..5b15dbf 100644 --- a/docs/BLUEPRINT.md +++ b/docs/BLUEPRINT.md @@ -46,7 +46,7 @@ borg memory-client enable --home /absolute/private/borg-home Use the actual `personal:` scope and the exact `instance_id` from this home's private installation configuration. Stage prints only the token SHA-256 digest and binding metadata. Grant that digest to the named principal on the existing Hub with the exact sorted read scopes and personal write scope, using the Hub's native grant authority; no token value or provider profile is copied. `enable` first checks `memory_whoami` against the binding, then uses the bundled configurator's native `config/read`, `hooks/list` and versioned `config/batchWrite` to install and trust the four lifecycle hooks in this home's isolated primary Codex profile. It preserves unrelated hooks and their trust; a native policy or permission refusal stays a refusal. Run `check` again after native approval if needed. The generated hooks bind `BORG_HOME` explicitly so a direct isolated Codex launch uses the same owned route. The default tools profile remains hook-free until this explicit step. -`borg doctor` reports `remote_memory_client` only for a staged sidecar. On macOS, `memory-client enable` also registers one instance-owned user LaunchAgent for bounded periodic replay of retained capture requests. It uses the reviewed driver, owned Python and private token/endpoint **file paths**, with a five-minute interval and caps of four requests and 30 seconds per pass. It does not put credential values in the job, modify the global miner roster, or restart other services. An absent job reports `NEEDS_REPLAY`/`replay_scheduler_state=MISSING`; an exact loaded job reports `VERIFIED`/`VERIFIED`. A foreign or stale job refuses without replacement, and a timed-out native registration remains `NATIVE_UNCERTAIN` until manually inspected. Unsupported platforms report `REPLAY_UNSUPPORTED`/`UNSUPPORTED`. `VERIFIED` means the scoped route authenticated, all four native hooks were trusted, and this exact scheduler is loaded; it does **not** mean a conversation was captured or recalled. `lifecycle_e2e` remains `NOT_VERIFIED` until a separate real conversation canary proves capture, recall and replay on the Hub. A missing grant, changed binding, unavailable route, or native hook refusal cannot be reported as ready. The opt-in does not turn on the local Mem0, graph or model services and does not change Inbox hooks. +`borg doctor` reports `remote_memory_client` only for a staged sidecar. On macOS, `memory-client enable` also registers one instance-owned user LaunchAgent for bounded periodic replay of retained capture requests. It uses the reviewed driver, owned Python in isolated `-I -B` mode, and private token/endpoint **file paths**, with a five-minute interval and caps of four requests and 30 seconds per pass. It preserves the driver's `MEM0_CAPTURE_SKIP` kill switch. It does not put credential values in the job, modify the global miner roster, or restart other services. An absent job reports `NEEDS_REPLAY`/`replay_scheduler_state=MISSING`; an exact loaded job reports `VERIFIED`/`VERIFIED` only when its native command, environment, `/dev/null` output paths and process priority match the owned definition. A foreign or stale job refuses without replacement, and a timed-out native registration remains `NATIVE_UNCERTAIN` until manually inspected. Unsupported platforms report `REPLAY_UNSUPPORTED`/`UNSUPPORTED`. `VERIFIED` means the scoped route authenticated, all four native hooks were trusted, and this exact scheduler is loaded; it does **not** mean a conversation was captured or recalled. `lifecycle_e2e` remains `NOT_VERIFIED` until a separate real conversation canary proves capture, recall and replay on the Hub. A missing grant, changed binding, unavailable route, or native hook refusal cannot be reported as ready. The opt-in does not turn on the local Mem0, graph or model services and does not change Inbox hooks. For an existing tools installation whose `bin/borg` predates `memory-client`, stage the complete reviewed source release under the **same** owner's `BORG_HOME/tools/releases/<40-character-reviewed-commit>`. Preserve its original release inventory and allowlist, owner-controlled regular files and directory modes; do not put a symlink or mutable checkout at that path. Run `python3 -B BORG_HOME/tools/releases//borg.py memory-client stage|check|enable --home BORG_HOME` with the same stage arguments above. The controller must verify the exact release inventory and ownership **before** executing that source; the client checks the inventory again. The bundled configurator pins its sibling hook driver in the trusted native command, and the client uses the release's sibling curl for route checks. This remote client resolves only its owned BORG home and state path; it needs no local model identity or local extraction service. The original installed driver and curl remain byte-matched to their existing app manifest and are not replaced. This source-only recovery leaves the installed app, its source manifest, runtime, services, account and Inbox state untouched. It is not an in-place app upgrade or a substitute for native conversation acceptance. diff --git a/installer/remote_memory.py b/installer/remote_memory.py index b083940..4399d90 100644 --- a/installer/remote_memory.py +++ b/installer/remote_memory.py @@ -353,7 +353,7 @@ def _replay_definition(doc: dict, paths: dict[str, Path], manifest: dict) -> tup specification = { "Label": label, "ProgramArguments": ["/usr/bin/env", "-u", "MEM0_FLEET_TEST_CAPTURE_JSON", - "-u", "MEM0_FLEET_TEST_SEARCH_JSON", str(python), "-B", + "-u", "MEM0_FLEET_TEST_SEARCH_JSON", str(python), "-I", "-B", str(paths["driver"]), "replay"], "WorkingDirectory": str(root), "EnvironmentVariables": environment, @@ -433,7 +433,10 @@ def _replay_state(doc: dict, paths: dict[str, Path], manifest: dict) -> str: "path": str(target), "type": "LaunchAgent", "program": specification["ProgramArguments"][0], "working directory": specification["WorkingDirectory"], - "run interval": str(REPLAY_INTERVAL_SECONDS) + " seconds"}.items()): + "run interval": str(REPLAY_INTERVAL_SECONDS) + " seconds", + "stdout path": specification["StandardOutPath"], + "stderr path": specification["StandardErrorPath"], + "nice": str(specification["Nice"])}.items()): raise NativeRefusal("Loaded native memory replay job differs from its owned definition") if "runatload" not in (fields.get("properties") or "").split(" | "): raise NativeRefusal("Loaded native memory replay job did not retain RunAtLoad") diff --git a/installer/test_remote_memory.py b/installer/test_remote_memory.py index f172523..10967b1 100644 --- a/installer/test_remote_memory.py +++ b/installer/test_remote_memory.py @@ -165,6 +165,9 @@ def _loaded_replay(target, spec): "\tenvironment = {"] rows += [f"\t\t{key} => {value}" for key, value in spec["EnvironmentVariables"].items()] rows += ["\t}", f"\trun interval = {spec['StartInterval']} seconds", + f"\tstdout path = {spec['StandardOutPath']}", + f"\tstderr path = {spec['StandardErrorPath']}", + f"\tnice = {spec['Nice']}", "\tproperties = runatload | low priority i/o", "}"] return "\n".join(rows) + "\n" @@ -182,7 +185,8 @@ def test_periodic_replay_registers_once_and_never_claims_e2e(self): self.assertNotIn(token, body.decode()) self.assertEqual(spec["ProgramArguments"], ["/usr/bin/env", "-u", "MEM0_FLEET_TEST_CAPTURE_JSON", "-u", "MEM0_FLEET_TEST_SEARCH_JSON", - str(self.root / "mem0/venv/bin/python"), "-B", str(paths["driver"]), "replay"]) + str(self.root / "mem0/venv/bin/python"), "-I", "-B", str(paths["driver"]), "replay"]) + self.assertNotIn("MEM0_CAPTURE_SKIP", spec["ProgramArguments"]) self.assertEqual(spec["EnvironmentVariables"]["MEM0_MACHINE"], self.args["machine"]) self.assertEqual(spec["EnvironmentVariables"]["MEM0_FLEET_ENDPOINT"], "") self.assertEqual(spec["StartInterval"], 300) @@ -252,6 +256,28 @@ def test_replay_rejects_foreign_stale_or_symlinked_job_and_loaded_collision(self with self.assertRaisesRegex(remote_memory.NativeRefusal, "environment differs"): remote_memory._replay_state(self.doc, paths, manifest) + def test_replay_rejects_stale_loaded_output_paths_and_nice_without_bootstrap(self): + directory = self._replay_fixture() + self.stage() + paths = remote_memory._identity(self.doc) + manifest = remote_memory._read_manifest(self.doc, paths) + with patch.object(remote_memory, "_launch_agents_dir", return_value=directory), \ + patch.object(remote_memory.platform, "system", return_value="Darwin"): + target, body, spec = remote_memory._replay_definition(self.doc, paths, manifest) + target.write_bytes(body); target.chmod(0o600) + for changed in ({"StandardOutPath": "/foreign/capture.log"}, + {"StandardErrorPath": "/foreign/error.log"}, + {"Nice": 0}): + with self.subTest(changed=changed): + loaded_spec={**spec, **changed} + loaded=self._loaded_replay(target, loaded_spec) + with patch.object(remote_memory, "_launchctl", return_value=subprocess.CompletedProcess([],0,loaded)) as native: + with self.assertRaisesRegex(remote_memory.NativeRefusal, "differs"): + remote_memory._enable_replay(self.doc, paths, manifest) + native.assert_called_once() + self.assertEqual(native.call_args.args[0][0], "print") + self.assertEqual(target.read_bytes(), body) + def test_replay_release_driver_is_pinned_and_launch_agent_directory_is_owned(self): directory = self._replay_fixture() self.stage() diff --git a/memory/tests/test_borg_client_config.py b/memory/tests/test_borg_client_config.py index 62a3ef9..8fbb7b3 100644 --- a/memory/tests/test_borg_client_config.py +++ b/memory/tests/test_borg_client_config.py @@ -55,6 +55,23 @@ def test_real_driver_and_curl_load_without_local_model_ids(self): self.assertEqual(who.returncode, 0, who.stderr) self.assertEqual(json.loads(who.stdout)["principal"], "bounded-test") + def test_isolated_replay_ignores_pythonpath_but_keeps_capture_skip(self): + poison = self.home.parent / "unreviewed-pythonpath" + poison.mkdir(mode=0o700) + (poison / "json.py").write_text("raise RuntimeError('unreviewed import')\n") + environment = {**self.env, "PYTHONPATH": str(poison), "MEM0_CAPTURE_SKIP": "1"} + replay = subprocess.run( + [sys.executable, "-I", "-B", str(BIN / "mem0-fleet-hook"), "replay"], + input="{}", env=environment, capture_output=True, text=True, timeout=20, check=False, + ) + self.assertEqual(replay.returncode, 0, replay.stderr) + self.assertEqual(replay.stdout, "") + inherited = subprocess.run( + [sys.executable, "-I", "-B", "-c", "import os; print(os.getenv('MEM0_CAPTURE_SKIP'))"], + env=environment, capture_output=True, text=True, timeout=20, check=False, + ) + self.assertEqual((inherited.returncode, inherited.stdout.strip()), (0, "1")) + def test_native_hook_and_curl_leave_reviewed_release_without_bytecode(self): """Direct shebang entry points do not inherit the installer's -B flag.""" source_bin = self.home.parent / "reviewed-release" / "memory" / "bin"