refactor: isolate Rayforge driver registry seam

This commit is contained in:
Codex 2026-10-21 15:18:50 +02:00
parent 415b00e157
commit fe2625e423
12 changed files with 292 additions and 28 deletions

View file

@ -21,6 +21,7 @@ The repository now provides:
synthetic inputs; synthetic inputs;
- a fail-closed manual desktop-acceptance report and validator; - a fail-closed manual desktop-acceptance report and validator;
- pinned upstream contract/license manifests and a drift checker; - pinned upstream contract/license manifests and a drift checker;
- a fail-closed API-23 adapter isolating Rayforge's internal driver registry;
- a proposed MIT project-license decision in ADR 0003; Kaz has not accepted - a proposed MIT project-license decision in ADR 0003; Kaz has not accepted
it, so the repository remains unlicensed. it, so the repository remains unlicensed.

View file

@ -58,6 +58,10 @@ Kaz must explicitly choose the project license. Until then this ADR remains
Proposed, the README's no-license statement remains authoritative, and no root Proposed, the README's no-license statement remains authoritative, and no root
`LICENSE` file is created. `LICENSE` file is created.
The compatibility-adapter part of item 5 is implemented for the offline addon
and fails closed outside Rayforge addon API 23. This implementation evidence
does not accept this ADR or select the project license.
See the See the
[upstream contract and license audit](../research/upstream-contract-and-license-audit.md) [upstream contract and license audit](../research/upstream-contract-and-license-audit.md)
for the evidence and API-stability assessment. for the evidence and API-stability assessment.

View file

@ -13,7 +13,7 @@ An external addon loaded through Rayforge's real `AddonManager` and registered
discovery specification or transport, and returns a recognizable binary test discovery specification or transport, and returns a recognizable binary test
payload from an `OpsEncoder`. payload from an `OpsEncoder`.
The following lifecycle completed twice with identical JSON results: The following lifecycle completes against the pinned Rayforge checkout:
1. load the external addon; 1. load the external addon;
2. resolve the driver by class name; 2. resolve the driver by class name;
@ -21,12 +21,12 @@ The following lifecycle completed twice with identical JSON results:
4. disable the addon and verify driver removal; 4. disable the addon and verify driver removal;
5. re-enable it and verify re-registration; 5. re-enable it and verify re-registration;
6. reload it and verify there is exactly one registry entry; 6. reload it and verify there is exactly one registry entry;
7. package its three source/manifest files into a ZIP; and 7. package its source and manifest files into a ZIP; and
8. disable it cleanly. 8. disable it cleanly.
The two result files had SHA-256 The current checked result is `tests/fixtures/rayforge-addon-spike.json` with
`610f8f191526afe4685f8659c1ef2b34fac3c68a8f8088d77ca5e1fdd6080bfc`. SHA-256
The checked result is `tests/fixtures/rayforge-addon-spike.json`. `c7a7ba7d561af58829c9c0fc271234e7600d50cca5f8125fc574fa3f0a1aa25f`.
## Registration seam finding ## Registration seam finding
@ -37,10 +37,13 @@ a driver class. The source-level driver registry instead exposes
`rayforge.machine.driver.register_driver(driver)`. `rayforge.machine.driver.register_driver(driver)`.
The working spike therefore registers during import of the addon's worker The working spike therefore registers during import of the addon's worker
entry point and removes its class in `on_unload`. This survives load, disable, entry point and removes its class in `on_unload`. All access to the mutable
enable and reload, but it relies on mutable module-level registry internals for module-level registry is now isolated in `rayforge_compat.py`. The adapter
cleanup. Before a production addon, Rayforge should ideally expose a dedicated accepts only addon API 23, checks the expected registry shape, refuses class
addon-aware driver registry hook with supported unregister behavior. name collisions, is idempotent, and verifies symmetric cleanup. Contract drift
therefore fails closed instead of silently replacing another driver. Before a
production addon, Rayforge should still expose a dedicated addon-aware driver
registry hook with supported unregister behavior.
## Reproduce ## Reproduce
@ -67,8 +70,9 @@ The script refuses any Rayforge revision other than the pinned commit.
- The ZIP check is addon packaging, not a proof that a complete PyInstaller or - The ZIP check is addon packaging, not a proof that a complete PyInstaller or
MSIX Rayforge build discovers an externally installed addon. MSIX Rayforge build discovers an externally installed addon.
- The current registration seam is functional but not a stable, addon-aware - The compatibility adapter contains the unstable seam, but does not make it
API. Upstream feasibility should be assessed before relying on it. a supported addon API. Upstream feasibility should be assessed before
relying on it in production.
- No JCZ command encoding, machine profile, UI, project reload, discovery, - No JCZ command encoding, machine profile, UI, project reload, discovery,
transport, controller status or cancellation is implemented here. transport, controller status or cancellation is implemented here.
- Nothing in this spike authorizes USB access, galvo movement or emission. - Nothing in this spike authorizes USB access, galvo movement or emission.

View file

@ -65,7 +65,10 @@ its manifest and sidecar; installer-only publication is not accepted.
The immediate upstream contribution should be a narrow, addon-aware driver The immediate upstream contribution should be a narrow, addon-aware driver
registry contract with symmetric registration and cleanup. Until upstream registry contract with symmetric registration and cleanup. Until upstream
accepts such a contract, all internal registry access must remain isolated in accepts such a contract, all internal registry access must remain isolated in
one compatibility adapter and guarded by the hashes/symbol tests. one compatibility adapter and guarded by the hashes/symbol tests. The current
addon now does this: its API-23 adapter rejects version drift, malformed
registries, duplicate entries and foreign class-name collisions, and its
load/disable/enable/reload lifecycle passes against the pinned checkout.
## Code-origin boundary ## Code-origin boundary

View file

@ -333,8 +333,9 @@ ignored build area, records their hashes and refuses to overwrite an existing
test run. test run.
The pinned upstream contracts and license notices are now machine-verifiable. The pinned upstream contracts and license notices are now machine-verifiable.
Rayforge's documented addon lifecycle is viable, but production driver Rayforge's documented addon lifecycle is viable, but production driver
registration still needs a supported symmetric registry API; project license registration still needs a supported symmetric registry API. The current
selection remains an explicit owner decision in ADR 0003. internal access is contained by a fail-closed API-23 compatibility adapter;
project license selection remains an explicit owner decision in ADR 0003.
## Decision rules ## Decision rules

View file

@ -87,7 +87,9 @@ evidence for:
run directory; run directory;
- pinned Rayforge and MeerK40t license/source contracts with drift tests, plus - pinned Rayforge and MeerK40t license/source contracts with drift tests, plus
checkpoint schema 2 requiring the Rayforge MIT notice in the portable ZIP checkpoint schema 2 requiring the Rayforge MIT notice in the portable ZIP
and beside the installer manifest. and beside the installer manifest;
- a single fail-closed Rayforge API-23 compatibility adapter around the
internal driver registry, with collision, drift and symmetric-cleanup tests.
The remaining M1 gates are deliberately explicit: The remaining M1 gates are deliberately explicit:

View file

@ -113,7 +113,9 @@ def run(source: Path, addon_path: Path) -> dict[str, object]:
), ),
"lifecycle": ["load", "disable", "enable", "reload", "disable"], "lifecycle": ["load", "disable", "enable", "reload", "disable"],
"packaged_files": packaged_files, "packaged_files": packaged_files,
"registration_seam": "worker import side effect + on_unload cleanup", "registration_seam": (
"fail-closed API 23 compatibility adapter + on_unload cleanup"
),
} }

View file

@ -0,0 +1,127 @@
"""Fail-closed compatibility boundary for Rayforge's driver registry.
Rayforge API 23 does not expose addon-aware driver registration and cleanup.
Keep the unavoidable internal registry access here so it can be removed when
the host provides a supported symmetric API.
"""
from typing import Any
SUPPORTED_PLUGIN_API_VERSION = 23
class RayforgeCompatibilityError(RuntimeError):
"""The pinned Rayforge registry contract is unavailable or has drifted."""
def _resolve_host() -> tuple[Any, int]:
from rayforge.core.hooks import PLUGIN_API_VERSION
from rayforge.machine import driver as registry
return registry, PLUGIN_API_VERSION
def _validate_host(registry: Any, plugin_api_version: int) -> None:
if plugin_api_version != SUPPORTED_PLUGIN_API_VERSION:
raise RayforgeCompatibilityError(
"unsupported Rayforge addon API: "
f"expected {SUPPORTED_PLUGIN_API_VERSION}, got {plugin_api_version}"
)
if not isinstance(getattr(registry, "driver_by_classname", None), dict):
raise RayforgeCompatibilityError(
"Rayforge driver_by_classname registry is unavailable"
)
if not isinstance(getattr(registry, "drivers", None), list):
raise RayforgeCompatibilityError("Rayforge drivers registry is unavailable")
if not callable(getattr(registry, "register_driver", None)):
raise RayforgeCompatibilityError("Rayforge register_driver is unavailable")
def _host(
registry: Any | None, plugin_api_version: int | None
) -> tuple[Any, int]:
if registry is None or plugin_api_version is None:
resolved_registry, resolved_version = _resolve_host()
registry = resolved_registry if registry is None else registry
plugin_api_version = (
resolved_version
if plugin_api_version is None
else plugin_api_version
)
_validate_host(registry, plugin_api_version)
return registry, plugin_api_version
def register_driver(
driver_cls: type,
*,
registry: Any | None = None,
plugin_api_version: int | None = None,
) -> bool:
"""Register exactly one driver class, or fail before replacing anything."""
registry, _version = _host(registry, plugin_api_version)
name = driver_cls.__name__
registered = registry.driver_by_classname.get(name)
named_entries = [item for item in registry.drivers if item.__name__ == name]
if registered is driver_cls:
if named_entries == [driver_cls]:
return False
raise RayforgeCompatibilityError(
f"inconsistent Rayforge driver registry for {name}"
)
if registered is not None or named_entries:
raise RayforgeCompatibilityError(
f"Rayforge driver name collision for {name}"
)
registry.register_driver(driver_cls)
if registry.driver_by_classname.get(name) is not driver_cls:
raise RayforgeCompatibilityError(
f"Rayforge did not index registered driver {name}"
)
if sum(item is driver_cls for item in registry.drivers) != 1:
raise RayforgeCompatibilityError(
f"Rayforge did not register exactly one {name} driver"
)
return True
def unregister_driver(
driver_cls: type,
*,
registry: Any | None = None,
plugin_api_version: int | None = None,
) -> bool:
"""Remove only the exact class previously registered by this addon."""
registry, _version = _host(registry, plugin_api_version)
name = driver_cls.__name__
registered = registry.driver_by_classname.get(name)
identity_entries = [item for item in registry.drivers if item is driver_cls]
named_foreign = [
item
for item in registry.drivers
if item.__name__ == name and item is not driver_cls
]
if registered is None and not identity_entries and not named_foreign:
return False
if registered is not driver_cls or named_foreign:
raise RayforgeCompatibilityError(
f"refusing to unregister foreign Rayforge driver {name}"
)
del registry.driver_by_classname[name]
registry.drivers[:] = [
item for item in registry.drivers if item is not driver_cls
]
if name in registry.driver_by_classname or any(
item is driver_cls for item in registry.drivers
):
raise RayforgeCompatibilityError(
f"Rayforge did not fully unregister driver {name}"
)
return True

View file

@ -12,7 +12,6 @@ import struct
from typing import Any from typing import Any
from rayforge.core.hooks import hookimpl from rayforge.core.hooks import hookimpl
from rayforge.machine import driver as driver_registry
from rayforge.machine.driver import NoDeviceDriver from rayforge.machine.driver import NoDeviceDriver
from rayforge.pipeline.encoder.base import ( from rayforge.pipeline.encoder.base import (
EncodedOutput, EncodedOutput,
@ -24,6 +23,7 @@ from .offline_session import (
OfflineJCZSession, OfflineJCZSession,
OfflineSessionState, OfflineSessionState,
) )
from .rayforge_compat import register_driver, unregister_driver
class OfflineJCZEncoder(OpsEncoder): class OfflineJCZEncoder(OpsEncoder):
@ -341,19 +341,11 @@ class PirateRayJCZNoDeviceDriver(NoDeviceDriver):
def _register() -> None: def _register() -> None:
name = PirateRayJCZNoDeviceDriver.__name__ register_driver(PirateRayJCZNoDeviceDriver)
if name not in driver_registry.driver_by_classname:
driver_registry.register_driver(PirateRayJCZNoDeviceDriver)
def _unregister() -> None: def _unregister() -> None:
name = PirateRayJCZNoDeviceDriver.__name__ unregister_driver(PirateRayJCZNoDeviceDriver)
registered = driver_registry.driver_by_classname.get(name)
if registered is PirateRayJCZNoDeviceDriver:
del driver_registry.driver_by_classname[name]
driver_registry.drivers[:] = [
item for item in driver_registry.drivers if item is not registered
]
# Rayforge currently has no driver registry in call_registration_hooks(). # Rayforge currently has no driver registry in call_registration_hooks().

View file

@ -19,9 +19,10 @@
"pirate_ray_jcz/__init__.py", "pirate_ray_jcz/__init__.py",
"pirate_ray_jcz/offline_session.py", "pirate_ray_jcz/offline_session.py",
"pirate_ray_jcz/profile_import.py", "pirate_ray_jcz/profile_import.py",
"pirate_ray_jcz/rayforge_compat.py",
"pirate_ray_jcz/recipe_metadata.py", "pirate_ray_jcz/recipe_metadata.py",
"pirate_ray_jcz/worker.py", "pirate_ray_jcz/worker.py",
"rayforge-addon.yaml" "rayforge-addon.yaml"
], ],
"registration_seam": "worker import side effect + on_unload cleanup" "registration_seam": "fail-closed API 23 compatibility adapter + on_unload cleanup"
} }

View file

@ -28,6 +28,10 @@ class RayforgeAddonSpikeTests(unittest.TestCase):
result["lifecycle"], result["lifecycle"],
) )
self.assertEqual(23, result["rayforge_api_version"]) self.assertEqual(23, result["rayforge_api_version"])
self.assertEqual(
"fail-closed API 23 compatibility adapter + on_unload cleanup",
result["registration_seam"],
)
def test_addon_contains_no_hardware_transport_import(self): def test_addon_contains_no_hardware_transport_import(self):
source = "\n".join( source = "\n".join(

View file

@ -0,0 +1,123 @@
import importlib.util
import unittest
from pathlib import Path
ROOT = Path(__file__).parents[1]
COMPAT_PATH = (
ROOT
/ "spikes"
/ "rayforge-jcz-no-device-addon"
/ "pirate_ray_jcz"
/ "rayforge_compat.py"
)
SPEC = importlib.util.spec_from_file_location("rayforge_compat_test", COMPAT_PATH)
COMPAT = importlib.util.module_from_spec(SPEC)
SPEC.loader.exec_module(COMPAT)
class FakeRegistry:
def __init__(self):
self.drivers = []
self.driver_by_classname = {}
def register_driver(self, driver_cls):
self.drivers.append(driver_cls)
self.driver_by_classname[driver_cls.__name__] = driver_cls
class ExampleDriver:
pass
class RayforgeCompatibilityTests(unittest.TestCase):
def test_registration_and_cleanup_are_symmetric_and_idempotent(self):
registry = FakeRegistry()
self.assertTrue(
COMPAT.register_driver(
ExampleDriver, registry=registry, plugin_api_version=23
)
)
self.assertFalse(
COMPAT.register_driver(
ExampleDriver, registry=registry, plugin_api_version=23
)
)
self.assertEqual([ExampleDriver], registry.drivers)
self.assertIs(
ExampleDriver, registry.driver_by_classname["ExampleDriver"]
)
self.assertTrue(
COMPAT.unregister_driver(
ExampleDriver, registry=registry, plugin_api_version=23
)
)
self.assertFalse(
COMPAT.unregister_driver(
ExampleDriver, registry=registry, plugin_api_version=23
)
)
self.assertEqual([], registry.drivers)
self.assertEqual({}, registry.driver_by_classname)
def test_api_version_drift_fails_before_mutation(self):
registry = FakeRegistry()
with self.assertRaisesRegex(
COMPAT.RayforgeCompatibilityError,
"expected 23, got 24",
):
COMPAT.register_driver(
ExampleDriver, registry=registry, plugin_api_version=24
)
self.assertEqual([], registry.drivers)
self.assertEqual({}, registry.driver_by_classname)
def test_name_collision_is_never_replaced_or_removed(self):
foreign_driver = type("ExampleDriver", (), {})
registry = FakeRegistry()
registry.register_driver(foreign_driver)
with self.assertRaisesRegex(
COMPAT.RayforgeCompatibilityError, "name collision"
):
COMPAT.register_driver(
ExampleDriver, registry=registry, plugin_api_version=23
)
with self.assertRaisesRegex(
COMPAT.RayforgeCompatibilityError, "foreign"
):
COMPAT.unregister_driver(
ExampleDriver, registry=registry, plugin_api_version=23
)
self.assertEqual([foreign_driver], registry.drivers)
self.assertIs(
foreign_driver, registry.driver_by_classname["ExampleDriver"]
)
def test_incomplete_registry_contract_fails_closed(self):
class MissingRegister:
drivers = []
driver_by_classname = {}
with self.assertRaisesRegex(
COMPAT.RayforgeCompatibilityError, "register_driver"
):
COMPAT.register_driver(
ExampleDriver,
registry=MissingRegister(),
plugin_api_version=23,
)
def test_adapter_has_no_transport_imports(self):
source = COMPAT_PATH.read_text(encoding="utf-8").lower()
for forbidden in ("import usb", "import serial", "import socket"):
self.assertNotIn(forbidden, source)
if __name__ == "__main__":
unittest.main()