diff --git a/src/aipass/aipass/.seedgo/bypass.json b/src/aipass/aipass/.seedgo/bypass.json index 26d8d28f..99cb67c7 100644 --- a/src/aipass/aipass/.seedgo/bypass.json +++ b/src/aipass/aipass/.seedgo/bypass.json @@ -375,6 +375,41 @@ "file": "shared/json_ops.py", "standard": "unused_function", "reason": "backup_json() is consumed by @spawn (cross-branch caller). Appears unused in @aipass-only scan but is a shared API." + }, + { + "file": "apps/modules/trust.py", + "standard": "encapsulation", + "reason": "Imports frozen trust_registry interface (enroll/revoke/is_trusted/read_registry) from @hooks by DPLAN-0244 design. Cross-branch import required — the registry module lives in hooks, consumers live in aipass." + }, + { + "file": "apps/modules/trust.py", + "standard": "json_structure", + "reason": "No JSON file operations — trust.py is a thin CLI wrapper that delegates all JSON I/O to the trust_registry module in @hooks. No json_handler needed." + }, + { + "file": "apps/modules/trust.py", + "standard": "introspection", + "reason": "aipass is binary-invoked: bare 'aipass trust' shows registry table; introspection via --info" + }, + { + "file": "apps/handlers/init/bootstrap.py", + "standard": "encapsulation", + "reason": "Imports enroll() from @hooks trust_registry (DPLAN-0244 frozen interface). Cross-branch import required — init must enroll projects in the trust registry after writing hooks.json." + }, + { + "file": "apps/handlers/init/bootstrap.py", + "standard": "handlers", + "reason": "Imports enroll() from @hooks trust_registry by DPLAN-0244 design. Cross-handler import required — bootstrap auto-enrolls projects after hooks.json creation/merge." + }, + { + "file": "tests/test_trust.py", + "standard": "architecture", + "reason": "Test file lives in tests/ by convention — not in apps/. Standard 3-layer structure applies to production code only." + }, + { + "file": "tests/test_trust.py", + "standard": "encapsulation", + "reason": "Tests import trust_registry directly to verify enrollment/revocation in isolation with monkeypatched REGISTRY_PATH." } ] } diff --git a/src/aipass/aipass/README.md b/src/aipass/aipass/README.md index f4b41e15..e54374e8 100644 --- a/src/aipass/aipass/README.md +++ b/src/aipass/aipass/README.md @@ -22,7 +22,8 @@ aipass/ │ │ ├── help_chat.py # README-backed Q&A (reads via readme_map handler) │ │ ├── init_flow.py # 10-stage guided setup │ │ ├── install.py # aipass install — one-command bootstrap (clone + setup + init) -│ │ └── profile.py # User profile read/write +│ │ ├── profile.py # User profile read/write +│ │ └── trust.py # Trust registry — aipass trust / aipass revoke │ ├── handlers/ │ │ ├── cross_os/ # Cross-OS pre-flight: gap_registry, preflight, run_record │ │ ├── handoff_platform/ # Platform-specific handoff detection @@ -56,6 +57,8 @@ aipass/ | `aipass init` | 10-stage guided setup (resumable) | | `aipass install` | One-command bootstrap — clone + setup.sh + hooks, then hand off to init (`--no-init`/`--with-init`/`--path`/`--here`) | | `aipass profile` | Show/edit user profile | +| `aipass trust [path]` | Show enrolled projects or enroll a project in the trust registry | +| `aipass revoke ` | Remove a project from the trust registry | | `aipass --version` | Version | ## Integration Points diff --git a/src/aipass/aipass/apps/modules/trust.py b/src/aipass/aipass/apps/modules/trust.py index 2adee3da..cf4ccdd8 100644 --- a/src/aipass/aipass/apps/modules/trust.py +++ b/src/aipass/aipass/apps/modules/trust.py @@ -19,27 +19,61 @@ from __future__ import annotations from pathlib import Path from aipass.cli.apps.modules import console, error, success -from aipass.hooks.apps.handlers.config.trust_registry import enroll, revoke +from aipass.hooks.apps.handlers.config.trust_registry import ( + enroll, + read_registry, + revoke, +) from aipass.prax import logger COMMAND = "trust" _COMMAND_REVOKE = "revoke" -def _print_help() -> None: +def print_introspection() -> None: + """Display the current trusted-project registry.""" + from rich.table import Table + + registry = read_registry() + projects = registry.get("projects", {}) + + console.print() + console.print("[bold cyan]aipass trust[/bold cyan] — trusted-project registry") + console.print() + + if not projects: + console.print("[dim]No projects enrolled.[/dim]") + else: + table = Table(show_header=True, header_style="bold yellow") + table.add_column("Project", style="cyan") + table.add_column("Hash", style="dim", max_width=24) + table.add_column("Enrolled") + for path, entry in projects.items(): + short_hash = entry.get("config_hash", "")[:18] + "..." + table.add_row(path, short_hash, entry.get("enrolled", "")) + console.print(table) + + console.print() + console.print("[dim]Use 'aipass trust ' to enroll or 'aipass revoke ' to remove.[/dim]") + console.print() + + +def print_help() -> None: + """Print usage help for the trust/revoke commands.""" console.print() console.print("[bold cyan]aipass trust / revoke[/bold cyan] — trusted-project registry") console.print() console.print("[yellow]USAGE:[/yellow]") - console.print(" [green]aipass trust [/green] [dim]# Enroll a project (requires .aipass/hooks.json)[/dim]") - console.print(" [green]aipass revoke [/green] [dim]# Remove a project from the registry[/dim]") + console.print(" [green]aipass trust[/green] [dim]# Show enrolled projects[/dim]") + console.print( + " [green]aipass trust [/green] [dim]# Enroll a project (requires .aipass/hooks.json)[/dim]" + ) + console.print(" [green]aipass revoke [/green] [dim]# Remove a project from the registry[/dim]") console.print() -def _handle_trust(args: list[str]) -> bool: - if not args or args[0] in ("--help", "-h"): - _print_help() - return True +def _do_trust(args: list[str]) -> bool: + """Execute the trust enrollment for a given path.""" target = Path(args[0]).resolve() if not target.is_dir(): error(f"Not a directory: {target}") @@ -56,10 +90,8 @@ def _handle_trust(args: list[str]) -> bool: return True -def _handle_revoke(args: list[str]) -> bool: - if not args or args[0] in ("--help", "-h"): - _print_help() - return True +def _do_revoke(args: list[str]) -> bool: + """Execute the revocation for a given path.""" target = Path(args[0]).resolve() if revoke(str(target)): success(f"Revoked {target}") @@ -72,7 +104,19 @@ def _handle_revoke(args: list[str]) -> bool: def handle_command(command: str, args: list[str]) -> bool: """Route trust/revoke subcommands. Returns True if handled.""" if command == COMMAND: - return _handle_trust(args) + if not args: + print_introspection() + return True + if args[0] in ("--help", "-h", "help"): + print_help() + return True + if args[0] == "--info": + print_introspection() + return True + return _do_trust(args) if command == _COMMAND_REVOKE: - return _handle_revoke(args) + if not args or args[0] in ("--help", "-h", "help"): + print_help() + return True + return _do_revoke(args) return False diff --git a/src/aipass/hooks/.seedgo/bypass.json b/src/aipass/hooks/.seedgo/bypass.json index 514cabc1..db381bd7 100644 --- a/src/aipass/hooks/.seedgo/bypass.json +++ b/src/aipass/hooks/.seedgo/bypass.json @@ -406,6 +406,11 @@ "standard": "json_structure", "reason": "Sound handler \u2014 no JSON operations, plays WAV files." }, + { + "file": "apps/handlers/config/trust_registry.py", + "standard": "unused_function", + "reason": "enroll() and revoke() are the public API consumed CROSS-BRANCH by @aipass CLI (init/trust/revoke commands, DPLAN-0244 phase 2). seedgo's intra-branch static analysis cannot see cross-branch callers. read_registry() also exported for @aipass CLI use." + }, { "file": "apps/handlers/config/loader.py", "standard": "json_structure", diff --git a/src/aipass/hooks/apps/handlers/config/trust_registry.py b/src/aipass/hooks/apps/handlers/config/trust_registry.py index 3e0f4f6b..6258d086 100644 --- a/src/aipass/hooks/apps/handlers/config/trust_registry.py +++ b/src/aipass/hooks/apps/handlers/config/trust_registry.py @@ -21,6 +21,7 @@ import json import os from pathlib import Path +from aipass.hooks.apps.handlers.json import json_handler from aipass.prax.apps.modules.logger import system_logger as logger REGISTRY_PATH = Path.home() / ".aipass" / "trusted_projects.json" @@ -37,7 +38,7 @@ def read_registry() -> dict: if not REGISTRY_PATH.exists(): return {"version": 1, "projects": {}} try: - data = json.loads(REGISTRY_PATH.read_text(encoding="utf-8")) + data = json_handler.read_json_file(REGISTRY_PATH) if not isinstance(data.get("projects"), dict): return {"version": 1, "projects": {}} return data @@ -49,10 +50,7 @@ def read_registry() -> dict: def _write_registry(registry: dict) -> None: """Write the registry to disk, creating parent dirs if needed.""" REGISTRY_PATH.parent.mkdir(parents=True, exist_ok=True) - REGISTRY_PATH.write_text( - json.dumps(registry, indent=2) + "\n", - encoding="utf-8", - ) + json_handler.write_json_file(REGISTRY_PATH, registry) def enroll(project_dir: str) -> bool: @@ -70,6 +68,7 @@ def enroll(project_dir: str) -> bool: "config_path": str(config_path), } _write_registry(registry) + json_handler.log_operation("enroll", {"project": str(project_path)}, module_name="trust_registry") logger.info("[HOOKS] enrolled %s (hash=%s)", project_path, config_hash) return True @@ -82,6 +81,7 @@ def revoke(project_dir: str) -> bool: return False del registry["projects"][project_path] _write_registry(registry) + json_handler.log_operation("revoke", {"project": project_path}, module_name="trust_registry") logger.info("[HOOKS] revoked %s", project_path) return True @@ -126,6 +126,7 @@ def bootstrap() -> bool: "config_path": str(config_path), } _write_registry(registry) + json_handler.log_operation("bootstrap", {"aipass_home": str(aipass_path)}, module_name="trust_registry") logger.info("[HOOKS] registry bootstrapped, enrolled AIPass install: %s", aipass_path) return True