← Files stark AI DeveloperARCHIVED FILE

skills/jev-capability-advisor/scripts/jev_hooks.py

40.8 KB · Oct 4, 2026 · 12:30 UTC

↓ Download file

#!/usr/bin/env python3
"""Opt-in Jev hook registration. No provider access or capability discovery.

install/uninstall modify only the selected hook config and private ownership state.
status and --dry-run never write. render prints a fragment without accessing
user configuration or ownership state. No third-party dependencies.
"""
import argparse
import base64
from contextlib import contextmanager
from copy import copy
from dataclasses import dataclass
import hashlib
import json
import os
from pathlib import Path
import shlex
import shutil
import re
import stat
import subprocess
import sys
import tempfile
import time

MARKER = 'jev-capability-advisor hook v1'
STATE_VERSION = 1
MAX_WINDOWS_COMMAND_CHARS = 8000
MAX_FILE_BYTES = 16 * 1024 * 1024
STATE_IGNORE = b'*\n'
EMITTER = """# jev-capability-advisor hook v1
import base64, sys
try:
    payload = base64.b64decode(sys.argv[1], validate=True)
    while sys.stdin.buffer.read(65536):
        pass
    sys.stdout.buffer.write(payload + b'\\n')
    sys.stdout.buffer.flush()
except BaseException:
    sys.exit(1)
"""
DISCLOSURE = ('Opt-in: for new actionable tasks, the agent may send a minimal task '
              'summary and bounded eligible capability metadata to TypeSafe using '
              'existing credentials. The hook itself performs no network access. '
              'Host restrictions and native fallback remain in effect.')

CONSENT_TERMS = ('For new actionable tasks in this registration scope, send only a '
                 'minimal non-sensitive task summary and public eligible capability '
                 'metadata to https://api.typesafe.ai/v1/systemone for capability advice. '
                 'Exclude secrets, private paths, transcripts, tool results and unrelated '
                 'private content. Native approvals and user opt-outs remain binding.')
CONSENT_NOTICE = ('User opt-in recorded for this registration: send minimal non-sensitive '
                  'task summaries and public capability metadata to '
                  'https://api.typesafe.ai/v1/systemone for advice. Verify current '
                  'advice_consent via status. Native approvals still apply.')


class HookError(Exception):
    """An actionable failure which must not overwrite user configuration."""


class Parser(argparse.ArgumentParser):
    def error(self, message):
        raise HookError('invalid_arguments: ' + message)


@dataclass(frozen=True)
class Snapshot:
    data: bytes | None
    identity: tuple | None


def _identity(value, *, descriptor=False):
    # Windows path stat aliases ctime to creation time, while Python 3.14
    # fstat can report change time. Compare the same timestamp on both APIs.
    timestamp = (getattr(value, 'st_birthtime_ns', value.st_ctime_ns)
                 if os.name == 'nt' and not descriptor else value.st_ctime_ns)
    return (value.st_dev, value.st_ino, value.st_size,
            value.st_mtime_ns, timestamp, value.st_mode)


def _same_path_metadata(left, right):
    if sys.platform == 'win32':
        # Windows can report different file IDs and change times for lstat and
        # fstat on the same file. Keep those full identity checks between fstats.
        return (left.st_size, left.st_mtime_ns, left.st_mode) == (
            right.st_size, right.st_mtime_ns, right.st_mode)
    return _identity(left) == _identity(right)


def _same_path_identity(left, right):
    if sys.platform == 'win32':
        # st_ino is a Windows file index when available. If either identifier
        # is unavailable, matching content metadata cannot prove path stability.
        left_identity = (left.st_dev, left.st_ino)
        right_identity = (right.st_dev, right.st_ino)
        return all(left_identity) and left_identity == right_identity
    return _identity(left) == _identity(right)


def safe_path(path):
    """Reject symlinked write paths, including existing ancestor components."""
    path = Path(os.path.abspath(path))
    for part in (*reversed(path.parents), path):
        try:
            metadata = part.lstat()
        except FileNotFoundError:
            continue
        # Junctions redirect Windows paths too, but is_symlink() is false for
        # them on supported Python versions before Path.is_junction existed.
        redirect_tags = (getattr(stat, 'IO_REPARSE_TAG_SYMLINK', 0xA000000C),
                         getattr(stat, 'IO_REPARSE_TAG_MOUNT_POINT', 0xA0000003))
        if (stat.S_ISLNK(metadata.st_mode)
                or getattr(metadata, 'st_reparse_tag', 0) in redirect_tags):
            raise HookError('symlink_path: ' + str(part))
    return path


def read_snapshot(path):
    path = safe_path(path)
    try:
        before = path.lstat()
    except FileNotFoundError:
        return Snapshot(None, None)
    if not stat.S_ISREG(before.st_mode):
        raise HookError('not_regular_file: ' + str(path))
    if before.st_size > MAX_FILE_BYTES:
        raise HookError('file_too_large: ' + str(path))
    flags = os.O_RDONLY | getattr(os, 'O_NOFOLLOW', 0) | getattr(os, 'O_BINARY', 0)
    with os.fdopen(os.open(path, flags), 'rb') as stream:
        opened = os.fstat(stream.fileno())
        data = stream.read(MAX_FILE_BYTES + 1)
        after = os.fstat(stream.fileno())
    path_after = path.lstat()
    if (len(data) > MAX_FILE_BYTES or not _same_path_metadata(before, opened)
            or _identity(opened, descriptor=True) != _identity(after, descriptor=True)
            or not _same_path_identity(before, path_after)
            or not _same_path_metadata(after, path_after)):
        raise HookError('concurrent_modification: ' + str(path))
    return Snapshot(data, _identity(after, descriptor=True))



def atomic_write(path, data, expected):
    """Stage privately; refuse detected edits before atomic replacement.

    Serializes our managers via the enclosing lock. Filesystems do not offer a
    portable compare-and-swap against an unrelated writer after the last check.
    """
    path = safe_path(path)
    if read_snapshot(path) != expected:
        raise HookError('concurrent_modification: ' + str(path))
    path.parent.mkdir(parents=True, exist_ok=True, mode=0o700)
    safe_path(path)
    descriptor, name = tempfile.mkstemp(prefix='.' + path.name + '-', dir=path.parent)
    temporary = Path(name)
    try:
        with os.fdopen(descriptor, 'wb') as stream:
            stream.write(data)
            if expected.identity is not None and os.name != 'nt':
                os.fchmod(stream.fileno(), stat.S_IMODE(expected.identity[-1]))
            stream.flush()
            os.fsync(stream.fileno())
        if read_snapshot(path) != expected:
            raise HookError('concurrent_modification: ' + str(path))
        os.replace(temporary, path)
    finally:
        temporary.unlink(missing_ok=True)


def _pairs(pairs):
    result = {}
    for key, value in pairs:
        if key in result:
            raise HookError('duplicate_json_key: ' + key)
        result[key] = value
    return result


def parse_object(snapshot, label):
    if snapshot.data is None:
        return {}
    try:
        result = json.loads(snapshot.data.decode('utf-8-sig'), object_pairs_hook=_pairs,
                            parse_constant=lambda _: (_ for _ in ()).throw(
                                HookError('invalid_json_constant')))
    except (ValueError, UnicodeError) as error:
        raise HookError('invalid_json: ' + label) from error
    if not isinstance(result, dict):
        raise HookError('expected_json_object: ' + label)
    return result


def encode(value):
    return (json.dumps(value, ensure_ascii=False, indent=2) + '\n').encode('utf-8')


def digest(data):
    return hashlib.sha256(data).hexdigest() if data is not None else None


def load_guidance():
    return (Path(__file__).resolve().parent.parent / 'assets' /
            'hook-guidance.txt').read_text(encoding='utf-8').strip()


def registration_guidance(host, scope, project_root, consent=None):
    """Bind local status lookup to this registration, never provider metadata."""
    binding = {'host': host, 'scope': scope,
               'project_root': str(safe_path(project_root)) if scope == 'project' else None}
    notice = '\n' + CONSENT_NOTICE if consent else ''
    return (load_guidance() + notice + '\nLocal registration (never send to provider): ' +
            json.dumps(binding, ensure_ascii=False, separators=(',', ':')))


def new_consent():
    return {'version': 1, 'disclosure_sha256': digest(CONSENT_TERMS.encode('utf-8')),
            'basis': 'explicit_install_option', 'accepted_at': int(time.time())}


def active_consent(receipt, owned):
    """A local acceptance record is neither host trust nor call authorization."""
    value = receipt.get('advice_consent')
    if (not owned or not isinstance(value, dict)
            or set(value) != {'version', 'disclosure_sha256', 'basis', 'accepted_at'}
            or type(value.get('version')) is not int or value['version'] != 1
            or value.get('disclosure_sha256') != digest(CONSENT_TERMS.encode('utf-8'))
            or value.get('basis') != 'explicit_install_option'
            or type(value.get('accepted_at')) is not int or value['accepted_at'] <= 0
            or not isinstance(receipt.get('guidance'), str)
            or CONSENT_NOTICE not in receipt['guidance']
            or not isinstance(receipt.get('interpreter'), str)):
        return None
    try:
        expected = build_registration(receipt['host'], receipt['interpreter'],
                                      guidance=receipt['guidance'])
    except HookError:
        return None
    return value if owned == expected else None


def consent_status(consent, preview=False):
    return {'status': ('would_record' if preview else 'recorded') if consent else 'not_recorded',
            'terms': CONSENT_TERMS, 'record': consent,
            'native_approval': 'still_required_when_host_requires_it'}


def render_policy(host, policy):
    """Build a static fragment; never inspect installation or user state."""
    if host != 'codex' or policy != 'repository-adopted':
        raise HookError('unsupported_policy')
    guidance = (Path(__file__).resolve().parent.parent / 'assets' /
                'repository-hook-guidance.txt').read_text(encoding='utf-8').strip()
    return {'hooks': {'UserPromptSubmit': [build_registration(host, guidance=guidance)]}}


def _ps_quote(value):
    return "'" + value.replace("'", "''") + "'"


def build_registration(host, python_executable=None, platform=None, guidance=None):
    if host not in ('codex', 'claude-code'):
        raise HookError('unsupported_host')
    platform = platform or sys.platform
    python_executable = str(python_executable or Path(sys.executable).resolve())
    guidance = load_guidance() if guidance is None else guidance
    payload = json.dumps({'hookSpecificOutput': {
        'hookEventName': 'UserPromptSubmit', 'additionalContext': guidance}},
        ensure_ascii=True, separators=(',', ':'))
    # Base64 is data, not executable source. In particular it contains no double
    # quotes for Windows PowerShell 5.1's legacy native argument conversion.
    encoded = base64.b64encode(payload.encode('utf-8')).decode('ascii')
    argv = [python_executable, '-I', '-B', '-c', EMITTER, encoded]
    handler = {'type': 'command', 'timeout': 5}
    if host == 'claude-code':
        handler.update(command=argv[0], args=argv[1:])
    else:
        handler['command'] = shlex.join(argv) + ' || exit 1'
        if platform == 'win32':
            script = ("$ErrorActionPreference='Stop'; try { & " +
                      ' '.join(_ps_quote(arg) for arg in argv) +
                      "; if ($LASTEXITCODE -eq 0) { exit 0 }; exit 1 } catch { exit 1 }")
            command = ('powershell.exe -NoLogo -NoProfile -NonInteractive '
                       '-OutputFormat Text -EncodedCommand ' +
                       base64.b64encode(script.encode('utf-16le')).decode('ascii'))
            if len(command) > MAX_WINDOWS_COMMAND_CHARS:
                raise HookError('windows_command_too_long')
            handler['commandWindows'] = command
    return {'hooks': [handler]}


def verify_home_state_repository(home, state):
    """Allow only untracked private state in the actual HOME worktree."""
    environment = {key: value for key, value in os.environ.items()
                   if not key.startswith('GIT_')}
    environment['GIT_OPTIONAL_LOCKS'] = '0'

    def git(*arguments):
        try:
            result = subprocess.run(
                ['git', '--literal-pathspecs', '-C', str(home), *arguments],
                stdin=subprocess.DEVNULL, capture_output=True, env=environment,
                timeout=5, check=True)
        except (OSError, subprocess.SubprocessError) as error:
            raise HookError('home_state_git_check_failed') from error
        return result.stdout

    root = git('rev-parse', '--show-toplevel').rstrip(b'\r\n')
    if (not root or Path(os.fsdecode(root)) != home
            or git('rev-parse', '--is-bare-repository').strip() != b'false'):
        raise HookError('home_state_git_root_mismatch')
    if git('ls-files', '-z', '--', state.relative_to(home).as_posix()):
        raise HookError('home_state_already_tracked')


def state_exclusion(state):
    snapshot = read_snapshot(state / '.gitignore')
    if snapshot.data is None:
        return 'missing', snapshot
    return ('present' if snapshot.data == STATE_IGNORE else 'changed'), snapshot


def ensure_state_exclusion(state):
    status, snapshot = state_exclusion(state)
    if status == 'changed':
        raise HookError('state_git_exclusion_changed')
    if status == 'missing':
        atomic_write(state / '.gitignore', STATE_IGNORE, snapshot)


def locations(host, scope, project_root):
    if scope == 'project':
        if not project_root:
            raise HookError('project_root_required')
        root = safe_path(project_root)
        if not root.is_dir():
            raise HookError('project_root_not_directory')
        config = root / ('.codex/hooks.json' if host == 'codex'
                         else '.claude/settings.local.json')
    else:
        if project_root:
            raise HookError('project_root_requires_project_scope')
        variable = 'CODEX_HOME' if host == 'codex' else 'CLAUDE_CONFIG_DIR'
        override = os.environ.get(variable)
        if override and not Path(override).is_absolute():
            raise HookError('config_override_must_be_absolute: ' + variable)
        root = Path(override) if override else Path.home() / (
            '.codex' if host == 'codex' else '.claude')
        config = root / ('hooks.json' if host == 'codex' else 'settings.json')
    if sys.platform == 'win32':
        state_home = os.environ.get('LOCALAPPDATA')
        if not state_home:
            raise HookError('localappdata_required')
        state = Path(state_home)
    else:
        state = Path(os.environ.get('XDG_STATE_HOME', str(Path.home() / '.local/state')))
    if not state.is_absolute():
        raise HookError('state_directory_must_be_absolute')
    config = safe_path(config)
    state = safe_path(state / 'jev-capability-advisor/hooks')
    # Keep state outside the selected project even if an environment override
    # mistakenly points into it. User-scope callers must likewise select private
    # state rather than a repository directory.
    if project_root and state.is_relative_to(safe_path(project_root)):
        raise HookError('state_directory_inside_project')
    for parent in (state, *state.parents):
        marker = parent / '.git'
        if ((marker.is_dir() and (marker / 'HEAD').is_file())
                or (marker.is_file() and marker.read_bytes()[:8] == b'gitdir: ')):
            home = safe_path(Path.home())
            default_state = home / '.local/state/jev-capability-advisor/hooks'
            if sys.platform == 'win32' or parent != home or state != default_state:
                raise HookError('state_directory_inside_repository')
            verify_home_state_repository(home, state)
    target_id = hashlib.sha256(os.fsencode(os.path.normcase(str(config)))).hexdigest()
    return config, state / (target_id + '.json')


def event_entries(config):
    hooks = config.get('hooks', {})
    if not isinstance(hooks, dict):
        raise HookError('invalid_hooks_object')
    entries = hooks.get('UserPromptSubmit', [])
    if not isinstance(entries, list) or any(
            not isinstance(entry, dict) or not isinstance(entry.get('hooks'), list)
            or any(not isinstance(handler, dict) for handler in entry['hooks'])
            for entry in entries):
        raise HookError('invalid_user_prompt_submit_hooks')
    return entries


def receipt_entry(receipt, config_path, snapshot, host, scope):
    if not receipt:
        return None
    if (receipt.get('version') != STATE_VERSION
            or not isinstance(receipt.get('config_path'), str)
            or os.path.normcase(receipt['config_path']) != os.path.normcase(str(config_path))
            or receipt.get('host') != host or receipt.get('scope') != scope
            or receipt.get('phase') not in ('prepared', 'complete')):
        raise HookError('invalid_ownership_receipt')
    for field in ('entry', 'previous_entry'):
        entry = receipt.get(field)
        if entry is not None and (not isinstance(entry, dict)
                                 or MARKER not in json.dumps(entry)):
            raise HookError('invalid_ownership_receipt')
    if receipt['phase'] == 'complete':
        return receipt.get('entry')
    current = digest(snapshot.data)
    if current == receipt.get('after_sha256'):
        return receipt.get('entry')
    if current == receipt.get('before_sha256'):
        return receipt.get('previous_entry')
    raise HookError('incomplete_transaction: inspect private receipt and backup')


def inspect_ownership(entries, owned):
    if owned is None:
        if any(MARKER in json.dumps(entry) for entry in entries):
            raise HookError('ownership_receipt_missing')
        return None
    matches = [index for index, entry in enumerate(entries) if entry == owned]
    if len(matches) != 1:
        raise HookError('owned_entry_changed_missing_or_duplicated')
    if any(MARKER in json.dumps(entry) for index, entry in enumerate(entries)
           if index != matches[0]):
        raise HookError('duplicate_jev_registration')
    return matches[0]


@contextmanager
def manager_lock(receipt_path):
    parent = safe_path(receipt_path.parent)
    parent.mkdir(parents=True, exist_ok=True, mode=0o700)
    lock = safe_path(receipt_path.with_suffix('.lock'))
    try:
        fd = os.open(lock, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
    except FileExistsError as error:
        raise HookError('manager_locked: ' + str(lock)) from error
    try:
        with os.fdopen(fd, 'w', encoding='ascii') as stream:
            stream.write(str(os.getpid()) + '\n')
        ensure_state_exclusion(parent)
        yield
    finally:
        lock.unlink(missing_ok=True)


def change_config(config_path, receipt_path, config_snapshot, receipt_snapshot,
                  config, old_receipt, owned, replacement, host, scope,
                  interpreter, guidance, consent):
    with manager_lock(receipt_path):
        if (read_snapshot(config_path) != config_snapshot
                or read_snapshot(receipt_path) != receipt_snapshot):
            raise HookError('concurrent_modification')
        after = encode(config)
        backup = None
        if config_snapshot.data is not None:
            backup = receipt_path.parent / (
                receipt_path.stem + '-' + str(time.time_ns()) + '.backup.json')
            atomic_write(backup, config_snapshot.data, Snapshot(None, None))
        prepared = {
            'version': STATE_VERSION, 'host': host, 'scope': scope,
            'config_path': str(config_path), 'entry': replacement,
            'previous_entry': owned, 'phase': 'prepared',
            'before_sha256': digest(config_snapshot.data), 'after_sha256': digest(after),
            'backup_path': str(backup) if backup else None,
            'interpreter': interpreter, 'guidance': guidance,
            'advice_consent': consent,
            'previous_advice_consent': old_receipt.get('advice_consent'),
            'previous_interpreter': old_receipt.get('interpreter'),
            'previous_guidance': old_receipt.get('guidance'),
        }
        # Journal ownership first. A crash after either replacement remains
        # recoverable by comparing before/after hashes; never guess ownership.
        atomic_write(receipt_path, encode(prepared), receipt_snapshot)
        prepared_snapshot = read_snapshot(receipt_path)
        try:
            atomic_write(config_path, after, config_snapshot)
        except (OSError, HookError):
            # The prepared receipt is intentionally retained as recovery evidence.
            raise
        prepared['phase'] = 'complete'
        atomic_write(receipt_path, encode(prepared), prepared_snapshot)
        return str(backup) if backup else None


def registration_run(args):
    config_path, receipt_path = locations(args.host, args.scope, args.project_root)
    config_snapshot = read_snapshot(config_path)
    receipt_snapshot = read_snapshot(receipt_path)
    config = parse_object(config_snapshot, 'hook_config')
    receipt = parse_object(receipt_snapshot, 'ownership_receipt')
    entries = event_entries(config)
    owned = receipt_entry(receipt, config_path, config_snapshot, args.host, args.scope)
    index = inspect_ownership(entries, owned)
    exclusion, _ = state_exclusion(receipt_path.parent)
    if args.action != 'status' and exclusion == 'changed':
        raise HookError('state_git_exclusion_changed')
    if receipt.get('phase') == 'prepared':
        # receipt_entry already proved a before/after snapshot. Report metadata
        # for that actual registration and finalize only on explicit mutation.
        receipt = dict(receipt)
        if digest(config_snapshot.data) == receipt.get('before_sha256'):
            receipt['interpreter'] = receipt.get('previous_interpreter')
            receipt['guidance'] = receipt.get('previous_guidance')
            receipt['advice_consent'] = receipt.get('previous_advice_consent')
        receipt.update(entry=owned, phase='complete')
        if args.action != 'status' and not args.dry_run:
            with manager_lock(receipt_path):
                if read_snapshot(config_path) != config_snapshot:
                    raise HookError('concurrent_modification')
                atomic_write(receipt_path, encode(receipt), receipt_snapshot)
            receipt_snapshot = read_snapshot(receipt_path)
            exclusion = 'present'
    consent = active_consent(receipt, owned)
    interpreter = str(Path(sys.executable).resolve())
    result = {
        'host': args.host, 'scope': args.scope, 'config_path': str(config_path),
        'receipt_path': str(receipt_path), 'qualification': 'not_verified',
        'host_activation': 'not_verified', 'dry_run': args.dry_run,
        'state_git_exclusion': exclusion,
        'disclosure': DISCLOSURE, 'advice_consent': consent_status(consent),
    }
    if args.action == 'status':
        result.update(status='configured' if owned else 'absent',
                      registration=owned, interpreter=receipt.get('interpreter'),
                      guidance=receipt.get('guidance') if owned else None,
                      prerequisites=[
                          'Review and trust hooks in the host; managed policy may prevent activation.',
                          'Qualify current eligible inventory, delivery and adoption independently.',
                      ])
        registered_python = receipt.get('interpreter') if owned else None
        result['interpreter_available'] = bool(
            isinstance(registered_python, str) and Path(registered_python).is_file())
        result['known_obstacles'] = []
        if not consent:
            result['known_obstacles'].append('advice_consent_missing_or_changed')
        if exclusion != 'present':
            result['known_obstacles'].append('state_git_exclusion_' + exclusion)
        if owned and not result['interpreter_available']:
            result['known_obstacles'].append('registered_interpreter_missing')
        if config.get('disableAllHooks') is True:
            result['host_activation'] = 'disabled_in_selected_config'
            result['known_obstacles'].append('hooks_disabled_in_selected_config')
        return result
    if args.action == 'install':
        if sys.version_info < (3, 10):
            raise HookError('python_3_10_required')
        if not Path(interpreter).is_file():
            raise HookError('python_interpreter_unavailable')
        if sys.platform == 'win32':
            if Path(interpreter).suffix.lower() != '.exe':
                raise HookError('windows_interpreter_requires_exe')
            if args.host == 'codex':
                if shutil.which('powershell.exe') is None:
                    raise HookError('powershell_unavailable')
        choice = getattr(args, 'advice_consent', None)
        if choice == 'allow':
            consent = consent or new_consent()
        elif choice == 'revoke':
            consent = None
        result['advice_consent'] = consent_status(consent, args.dry_run)
        guidance = registration_guidance(args.host, args.scope, args.project_root, consent)
        replacement = build_registration(args.host, interpreter, guidance=guidance)
        result.update(registration=replacement, interpreter=interpreter, guidance=guidance)
        if owned == replacement and consent == receipt.get('advice_consent'):
            if not args.dry_run and exclusion == 'missing':
                with manager_lock(receipt_path):
                    pass
                result['state_git_exclusion'] = 'present'
            result['status'] = 'unchanged'
            return result
        updated = list(entries)
        if index is None:
            updated.append(replacement)
        else:
            updated[index] = replacement
        config.setdefault('hooks', {})['UserPromptSubmit'] = updated
        result['status'] = 'would_install' if args.dry_run else 'installed'
    else:
        consent = None
        result['advice_consent'] = consent_status(None)
        if owned is None:
            result['status'] = 'absent'
            return result
        replacement = None
        guidance = receipt.get('guidance')
        interpreter = receipt.get('interpreter')
        # Preserve surrounding keys, including intentionally empty objects.
        config['hooks']['UserPromptSubmit'] = [
            entry for position, entry in enumerate(entries) if position != index]
        result['status'] = 'would_uninstall' if args.dry_run else 'uninstalled'
    if not args.dry_run:
        result['backup_path'] = change_config(
            config_path, receipt_path, config_snapshot, receipt_snapshot, config,
            receipt, owned, replacement, args.host, args.scope, interpreter, guidance, consent)
        result['state_git_exclusion'] = 'present'
    return result



def key_file_readiness(path):
    """Check access without reading, retaining, or authenticating key contents."""
    try:
        path = safe_path(path)
        before = path.lstat()
        if not stat.S_ISREG(before.st_mode):
            return 'key_file_not_regular'
        flags = os.O_RDONLY | getattr(os, 'O_NOFOLLOW', 0) | getattr(os, 'O_BINARY', 0)
        with os.fdopen(os.open(path, flags), 'rb') as stream:
            opened = os.fstat(stream.fileno())
            path_after = path.lstat()
            if (not _same_path_metadata(before, opened)
                    or not _same_path_identity(before, path_after)
                    or not _same_path_metadata(opened, path_after)):
                return 'key_file_changed'
            if not opened.st_size:
                return 'key_file_empty'
        return None
    except FileNotFoundError:
        return 'key_file_missing'
    except (OSError, HookError, ValueError):
        return 'key_file_unreadable'


def credential_settings(path, host):
    snapshot = read_snapshot(path)
    settings = parse_object(snapshot, 'credential_config')
    if snapshot.data is not None:
        key_file = settings.get('key_file')
        if (settings.get('version') != 1 or settings.get('host') != host
                or not isinstance(key_file, str) or not key_file or '\0' in key_file
                or not Path(key_file).is_absolute()):
            raise HookError('invalid_credential_config')
    return snapshot, settings


def credential_status(settings):
    key_file = settings.get('key_file')
    if key_file is not None:
        reason = key_file_readiness(key_file)
        return {'source': 'key_file', 'key_file': key_file,
                'readiness': 'unavailable' if reason else 'ready', 'reason': reason}
    available = bool(os.environ.get('TYPESAFE_API_KEY', '').strip())
    return {'source': 'environment' if available else 'none',
            'readiness': 'ready' if available else 'unavailable',
            'reason': None if available else 'credentials_missing'}


def write_credential_settings(path, settings, expected):
    """Caller holds the host lock; backups contain references, never key bytes."""
    if read_snapshot(path) != expected:
        raise HookError('concurrent_modification: credential_config')
    if expected.data is not None:
        backup = path.with_name(path.stem + '-' + str(time.time_ns()) + '.backup.json')
        atomic_write(backup, expected.data, Snapshot(None, None))
    atomic_write(path, encode(settings), expected)


def integration_fingerprint():
    root = Path(__file__).resolve().parent.parent
    paths = [root / 'SKILL.md']
    for folder in ('scripts', 'assets', 'references', 'agents'):
        paths.extend(path for path in (root / folder).rglob('*')
                     if path.is_file() and '__pycache__' not in path.parts
                     and path.suffix != '.pyc')
    hasher = hashlib.sha256()
    for path in sorted(paths):
        hasher.update(path.relative_to(root).as_posix().encode('utf-8') + b'\0')
        hasher.update(hashlib.sha256(path.read_bytes()).digest())
    return hasher.hexdigest()


def host_version(host):
    # A status inspection must not start another host or create its runtime
    # files. The current agent supplies/verifies its version during live tests;
    # a standalone manager cannot attest that session's build from disk.
    return None


def qualification_context(host, registration):
    platform = ('windows' if sys.platform == 'win32' else
                'macos' if sys.platform == 'darwin' else
                'wsl' if os.environ.get('WSL_DISTRO_NAME') else 'linux')
    return {'host_version': host_version(host), 'platform': platform,
            'integration_sha256': integration_fingerprint(),
            'registration_sha256': digest(encode(registration)) if registration else None}


QUALIFICATION_SCENARIOS = (
    'registration', 'delivery', 'catalog', 'provider', 'adoption', 'disabled',
    'explicit_only', 'availability_change', 'incomplete_metadata', 'plan_mode',
    'missing_key', 'error', 'timeout', 'cancellation', 'followup',
)


def qualification_evidence(path, host, current):
    """Read bounded historical evidence; never treat it as a live session probe."""
    result = {'evidence_path': str(path), 'status': 'absent'}
    try:
        snapshot = read_snapshot(path)
        if snapshot.data is None:
            return result
        record = parse_object(snapshot, 'qualification_evidence')
        catalog = record.get('catalog', {})
        scenarios = record.get('scenarios', {})
        valid = (record.get('version') == 1 and record.get('host') == host
                 and isinstance(record.get('host_version'), str)
                 and re.fullmatch(r'[0-9]+\.[0-9]+\.[0-9]+(?:[-+][a-zA-Z0-9.-]+)?',
                                  record['host_version'])
                 and record.get('platform') in ('linux', 'wsl', 'macos', 'windows')
                 and all(isinstance(record.get(field), str) and re.fullmatch(
                     r'[a-f0-9]{64}', record[field])
                     for field in ('integration_sha256', 'registration_sha256'))
                 and isinstance(catalog, dict)
                 and all(type(catalog.get(field)) is int and 0 <= catalog[field] <= 240
                         for field in ('skills', 'mcp_tools'))
                 and catalog.get('completeness') in ('bounded', 'unknown')
                 and isinstance(scenarios, dict)
                 and all(scenarios.get(field) in ('passed', 'failed', 'simulated', 'not_run')
                         for field in QUALIFICATION_SCENARIOS))
        if not valid:
            raise HookError('invalid_qualification_evidence')
        # Do not echo arbitrary record fields, paths, transcripts or raw data.
        result.update({field: record[field] for field in (
            'host', 'host_version', 'platform', 'integration_sha256', 'registration_sha256')})
        result['catalog'] = {field: catalog[field] for field in ('skills', 'mcp_tools', 'completeness')}
        result['scenarios'] = {field: scenarios[field] for field in QUALIFICATION_SCENARIOS}
        result['status'] = 'matching_environment'
        if any(current[field] is not None and current[field] != record[field]
               for field in current):
            result['status'] = 'stale'
        elif any(value is None for value in current.values()):
            result['status'] = 'historical'
        result['all_scenarios_passed'] = all(value == 'passed' for value in result['scenarios'].values())
    except (HookError, OSError, UnicodeError, TypeError):
        result.update(status='invalid', reason='invalid_qualification_evidence')
    return result


def run(args):
    if getattr(args, 'advice_consent', None) is not None and args.action != 'install':
        raise HookError('advice_consent_requires_install')
    key_file = getattr(args, 'key_file', None)
    if key_file is not None and args.action != 'install':
        raise HookError('key_file_requires_install')
    _, receipt_path = locations(args.host, args.scope, args.project_root)
    settings_path = receipt_path.parent / (args.host + '-settings.json')
    pending_path = settings_path.with_suffix('.pending.json')
    try:
        pending_snapshot = read_snapshot(pending_path)
        pending = parse_object(pending_snapshot, 'credential_transaction')
    except (HookError, OSError, UnicodeError):
        if args.action == 'install':
            raise HookError('invalid_credential_transaction') from None
        pending_snapshot, pending = None, {'invalid': True}
    settings_error = None
    try:
        settings_snapshot, settings = credential_settings(settings_path, args.host)
    except (HookError, OSError, UnicodeError):
        if args.action == 'install':
            raise HookError('invalid_credential_config') from None
        settings_snapshot, settings = None, {}
        settings_error = 'invalid_credential_config'
    updated = dict(settings)
    if key_file is not None:
        key_file = str(safe_path(key_file))
        reason = key_file_readiness(key_file)
        if reason:
            raise HookError(reason)
        updated.update(version=1, host=args.host, key_file=key_file)
    changed = updated != settings
    if pending_snapshot is None or pending_snapshot.data is not None:
        settings_error = 'incomplete_credential_update'
        if args.action == 'install':
            if (pending.get('version') != 1 or pending.get('host') != args.host
                    or key_file is None or pending.get('key_file') != key_file
                    or digest(settings_snapshot.data) not in (
                        pending.get('before_sha256'), pending.get('after_sha256'))
                    or digest(encode(updated)) != pending.get('after_sha256')):
                raise HookError('incomplete_credential_update: retry the original install with --key-file; preserve the private journal')
            settings_error = None

    def apply():
        if settings_snapshot is not None and read_snapshot(settings_path) != settings_snapshot:
            raise HookError('concurrent_modification: credential_config')
        transaction_snapshot = pending_snapshot
        updating = args.action == 'install' and (changed or pending)
        if updating and not args.dry_run:
            if read_snapshot(pending_path) != pending_snapshot:
                raise HookError('concurrent_modification: credential_transaction')
            if not pending:
                journal = {'version': 1, 'host': args.host, 'key_file': key_file,
                           'before_sha256': digest(settings_snapshot.data),
                           'after_sha256': digest(encode(updated))}
                atomic_write(pending_path, encode(journal), pending_snapshot)
                transaction_snapshot = read_snapshot(pending_path)
        try:
            result = registration_run(args)
            if changed and not args.dry_run:
                write_credential_settings(settings_path, updated, settings_snapshot)
            if updating and not args.dry_run:
                if read_snapshot(pending_path) != transaction_snapshot:
                    raise HookError('concurrent_modification: credential_transaction')
                pending_path.unlink()
        except (HookError, OSError, UnicodeError) as error:
            if updating and not args.dry_run:
                raise HookError('credential_update_incomplete: hook may have changed; advice is disabled until the original install with --key-file succeeds') from error
            raise
        result['credential_config_path'] = str(settings_path)
        result['credential_transaction_path'] = str(pending_path)
        result['credential_configuration'] = (
            'would_update' if args.dry_run else 'updated') if changed else 'unchanged'
        result['credentials'] = ({'source': 'unknown', 'readiness': 'unavailable',
                                  'reason': settings_error} if settings_error
                                 else credential_status(updated))
        if args.action == 'status':
            context = qualification_context(args.host, result['registration'])
            result['qualification_context'] = context
            result['qualification_evidence'] = qualification_evidence(
                receipt_path.with_suffix('.qualification.json'), args.host, context)
            result['currently_unverified'] = [
                'effective_host_trust_and_policy', 'current_session_catalog_and_restrictions',
                'hook_delivery', 'provider_authentication', 'recommendation_adoption',
                'failure_and_followup_behavior',
            ]
            if result['credentials']['reason']:
                result['known_obstacles'].append(result['credentials']['reason'])
        return result

    if args.action == 'status' or args.dry_run:
        return apply()
    # Validate registration before creating any state or lock. Preserve the
    # existing no-op uninstall behavior when no registration exists.
    preview_args = copy(args)
    preview_args.dry_run = True
    preview = registration_run(preview_args)
    if args.action == 'uninstall' and preview['status'] == 'absent':
        return apply()
    # Serialize credential updates across all scopes for this host. Existing
    # registration locks and optimistic snapshots continue to protect ownership.
    with manager_lock(settings_path):
        return apply()

def main(argv=None):
    parser = Parser(description=__doc__, epilog=DISCLOSURE)
    parser.add_argument('action', choices=('install', 'status', 'uninstall', 'render'))
    parser.add_argument('--host', choices=('codex', 'claude-code'), required=True)
    parser.add_argument('--scope', choices=('user', 'project'))
    parser.add_argument('--project-root', type=Path)
    parser.add_argument('--dry-run', action='store_true')
    parser.add_argument('--advice-consent', choices=('allow', 'revoke'),
                        help='Install only: record or revoke explicit TypeSafe processing consent for this registration. ' + CONSENT_TERMS)
    parser.add_argument('--policy', choices=('repository-adopted',))
    parser.add_argument('--key-file', type=Path, help='Install only: save an existing key-file reference privately; never copy its contents')
    try:
        args = parser.parse_args(argv)
        if args.action == 'render':
            if (args.host != 'codex' or args.policy != 'repository-adopted'
                    or args.scope is not None or args.project_root is not None or args.dry_run
                    or args.key_file is not None or args.advice_consent is not None):
                raise HookError('invalid_arguments: render requires --host codex '
                                '--policy repository-adopted and no installer options')
            result = render_policy(args.host, args.policy)
        else:
            if args.policy is not None:
                raise HookError('invalid_arguments: --policy is only supported by render')
            args.scope = args.scope or 'user'
            result = run(args)
    except (HookError, OSError, UnicodeError, ValueError) as error:
        print(json.dumps({'status': 'error', 'reason': str(error)}, ensure_ascii=True))
        return 1
    print(json.dumps(result, ensure_ascii=True, indent=2))
    return 0


if __name__ == '__main__':
    sys.dont_write_bytecode = True
    raise SystemExit(main())

SHA-256: 356abd71f741a228ac0737496c1e6253da0f621605b2f43a0cd3bf3e2630b79b