Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,15 @@ packages = ["src/hermes_workflows"]

[tool.hatch.build.targets.wheel.force-include]
"src/hermes_workflows/plugin_payload_manifest.v1.json" = "hermes_workflows/plugin_payload_manifest.v1.json"
"src/hermes_workflows/plugin_payload/hermes-workflows-approvals/dashboard/dist/index.js" = "hermes_workflows/plugin_payload/hermes-workflows-approvals/dashboard/dist/index.js"
"src/hermes_workflows/plugin_payload/hermes-workflows-approvals/dashboard/dist/style.css" = "hermes_workflows/plugin_payload/hermes-workflows-approvals/dashboard/dist/style.css"

[tool.hatch.build.targets.sdist]
exclude = ["docs/presentation/2026-07-02/**"]
artifacts = [
"src/hermes_workflows/plugin_payload/hermes-workflows-approvals/dashboard/dist/index.js",
"src/hermes_workflows/plugin_payload/hermes-workflows-approvals/dashboard/dist/style.css",
]

[dependency-groups]
dev = ["pytest>=7", "build>=1", "tomli>=2; python_version<'3.11'"]
Expand Down
219 changes: 211 additions & 8 deletions src/hermes_workflows/package_resources.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

import hashlib
import json
import os
import re
import stat
from dataclasses import dataclass
from importlib import metadata, resources
from pathlib import Path, PurePosixPath
Expand All @@ -12,6 +14,14 @@
_SCHEMA_VERSION = 1
_DISTRIBUTION_NAME = "hermes-workflows"
_MANIFEST_RESOURCE = "plugin_payload_manifest.v1.json"
_CANONICAL_PAYLOAD_PATHS = (
"plugin_payload/hermes-workflows-approvals/__init__.py",
"plugin_payload/hermes-workflows-approvals/dashboard/dist/index.js",
"plugin_payload/hermes-workflows-approvals/dashboard/dist/style.css",
"plugin_payload/hermes-workflows-approvals/dashboard/manifest.json",
"plugin_payload/hermes-workflows-approvals/dashboard/plugin_api.py",
"plugin_payload/hermes-workflows-approvals/plugin.yaml",
)
_IDENTIFIER_PATTERN = re.compile(r"^[a-z0-9][a-z0-9_.-]{0,127}$")
_SHA256_PATTERN = re.compile(r"^[0-9a-f]{64}$")
_FILE_FIELDS = frozenset({"schema_version", "path", "sha256", "size_bytes"})
Expand Down Expand Up @@ -185,15 +195,38 @@ def manifest_from_json(value: str) -> PackageResourceManifestV1:
return manifest


def _resource_bytes(relative: str) -> bytes:
resource = resources.files("hermes_workflows")
for part in relative.split("/"):
resource = resource.joinpath(part)
return resource.read_bytes()


def _validated_payload_bytes(manifest: PackageResourceManifestV1) -> Tuple[Tuple[PackageResourceFileV1, bytes], ...]:
validated = []
for entry in manifest.files:
try:
data = _resource_bytes(entry.path)
except (FileNotFoundError, IsADirectoryError, OSError) as exc:
raise ValueError(f"packaged payload resource is missing or unreadable: {entry.path}") from exc
if len(data) != entry.size_bytes or hashlib.sha256(data).hexdigest() != entry.sha256:
raise ValueError(f"packaged payload resource does not match its manifest: {entry.path}")
validated.append((entry, data))
return tuple(validated)


def foundation_manifest() -> PackageResourceManifestV1:
text = resources.files("hermes_workflows").joinpath(_MANIFEST_RESOURCE).read_text(encoding="utf-8")
manifest = manifest_from_json(text)
if manifest.owner_id != _DISTRIBUTION_NAME or manifest.package_name != _DISTRIBUTION_NAME:
raise ValueError("foundation manifest owner and package must be hermes-workflows")
if manifest.payload_root != "plugin_payload" or manifest.files:
raise ValueError("foundation manifest must declare the empty plugin_payload foundation")
if manifest.payload_root != "plugin_payload":
raise ValueError("foundation manifest payload_root must equal plugin_payload")
if tuple(entry.path for entry in manifest.files) != _CANONICAL_PAYLOAD_PATHS:
raise ValueError("foundation manifest must declare the exact canonical plugin payload")
if text != canonical_manifest_json(manifest):
raise ValueError("foundation manifest resource must contain canonical JSON")
_validated_payload_bytes(manifest)
return manifest


Expand All @@ -202,14 +235,184 @@ def ownership_key(manifest: PackageResourceManifestV1) -> str:
return hashlib.sha256(canonical.encode("utf-8")).hexdigest()


def _validate_new_destination(root: Path) -> None:
if not root.is_absolute():
raise ValueError("package payload destination must be absolute")
if ".." in root.parts:
raise ValueError("package payload destination must not contain traversal components")

current = Path(root.anchor)
for component in root.parts[1:-1]:
current = current / component
try:
mode = current.lstat().st_mode
except FileNotFoundError:
raise ValueError(f"package payload destination parent does not exist: {current}")
if stat.S_ISLNK(mode):
raise ValueError(f"package payload destination contains symlink ancestor: {current}")
if not stat.S_ISDIR(mode):
raise ValueError(f"package payload destination ancestor is not a directory: {current}")


def _directory_open_flags() -> int:
required_flags = ("O_DIRECTORY", "O_NOFOLLOW")
if any(not hasattr(os, name) for name in required_flags):
raise OSError("package payload writes require no-follow directory descriptors")
required_dir_fd = (os.open, os.mkdir, os.stat, os.unlink, os.rmdir)
if any(function not in os.supports_dir_fd for function in required_dir_fd):
raise OSError("package payload writes require descriptor-relative filesystem operations")
if os.stat not in os.supports_follow_symlinks:
raise OSError("package payload writes require no-follow descriptor-relative stat")
return os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW | getattr(os, "O_CLOEXEC", 0)


def _open_directory_path(path: Path) -> int:
flags = _directory_open_flags()
descriptor = os.open(path.anchor, flags)
current = Path(path.anchor)
try:
for component in path.parts[1:]:
current = current / component
try:
child = os.open(component, flags, dir_fd=descriptor)
except OSError as exc:
try:
mode = os.stat(component, dir_fd=descriptor, follow_symlinks=False).st_mode
except OSError:
raise ValueError(f"package payload destination parent changed during validation: {current}") from exc
if stat.S_ISLNK(mode):
raise ValueError(f"package payload destination contains symlink ancestor: {current}") from exc
if not stat.S_ISDIR(mode):
raise ValueError(f"package payload destination ancestor is not a directory: {current}") from exc
raise ValueError(f"package payload destination parent changed during validation: {current}") from exc
os.close(descriptor)
descriptor = child
return descriptor
except Exception:
os.close(descriptor)
raise


def _same_file_identity(left: os.stat_result, right: os.stat_result) -> bool:
return left.st_dev == right.st_dev and left.st_ino == right.st_ino


def _entry_matches_stat(parent_fd: int, name: str, expected: os.stat_result) -> bool:
try:
actual = os.stat(name, dir_fd=parent_fd, follow_symlinks=False)
except OSError:
return False
return _same_file_identity(actual, expected)


def _entry_matches_fd(parent_fd: int, name: str, descriptor: int) -> bool:
try:
actual = os.stat(name, dir_fd=parent_fd, follow_symlinks=False)
expected = os.fstat(descriptor)
except OSError:
return False
return stat.S_ISDIR(actual.st_mode) and _same_file_identity(actual, expected)


def _path_matches_fd(path: Path, descriptor: int) -> bool:
try:
current = _open_directory_path(path)
except (OSError, ValueError):
return False
try:
return _same_file_identity(os.fstat(current), os.fstat(descriptor))
finally:
os.close(current)


def _remove_created_entries(
files: list[Tuple[int, str, os.stat_result]],
directories: list[Tuple[int, str, os.stat_result]],
) -> None:
for parent_fd, name, expected in reversed(files):
if _entry_matches_stat(parent_fd, name, expected):
try:
os.unlink(name, dir_fd=parent_fd)
except OSError:
pass
for parent_fd, name, expected in reversed(directories):
if _entry_matches_stat(parent_fd, name, expected):
try:
os.rmdir(name, dir_fd=parent_fd)
except OSError:
pass


def write_package_payload(
manifest: PackageResourceManifestV1,
destination: PathLike,
) -> Tuple[Path, ...]:
"""Return without touching the filesystem for the empty foundation payload."""
"""Copy the exact validated package payload into a new destination root."""

if not isinstance(manifest, PackageResourceManifestV1):
raise TypeError("manifest must be a PackageResourceManifestV1")
if manifest.files:
raise NotImplementedError("package payload copying is outside the foundation seam")
return ()
canonical = canonical_manifest_json(manifest)
packaged = foundation_manifest()
if canonical != canonical_manifest_json(packaged):
raise ValueError("manifest must exactly match the packaged payload manifest")
payload = _validated_payload_bytes(packaged)
root = Path(destination)
_validate_new_destination(root)
if not root.name:
raise FileExistsError(f"refusing to replace existing destination: {root}")

parent_fd = _open_directory_path(root.parent)
root_fd = None
directory_fds = {}
created_files = []
created_directories = []
written = []
try:
try:
os.mkdir(root.name, dir_fd=parent_fd)
except FileExistsError as exc:
raise FileExistsError(f"refusing to replace existing destination: {root}") from exc
root_stat = os.stat(root.name, dir_fd=parent_fd, follow_symlinks=False)
created_directories.append((parent_fd, root.name, root_stat))
root_fd = os.open(root.name, _directory_open_flags(), dir_fd=parent_fd)
if not _entry_matches_fd(parent_fd, root.name, root_fd):
raise ValueError("package payload destination changed during creation")
directory_fds[()] = root_fd

for entry, data in payload:
parts = PurePosixPath(entry.path).parts
parent_parts = ()
for component in parts[:-1]:
child_parts = parent_parts + (component,)
if child_parts not in directory_fds:
component_parent_fd = directory_fds[parent_parts]
os.mkdir(component, dir_fd=component_parent_fd)
component_stat = os.stat(component, dir_fd=component_parent_fd, follow_symlinks=False)
created_directories.append((component_parent_fd, component, component_stat))
child_fd = os.open(component, _directory_open_flags(), dir_fd=component_parent_fd)
if not _entry_matches_fd(component_parent_fd, component, child_fd):
os.close(child_fd)
raise ValueError("package payload destination directory changed during creation")
directory_fds[child_parts] = child_fd
parent_parts = child_parts

file_parent_fd = directory_fds[parent_parts]
file_flags = os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW | getattr(os, "O_CLOEXEC", 0)
file_fd = os.open(parts[-1], file_flags, 0o666, dir_fd=file_parent_fd)
file_stat = os.fstat(file_fd)
created_files.append((file_parent_fd, parts[-1], file_stat))
with os.fdopen(file_fd, "wb") as handle:
handle.write(data)
written.append(root / entry.path)

if not _path_matches_fd(root.parent, parent_fd) or not _entry_matches_fd(parent_fd, root.name, root_fd):
raise ValueError("package payload destination changed before completion")
except Exception:
_remove_created_entries(created_files, created_directories)
raise
finally:
for parts, descriptor in reversed(tuple(directory_fds.items())):
if parts:
os.close(descriptor)
if root_fd is not None:
os.close(root_fd)
os.close(parent_fd)
return tuple(written)
22 changes: 21 additions & 1 deletion src/hermes_workflows/plugin_install.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from pathlib import Path, PurePosixPath
from typing import Any, Dict, Mapping, Optional, Sequence, Tuple, Union

from . import package_resources
from .package_resources import installed_package_version


Expand Down Expand Up @@ -203,6 +204,11 @@ def _tree_files(root: Path, *, error_type: type[PluginInstallError]) -> Tuple[st
return tuple(sorted(discovered))


def _is_installer_generated_bytecode(relative: str) -> bool:
parts = PurePosixPath(relative).parts
return "__pycache__" in parts and relative.endswith(".pyc")


def _plugin_yaml_identity(path: Path) -> Tuple[str, str]:
_regular_file(path, error_type=PayloadValidationError, label="plugin.yaml")
name = None
Expand Down Expand Up @@ -252,10 +258,13 @@ def inspect_payload(
*,
expected_package_version: Optional[str] = None,
) -> PayloadDescriptor:
packaged_payload = payload_root is None
root = canonical_payload_root() if payload_root is None else Path(payload_root)
if not root.is_absolute():
root = root.resolve()
actual_files = _tree_files(root, error_type=PayloadValidationError)
if packaged_payload:
actual_files = tuple(relative for relative in actual_files if not _is_installer_generated_bytecode(relative))
if actual_files != PAYLOAD_FILES:
missing = sorted(set(PAYLOAD_FILES) - set(actual_files))
unexpected = sorted(set(actual_files) - set(PAYLOAD_FILES))
Expand All @@ -274,6 +283,17 @@ def inspect_payload(
path = root / relative
_regular_file(path, error_type=PayloadValidationError, label=relative)
files.append({"path": relative, "sha256": _sha256_file(path), "size_bytes": path.stat().st_size})
if packaged_payload:
try:
manifest = package_resources.foundation_manifest()
except (TypeError, ValueError, OSError) as exc:
raise PayloadValidationError(f"package payload manifest validation failed: {exc}") from exc
manifest_paths = tuple(f"plugin_payload/{PLUGIN_NAME}/{relative}" for relative in PAYLOAD_FILES)
if tuple(entry.path for entry in manifest.files) != manifest_paths:
raise PayloadValidationError("package payload manifest paths do not match the canonical payload")
for entry, actual in zip(manifest.files, files):
if entry.sha256 != actual["sha256"] or entry.size_bytes != actual["size_bytes"]:
raise PayloadValidationError(f"package payload manifest bytes do not match: {entry.path}")
unsigned = {
"schema_version": RECEIPT_SCHEMA_VERSION,
"owner_id": OWNER_ID,
Expand Down Expand Up @@ -658,7 +678,7 @@ def _install_or_upgrade(
) -> PluginLifecycleReport:
validated_home = _validated_profile_home(profile_home)
source = canonical_payload_root() if payload_root is None else Path(payload_root).resolve()
descriptor = inspect_payload(source, expected_package_version=expected_package_version)
descriptor = inspect_payload(payload_root, expected_package_version=expected_package_version)
home, plugins, destination, rollback = _profile_paths(validated_home)
_recover_interrupted(plugins, destination, rollback)
if require_existing and not destination.exists():
Expand Down
2 changes: 1 addition & 1 deletion src/hermes_workflows/plugin_payload_manifest.v1.json
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{"files":[],"owner_id":"hermes-workflows","package_name":"hermes-workflows","package_version":"0.0.1rc1","payload_root":"plugin_payload","schema_version":1}
{"files":[{"path":"plugin_payload/hermes-workflows-approvals/__init__.py","schema_version":1,"sha256":"ac164c5f06ea4ca831d6dc5c0bccc783083ea418dd1ffb500ff99a3ffc1206ca","size_bytes":155},{"path":"plugin_payload/hermes-workflows-approvals/dashboard/dist/index.js","schema_version":1,"sha256":"1d9ec2b5580fff8115bd1baa48a4673c443a329d8f7cb6fcbba2cf13e695281f","size_bytes":90830},{"path":"plugin_payload/hermes-workflows-approvals/dashboard/dist/style.css","schema_version":1,"sha256":"d69785262f38704ff233a59352755fbe77b9a5e0dbf8a5f2415a0f4ec943ac3a","size_bytes":19192},{"path":"plugin_payload/hermes-workflows-approvals/dashboard/manifest.json","schema_version":1,"sha256":"397be901686af9369ead9d925df452a700064f6547a2939902277615931d256e","size_bytes":399},{"path":"plugin_payload/hermes-workflows-approvals/dashboard/plugin_api.py","schema_version":1,"sha256":"a1990fed6595cc89906aba3f7f3e82716c922e1a86e6305d8af87773a61fc64c","size_bytes":104986},{"path":"plugin_payload/hermes-workflows-approvals/plugin.yaml","schema_version":1,"sha256":"54ef3dc362939103f654d424b41c502b93145d62dfa771b9a00f187afab39dd0","size_bytes":416}],"owner_id":"hermes-workflows","package_name":"hermes-workflows","package_version":"0.0.1rc1","payload_root":"plugin_payload","schema_version":1}
Loading