afv-library/plugins/builder/salesforce-development/scripts/internal_preview_installer.py

313 lines
13 KiB
Python

#!/usr/bin/env python3
"""Guarded deterministic installer for explicitly authorized internal previews."""
from __future__ import annotations
import ast
import importlib.util
import json
import os
import re
import subprocess
from pathlib import Path
from typing import Callable, Mapping, Optional
try:
import capability_registry as registry
except ImportError:
module_path = Path(__file__).resolve().parent / "capability_registry.py"
spec = importlib.util.spec_from_file_location("internal_preview_capability_registry", module_path)
if spec is None or spec.loader is None:
raise
registry = importlib.util.module_from_spec(spec)
spec.loader.exec_module(registry)
INTERNAL_NOTICE = "INTERNAL PREVIEW — not publicly supported"
SOURCE_CHANNEL = "internal-preview"
CLI_VERSION = "skills@1.5.20"
DEFAULT_TIMEOUT_SECONDS = 120
MAX_STREAM_BYTES = 4000
# Trust boundary (#1030 adversarial review, finding 4): the npm_config_registry /
# npm_config_ca(file) keys below are forwarded on purpose so an internal user behind
# a corporate proxy/registry can install. The consequence is that the pinned
# `skills@1.5.20` spec carries no lockfile/integrity hash, so `npx` re-resolves it
# through whatever registry the environment points at — the version pin is an
# ergonomic default, NOT a supply-chain integrity guarantee. The post-install
# canonical-tree hash check (see _destination_hash / the postcondition below) rejects
# tampered *skill content*, but it does NOT constrain what the fetched CLI executes at
# install time; arbitrary code execution at install time is inherent to running any
# `npx` package and is out of scope for this gate. Do not read the pin+hash as
# defense against a hostile registry.
_SUBPROCESS_ENV_KEYS = {
"PATH", "HOME", "USERPROFILE", "TMPDIR", "TMP", "TEMP",
"XDG_CONFIG_HOME", "XDG_CACHE_HOME", "XDG_DATA_HOME", "XDG_STATE_HOME",
"npm_config_cache", "npm_config_prefix", "npm_config_userconfig", "npm_config_tmp",
"NPM_CONFIG_CACHE", "NPM_CONFIG_PREFIX", "NPM_CONFIG_USERCONFIG", "NPM_CONFIG_TMP",
"npm_config_registry", "NPM_CONFIG_REGISTRY", "npm_config_ca", "NPM_CONFIG_CA",
"npm_config_cafile", "NPM_CONFIG_CAFILE", "LANG", "COMSPEC", "SystemRoot",
"SYSTEMROOT", "PATHEXT",
}
class InstallerError(ValueError):
"""A safe, non-sensitive installer denial or postcondition failure."""
def _base(name: object) -> dict:
return {
"notice": INTERNAL_NOTICE,
"name": name if isinstance(name, str) and registry.NAME_PATTERN.fullmatch(name) else None,
"sourceChannel": SOURCE_CHANNEL,
"freshSessionRequired": True,
}
def _error(name: object, message: str, *, execution: Optional[dict] = None) -> tuple[int, dict]:
result = {**_base(name), "status": "error", "message": message}
if execution is not None:
result["execution"] = execution
return 1, result
def _read_holds(path: Path, authoring_names: set[str]) -> set[str]:
try:
lines = path.read_text(encoding="utf-8").splitlines()
except (OSError, UnicodeError) as exc:
raise InstallerError("internal checkout is invalid") from exc
for index, line in enumerate(lines):
stripped = line.strip()
if not stripped.startswith("internal:"):
continue
value = stripped.split(":", 1)[1].strip()
if value:
try:
parsed = ast.literal_eval(value)
except (SyntaxError, ValueError) as exc:
raise InstallerError("internal checkout is invalid") from exc
if not isinstance(parsed, list) or not all(isinstance(item, str) for item in parsed):
raise InstallerError("internal checkout is invalid")
held = set(parsed)
else:
held = set()
for child in lines[index + 1:]:
if child and not child[0].isspace():
break
match = re.match(r"^\s+-\s+(['\"]?)([a-z0-9-]+)\1\s*$", child)
if child.strip() and not match:
raise InstallerError("internal checkout is invalid")
if match:
held.add(match.group(2))
if any(not registry.NAME_PATTERN.fullmatch(item) for item in held) or held - authoring_names:
raise InstallerError("internal checkout is invalid")
return held
raise InstallerError("internal checkout is invalid")
def _validate_checkout(repo_root: Path, plugin_root: Path) -> tuple[Path, Path, dict[str, Path], dict[str, Path], dict[str, dict], set[str]]:
try:
repo = Path(repo_root).resolve(strict=True)
plugin = Path(plugin_root).resolve(strict=True)
plugin.relative_to(repo)
except (OSError, ValueError) as exc:
raise InstallerError("internal checkout is invalid") from exc
config = repo / "config.yml"
skills = repo / "skills"
if config.is_symlink() or not config.is_file() or skills.is_symlink() or not skills.is_dir():
raise InstallerError("internal checkout is invalid")
try:
authoring = registry.skill_directories(skills)
foundation = registry.skill_directories(plugin / "skills")
manifest = registry.load_public_manifest(plugin / registry.PUBLIC_MANIFEST_RELATIVE)
public = {row["name"]: row for row in manifest["skills"]}
held = _read_holds(config, set(authoring))
except (OSError, registry.RegistryError, InstallerError) as exc:
raise InstallerError("internal checkout is invalid") from exc
return repo, plugin, authoring, foundation, public, held
def _validate_project(cwd: Path) -> Path:
raw = Path(cwd).absolute()
try:
if raw.is_symlink():
raise InstallerError("current directory is not a valid Salesforce project")
project = raw.resolve(strict=True)
except OSError as exc:
raise InstallerError("current directory is not a valid Salesforce project") from exc
marker = project / "sfdx-project.json"
if not project.is_dir() or marker.is_symlink() or not marker.is_file():
raise InstallerError("current directory is not a valid Salesforce project")
return project
def _inspect_project_path(path: Path, project: Path) -> bool:
"""Validate one existing project path without following its final component."""
try:
stat = path.lstat()
except FileNotFoundError:
return False
except OSError as exc:
raise InstallerError("project destination is unsafe") from exc
if path.is_symlink() or not path.is_dir():
raise InstallerError("project destination is unsafe")
try:
path.resolve(strict=True).relative_to(project)
except (OSError, ValueError) as exc:
raise InstallerError("project destination is unsafe") from exc
return True
def _inspect_destination_chain(project: Path, destination: Path) -> bool:
_inspect_project_path(project, project)
_inspect_project_path(project / ".claude", project)
_inspect_project_path(project / ".claude" / "skills", project)
return _inspect_project_path(destination, project)
def _stream_bytes(value: object) -> int:
if value is None:
return 0
if isinstance(value, bytes):
return len(value)
if isinstance(value, str):
return len(value.encode("utf-8", errors="replace"))
try:
return len(str(value).encode("utf-8", errors="replace"))
except Exception:
return 0
def _execution(returncode: Optional[int], stdout: object, stderr: object, *, timed_out: bool) -> dict:
stdout_bytes = _stream_bytes(stdout)
stderr_bytes = _stream_bytes(stderr)
return {
"exitCode": returncode,
"timedOut": timed_out,
"stdoutBytes": stdout_bytes,
"stdoutTruncated": stdout_bytes > MAX_STREAM_BYTES,
"stderrBytes": stderr_bytes,
"stderrTruncated": stderr_bytes > MAX_STREAM_BYTES,
}
def _subprocess_env(env: Mapping[str, str]) -> dict[str, str]:
return {
key: value for key, value in env.items()
if isinstance(value, str)
and (key in _SUBPROCESS_ENV_KEYS or re.fullmatch(r"LC_[A-Z0-9_]+", key))
}
def _destination_hash(destination: Path, project: Path, expected_name: str) -> str:
try:
if destination.is_symlink() or not destination.is_dir():
raise InstallerError("project destination is not a real directory")
resolved = destination.resolve(strict=True)
resolved.relative_to(project)
skill = registry.read_skill(resolved / "SKILL.md")
if skill["name"] != expected_name:
raise InstallerError("project destination frontmatter is invalid")
return registry.canonical_tree_sha256(resolved, safety_root=project)
except (OSError, ValueError, registry.RegistryError) as exc:
if isinstance(exc, InstallerError):
raise
raise InstallerError("project destination validation failed") from exc
def install_internal_preview(
name: object,
*,
repo_root: Path,
plugin_root: Path,
cwd: Path,
env: Mapping[str, str],
runner: Callable = subprocess.run,
timeout_seconds: int = DEFAULT_TIMEOUT_SECONDS,
) -> tuple[int, dict]:
"""Install one eligible authoring variant into the current project.
The caller's explicit ``internal-preview install`` command is the user
authorization. This function independently requires the existing env gate;
it never adds or changes that gate itself.
"""
if env.get("SF_SKILLS_INTERNAL_PREVIEW") != "1":
return _error(name, "internal preview authorization denied")
if not isinstance(name, str) or len(name) > 64 or not registry.NAME_PATTERN.fullmatch(name):
return _error(name, "internal preview request denied")
if not isinstance(timeout_seconds, int) or not 1 <= timeout_seconds <= 300:
return _error(name, "internal preview request denied")
try:
repo, _plugin, authoring, foundation, public, held = _validate_checkout(repo_root, plugin_root)
project = _validate_project(cwd)
source = authoring.get(name)
if source is None or name not in held or name in foundation:
raise InstallerError("internal preview request denied")
expected_hash = registry.canonical_tree_sha256(source, safety_root=repo)
if name in public and public[name]["treeSha256"] == expected_hash:
raise InstallerError("internal preview request denied")
destination = project / ".claude" / "skills" / name
destination.parent.relative_to(project)
destination_exists = _inspect_destination_chain(project, destination)
except InstallerError as exc:
return _error(name, str(exc))
except (OSError, ValueError, registry.RegistryError):
return _error(name, "internal preview request denied")
if destination_exists:
try:
if _destination_hash(destination, project, name) != expected_hash:
raise InstallerError("project destination is not authoring-exact")
except InstallerError:
return _error(name, "internal preview request denied")
return 0, {
**_base(name),
"status": "already-installed",
"provenance": "authoring-exact",
"skillsLockPresent": (project / "skills-lock.json").is_file(),
}
argv = [
"npx", "--yes", CLI_VERSION, "add", str(repo / "skills"),
"--skill", name, "--agent", "claude-code", "--copy", "--yes",
]
try:
completed = runner(
argv,
cwd=project,
env=_subprocess_env(env),
capture_output=True,
shell=False,
timeout=timeout_seconds,
check=False,
)
except subprocess.TimeoutExpired as exc:
execution = _execution(None, exc.output, exc.stderr, timed_out=True)
return _error(name, "internal preview installer timed out", execution=execution)
except (OSError, ValueError):
return _error(name, "internal preview installer could not start")
except Exception:
return _error(name, "internal preview installer returned malformed output")
returncode = getattr(completed, "returncode", None)
execution = _execution(
returncode,
getattr(completed, "stdout", None),
getattr(completed, "stderr", None),
timed_out=False,
)
if type(returncode) is not int:
return _error(name, "internal preview installer returned malformed output", execution=execution)
if returncode != 0:
return _error(name, "internal preview installer failed", execution=execution)
try:
actual_hash = _destination_hash(destination, project, name)
except InstallerError:
return _error(name, "internal preview installation postcondition failed", execution=execution)
if actual_hash != expected_hash:
return _error(name, "internal preview installation content did not match authoring", execution=execution)
return 0, {
**_base(name),
"status": "installed",
"provenance": "authoring-exact",
"skillsLockPresent": (project / "skills-lock.json").is_file(),
"execution": execution,
}