#!/usr/bin/env python3 """Install generic file-based Cortex. Preview by default; no network/runtime/sudo.""" import argparse import base64 import datetime import hashlib import json import os from pathlib import Path, PurePosixPath import stat import sys import uuid import zipfile VERSION = "1.0.0" UPSTREAM_SHA = "16738466f142d1acb82f613b33468e4deabf0c9c67a528fa8b9b6e0b83d1e57e" PREFIX = "commander-cortex-0.1.0/" VERSIONS = {"home": "commander-cortex-0.1.0", "project": "0.2.0"} def sha(body): return hashlib.sha256(body).hexdigest() def guard(path): if not path.is_absolute() or ".." in path.parts or any(ord(c) < 32 or c in "`\x7f" for c in str(path)): raise ValueError("Use an absolute, normalized target path") for item in (path, *path.parents): if item.is_symlink(): raise ValueError(f"Refusing symlinked destination: {item}") existing = next(item for item in (path, *path.parents) if item.exists()) if existing.stat().st_uid != os.getuid(): raise ValueError(f"Destination is not owned by the current user: {existing}") def payload(scope, root): package = Path(__file__).resolve().parent archive = package / "framework.zip" if sha(archive.read_bytes()) != UPSTREAM_SHA: raise ValueError("Frozen framework archive hash mismatch") with zipfile.ZipFile(archive) as source: names = source.namelist() if len(names) != len(set(names)): raise ValueError("Duplicate framework members") for info in source.infolist(): path = PurePosixPath(info.filename) if not info.filename.startswith(PREFIX) or path.is_absolute() or ".." in path.parts or stat.S_ISLNK(info.external_attr >> 16): raise ValueError("Unsafe framework member") manifest = json.loads(source.read(PREFIX + "SOURCE-MANIFEST.json")) expected = {PREFIX + item["path"] for item in manifest["files"]} if set(names) != expected | {PREFIX + "SOURCE-MANIFEST.json"}: raise ValueError("Framework manifest file list mismatch") for item in manifest["files"]: body = source.read(PREFIX + item["path"]) if sha(body) != item["sha256"] or len(body) != item["bytes"]: raise ValueError("Framework source hash mismatch") files = {} for name in names: relative = name[len(PREFIX):] if scope == "project" and relative.startswith("project-cortex/template/"): files[relative[len("project-cortex/template/"):]] = source.read(name) elif scope == "home" and relative.startswith("commander-template/"): files[relative[len("commander-template/"):]] = source.read(name).decode().replace("{{COMMANDER_ROOT}}", str(root)).encode() elif scope == "home" and relative.startswith("project-cortex/"): files[".ai/tools/" + relative] = source.read(name) if scope == "project": files["AGENTS.md"] = b"\n" + files["AGENTS.md"] + b"\n" return files def directories(files, scope): dirs = {str(parent) for name in files for parent in PurePosixPath(name).parents if str(parent) != "."} if scope == "home": dirs.add(".ai/projects") return dirs def check_target(root, scope): guard(root) if not root.is_dir(): raise ValueError("Target directory must already exist and belong to the intended user") ai = root / ".ai" guard(ai) if ai.exists(): if not ai.is_dir() or ai.stat().st_mode & 0o077: raise ValueError("Existing .ai must be a private directory; inspect it before any permission change") version = ai / "CORTEX_VERSION" guard(version) if any(ai.iterdir()) and (not version.is_file() or version.read_text().strip() != VERSIONS[scope]): raise ValueError("Existing Cortex scope/version is unknown or different; no migration/overwrite is authorized") def state_directory(): state = Path(os.environ.get("XDG_STATE_HOME", str(Path.home() / ".local/state"))) / "ecosystem-field-guide/cortex-installs" guard(state) return state def plan(root, scope, merge_agents=False): check_target(root, scope) files = payload(scope, root) actions = [] marker = b"commander-cortex" if scope == "home" else b"project-cortex" start, end = b"", b"" for name, body in sorted(files.items()): path = root / name guard(path) if path.exists() and not path.is_file(): raise ValueError(f"Expected a regular file: {path}") previous = path.read_bytes() if path.exists() else None action = "create" if previous is None else "keep" if name == "AGENTS.md" and previous is not None and merge_agents: if previous.count(start) != previous.count(end) or previous.count(start) > 1: raise ValueError("Existing AGENTS.md has incomplete/duplicate managed markers") if start in previous and previous.index(start) > previous.index(end): raise ValueError("Existing AGENTS.md markers are reversed") routing = all(text in previous for text in [b".ai/START.md", b".ai/NOW.md", b".ai/protocols/operate.md"]) if start not in previous and not routing: previous.decode("utf-8") body = previous + b"\n\n" + body action = "merge" actions.append({"path": name, "action": action, "body": body, "before": previous}) for name in directories(files, scope): path = root / name guard(path) if path.exists() and not path.is_dir(): raise ValueError(f"Expected directory: {path}") return files, actions def apply_plan(root, scope, files, actions): changed = [item for item in actions if item["action"] != "keep"] if not changed: print(json.dumps({"status": "already-present", "changed": False, "existing_memory_preserved": True})) return os.umask(0o077) state = state_directory() state.mkdir(mode=0o700, parents=True, exist_ok=True) stamp = datetime.datetime.now(datetime.timezone.utc).strftime("%Y%m%dT%H%M%SZ") + "-" + uuid.uuid4().hex[:8] receipt_path = state / (stamp + ".json") receipt = {"recipe": "cortex-install", "version": VERSION, "uid": os.getuid(), "root": str(root), "scope": scope, "upstream_sha256": UPSTREAM_SHA, "created": {}, "modified": {}, "created_directories": []} written = [] try: for name in sorted(directories(files, scope), key=lambda name: (len(Path(name).parts), name)): path = root / name if not path.exists(): path.mkdir(mode=0o700) receipt["created_directories"].append(name) for item in changed: path = root / item["path"] guard(path) if item["action"] == "create": with path.open("xb") as handle: handle.write(item["body"]) receipt["created"][item["path"]] = sha(item["body"]) else: with path.open("r+b") as handle: if handle.read() != item["before"]: raise ValueError("Concurrent AGENTS.md edit; preserving it") receipt["modified"][item["path"]] = {"before_base64": base64.b64encode(item["before"]).decode(), "after_sha256": sha(item["body"])} handle.seek(0) handle.write(item["body"]) handle.truncate() written.append(item) receipt_path.write_text(json.dumps(receipt, indent=2) + "\n") except Exception: # Undo only exact bytes written by this incomplete attempt, not user edits. for item in reversed(written): path = root / item["path"] if path.is_file() and not path.is_symlink() and path.read_bytes() == item["body"]: if item["before"] is None: path.unlink() else: path.write_bytes(item["before"]) for name in reversed(receipt["created_directories"]): try: (root / name).rmdir() except OSError: pass raise print("Receipt: " + str(receipt_path)) print("Installed file-based context only. No runtime, model, shell, service, Git or network changes.") def rollback(receipt_path, apply=False): guard(receipt_path) data = json.loads(receipt_path.read_text()) if data.get("recipe") != "cortex-install" or data.get("uid") != os.getuid() or data.get("scope") not in VERSIONS: raise ValueError("Receipt does not belong to this recipe/current user") root, scope = Path(data["root"]), data["scope"] guard(root) files = payload(scope, root) if not set(data["created"]).issubset(files) or not set(data["modified"]).issubset({"AGENTS.md"}): raise ValueError("Unexpected receipt file list") dirs = directories(files, scope) if not set(data["created_directories"]).issubset(dirs): raise ValueError("Unexpected receipt directory list") # Refuse rollback if new notes/projects appeared: do not strand learned memory. for base, children, names in os.walk(root / ".ai", followlinks=False): for name in children + names: path = Path(base) / name relative = str(path.relative_to(root)) guard(path) if relative not in files and relative not in dirs: raise ValueError("New/untracked Cortex material exists; preserve and reconcile it before rollback") for name, expected in {**data["created"], **{name: info["after_sha256"] for name, info in data["modified"].items()}}.items(): path = root / name guard(path) if not path.is_file() or sha(path.read_bytes()) != expected: raise ValueError(f"Refusing rollback of modified/missing file: {name}") print(json.dumps({"action": "rollback", "apply": apply, "root": str(root), "remove_created_files": len(data["created"]), "restore_AGENTS": bool(data["modified"])}, indent=2)) if apply: for name, info in data["modified"].items(): (root / name).write_bytes(base64.b64decode(info["before_base64"], validate=True)) for name in data["created"]: (root / name).unlink() for name in sorted(data["created_directories"], key=lambda name: len(Path(name).parts), reverse=True): try: (root / name).rmdir() except OSError: pass receipt_path.with_suffix(".rolled-back.json").write_text(json.dumps({"completed": True}) + "\n") def main(): parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--root", type=Path, help="Existing absolute directory owned by the intended Cortex user") parser.add_argument("--scope", choices=list(VERSIONS), help="home/ecosystem coordination or one project") parser.add_argument("--merge-agents", action="store_true", help="Explicit opt-in to append missing Cortex routing to existing AGENTS.md") parser.add_argument("--apply", action="store_true", help="Write the previewed changes; otherwise no writes") parser.add_argument("--rollback", type=Path, help="Private install receipt to preview/roll back") args = parser.parse_args() if os.name != "posix" or os.geteuid() == 0: raise ValueError("Use a non-root POSIX account: the intended human/service user, not management sudo") if args.rollback: rollback(args.rollback.expanduser(), args.apply) return if not args.root or not args.scope: parser.error("--root and --scope are required; discover and obtain the owner's intended account/scope first") root = args.root.expanduser() files, actions = plan(root, args.scope, args.merge_agents) state_directory() # Validate receipt ownership/path even for a preview. print(json.dumps({"root": str(root), "scope": args.scope, "apply": args.apply, "merge_agents": args.merge_agents, "files": [{"path": item["path"], "action": item["action"]} for item in actions]}, indent=2)) if args.apply: apply_plan(root, args.scope, files, actions) if __name__ == "__main__": try: main() except (ValueError, OSError, UnicodeError, zipfile.BadZipFile) as error: print("Stopped: " + str(error), file=sys.stderr) sys.exit(1)