safety: define fail-closed M3 research protocol

This commit is contained in:
Codex 2026-10-21 21:52:04 +02:00
parent 7131b50e2e
commit ed59b928c9
8 changed files with 709 additions and 9 deletions

View file

@ -50,6 +50,11 @@ for the packaged no-device workflow test. See the
[Phase 0 status](docs/status/phase-0.md) and [Phase 0 status](docs/status/phase-0.md) and
[Rayforge GUI preflight](docs/research/rayforge-windows-gui-preflight.md). [Rayforge GUI preflight](docs/research/rayforge-windows-gui-preflight.md).
The first M3 deliverable is also available as a deliberately non-executable
[hardware-research protocol](docs/research/m3-hardware-research-protocol.md).
Its public template and validator can establish only `ready_for_review`; they
never authorize USB access or controller commands.
## The important discovery ## The important discovery
We do not need to begin with a blank-sheet LightBurn clone: We do not need to begin with a blank-sheet LightBurn clone:
@ -80,8 +85,8 @@ hardware-free:
installing a toolchain through this repository; installing a toolchain through this repository;
3. run and record the packaged no-device desktop interaction checklist; 3. run and record the packaged no-device desktop interaction checklist;
4. accept, revise, or reject the proposed reuse architecture from evidence; 4. accept, revise, or reject the proposed reuse architecture from evidence;
5. write and review a separate hardware protocol before any live controller 5. complete and independently review the M3 hardware-protocol template before
communication. seeking separate approval for any live controller communication.
An independent desktop application is the fallback only if both existing An independent desktop application is the fallback only if both existing
platforms fail concrete, documented requirements. platforms fail concrete, documented requirements.

View file

@ -31,6 +31,9 @@ The repository now provides:
- a fail-closed API-23 adapter isolating Rayforge's internal driver registry; - 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.
- a fail-closed M3 hardware-research protocol, incomplete public plan template
and validator; validation can reach only `ready_for_review` and never
authorizes hardware access.
Run the ordinary checkpoint with the bundled Codex Python or another supported Run the ordinary checkpoint with the bundled Codex Python or another supported
Python interpreter and a writable `TEMP`/`TMP`: Python interpreter and a writable `TEMP`/`TMP`:
@ -67,6 +70,8 @@ order:
If no artifact is available, do not install a build environment here. The next If no artifact is available, do not install a build environment here. The next
useful input is the private known-working `.lbrn2` collection or Kaz's explicit useful input is the private known-working `.lbrn2` collection or Kaz's explicit
decision on ADR 0003. Until either external gate arrives, ADR 0002 remains decision on ADR 0003. The M3 protocol can also be reviewed offline, but its
Proposed and no controller backend, USB discovery, physical framing, galvo operation allowlist must stay empty until exact read-only request evidence is
motion or laser output is authorized. available. Until the external gates and separate hardware approval arrive,
ADR 0002 remains Proposed and no controller backend, USB endpoint access,
physical framing, galvo motion or laser output is authorized.

View file

@ -0,0 +1,190 @@
# M3 controller identity and status research protocol
- Status: Draft; not approved for execution
- Date: 2026-10-21
- Target: Makerspace Næstved JPT 100 W E2/M7 MOPA fiber laser
- Controller: JCZ/EZCAD2-compatible, observed USB `9588:9899`
- Scope: identity and read-only status only
- Hardware access authorized by this document: **no**
## Purpose
This protocol is the review gate between the completed no-device work and any
future controller access. It is deliberately narrower than a controller test:
it defines how a proposed session must be specified, reviewed, stopped and
recorded before an endpoint may be opened.
The public template is
`tests/fixtures/m3-hardware-protocol-template.json`. It contains no allowed USB
operation because the repository does not yet have sufficient evidence to name
exact request bytes, response constraints and safety behavior. That absence is
a blocker, not a field to fill by inference.
The validator can only report `incomplete`, `invalid`, or `ready_for_review`.
It always reports `hardware_access_authorized: false`. A later, session-specific
human approval must reference the immutable plan hash; neither this document
nor a successful validator run grants permission to touch hardware.
## Known facts and unresolved assumptions
Observed facts:
- Windows passively enumerated `USBLMCV4`, VID/PID `9588:9899`, device
revision descriptor `0005`, with no compatible function driver installed.
- The LightBurn export identifies a `JCZFiber`/`LibUSB` JPT profile and a
110 × 110 mm work area.
- BalorMK independently targets the same USB identity and provides a mock
transport, but its live commands include motion- and emission-capable paths.
Not established:
- that descriptor `0005` is a board or firmware revision;
- that any apparently read-only controller request cannot reset state, move a
galvo, arm the source or change an output;
- the currently mounted lens and correction profile by physical verification;
- a reviewed, reversible Windows driver strategy for this computer;
- the exact request/response bytes safe to use for identity or status.
## Non-negotiable scope
M3 may eventually cover controller enumeration, identity, firmware/capability
discovery and status. The following remain prohibited throughout this protocol:
- persistent writes or configuration changes;
- job or list upload and execution;
- galvo motion and physical framing;
- source arming or laser emission;
- replay of an unknown request or response;
- automatic retry after an abort or unexpected response.
M4 requires a new protocol. An M3 result cannot be stretched to authorize
motion, framing, marking, parameter sweeps or output-off assumptions.
## Gates before a session can be reviewed
### 1. Evidence gate
Every proposed operation must be listed individually with:
- a unique operation identifier and single purpose;
- public evidence provenance using an artifact ID;
- evidence level (`observed` or `independently-implemented`);
- exact request bytes—never wildcards, inferred fields or exploratory ranges;
- a bounded expected-response contract;
- one timeout and exactly one attempt;
- explicit `read-only-no-motion-no-emission` classification;
- `state_change_expected: false`.
An empty allowlist keeps the plan incomplete. A malformed or state-changing
operation makes it invalid.
### 2. Physical gate
Before connecting the research computer, the session plan must record:
- the reviewed physical method that prevents emission independently of
software, and confirmation that it was verified;
- a cleared work area that assumes unexpected galvo motion is still possible;
- an accessible and tested emergency stop;
- a named safety observer present for the whole live portion;
- physical confirmation of the machine/profile identity and mounted optics.
“Laser power set to zero” or a UI toggle is not an independent emission
inhibition method.
### 3. Software and driver gate
- The production controller backend remains absent from The Pirate Ray.
- A narrow experiment harness must enforce the reviewed operation allowlist.
- Unknown responses are captured and stop the session; they are never replayed.
- No operation is retried automatically.
- The current Windows driver state is recorded privately before change.
- Driver installation, replacement and rollback require their own reviewed,
reversible plan. This repository does not install or switch drivers.
- The normal LightBurn control computer is not modified for convenience.
### 4. People and review gate
The plan names an operator, safety observer and protocol reviewer. One person
may not silently fill all roles during execution. The reviewer checks the
machine identity, evidence for every allowlisted operation, physical isolation,
driver rollback, capture destination and stop/recovery procedures.
After `validate_m3_hardware_protocol.py` reports `ready_for_review`, freeze the
plan bytes and SHA-256. A separate approval record must identify that hash,
reviewer, approval time with timezone, location and exact session window. Any
plan edit invalidates that approval.
## Session sequence
The eventual reviewed session follows this order and stops at the first failed
gate:
1. Brief operator and observer; verify emergency-stop access.
2. Confirm machine, source, lens and controller identity without opening an
endpoint.
3. Apply and verify the independent emission inhibition.
4. Clear the work area and assume motion remains possible.
5. Record the original Windows driver state and rollback evidence privately.
6. Verify hashes for the plan, harness and dependencies.
7. Start capture into the prepared private destination.
8. Connect only the allowlisted controller identity.
9. Execute one allowlisted operation once.
10. Compare the response only with its predeclared constraints.
11. Stop; do not improvise a follow-up request.
12. Disconnect, restore the reviewed driver state, and verify normal shutdown.
13. Hash raw evidence privately and create only sanitized derived public notes.
No batch of “harmless queries” is permitted. Additional operations require a
new reviewed plan or a new version of the allowlist.
## Immediate abort conditions
Any of these ends the session without automatic retry:
- unexpected galvo motion;
- any emission or source-arm indication;
- interlock state change;
- unexpected controller reset or disconnect;
- attempted transfer not present in the immutable allowlist;
- response outside the predeclared contract;
- stop request from either operator or observer.
On abort: stop the harness, disconnect USB if safe, use the reviewed physical
power-down procedure, preserve logs, record an incident, and do not send a
speculative reset/stop command. A controller stop command is itself prohibited
until separately evidenced and allowlisted.
## Evidence and publication
Raw captures, serials, instance identifiers, driver exports, photos and local
paths go to `ThePirateRay-PrivateStash` with byte count, SHA-256, acquisition
time, origin and classification. The public repository receives sanitized
derived notes and synthetic regression fixtures only. Vendor binaries are not
executed or committed publicly.
The session report must distinguish:
- **observed** bytes or state;
- **inferred** interpretation;
- **unknown** behavior requiring more evidence.
## Validate the draft plan
```powershell
python scripts/validate_m3_hardware_protocol.py `
tests/fixtures/m3-hardware-protocol-template.json
```
The committed template is expected to return `incomplete`: roles, physical
controls, recovery, driver plan, session metadata and exact evidence-backed
operations are intentionally absent. Do not change those fields merely to make
the validator green.
## Current conclusion
The written-protocol roadmap requirement is now represented and testable, but
the protocol is not executable. The next evidence needed for M3 is a reviewed
driver/rollback strategy and a justified exact read-only operation contract.
Until those exist and a specific plan receives separate human approval, no USB
endpoint may be opened and no controller bytes may be sent.

View file

@ -215,6 +215,12 @@ Before touching the connected machine, write a test protocol that specifies:
- abort conditions and rollback/recovery steps; - abort conditions and rollback/recovery steps;
- what is captured and how it is sanitized. - what is captured and how it is sanitized.
The initial, non-executable protocol, machine-readable template and validator
are now recorded in the
[M3 hardware-research protocol](research/m3-hardware-research-protocol.md).
The committed template is intentionally incomplete. Validator success means
only `ready_for_review`; it never grants permission to access hardware.
Exit criteria: the app recognizes the correct controller and reports status Exit criteria: the app recognizes the correct controller and reports status
without moving a galvo, arming the source, modifying persistent state, or without moving a galvo, arming the source, modifying persistent state, or
requiring proprietary executable code in the public build. requiring proprietary executable code in the public build.
@ -275,11 +281,14 @@ The next work package should be small enough to review in a few commits:
buffering and explicit safe state ordering, still without transport; buffering and explicit safe state ordering, still without transport;
5. exercise cancellation/recovery and the graphical workflow against mock 5. exercise cancellation/recovery and the graphical workflow against mock
backends, then accept, amend or reject ADR 0002; backends, then accept, amend or reject ADR 0002;
6. separately produce a written M3 hardware-research protocol before any USB 6. complete and independently review the written M3 hardware-research protocol
endpoint access or controller command. before seeking separate approval for any USB endpoint access or controller
command.
Hardware work cannot proceed until item 6 is reviewed and the physical setup The draft, template, validator and regression tests for item 6 are present.
is ready. Hardware work still cannot proceed until the exact read-only operation evidence,
driver/rollback plan, physical controls, distinct assigned roles and separate
human approval are all in place.
Item 2 is now covered by the Item 2 is now covered by the
[synthetic LightBurn JCZ device-import spike](research/rayforge-lightburn-device-import-spike.md): [synthetic LightBurn JCZ device-import spike](research/rayforge-lightburn-device-import-spike.md):

View file

@ -92,6 +92,10 @@ evidence for:
and beside the installer manifest; and beside the installer manifest;
- a single fail-closed Rayforge API-23 compatibility adapter around the - a single fail-closed Rayforge API-23 compatibility adapter around the
internal driver registry, with collision, drift and symmetric-cleanup tests. internal driver registry, with collision, drift and symmetric-cleanup tests.
- a written M3 hardware-research protocol, intentionally incomplete public
plan template and standard-library validator that requires exact
evidence-backed request bytes, distinct safety roles, physical controls and
recovery planning while always denying hardware authorization.
The remaining M1 gates are deliberately explicit: The remaining M1 gates are deliberately explicit:
@ -107,3 +111,8 @@ The remaining M1 gates are deliberately explicit:
No item in this section authorizes USB access, controller commands or laser No item in this section authorizes USB access, controller commands or laser
output. output.
The M3 protocol can currently report only `incomplete`. It has no allowlisted
controller operation, reviewed driver/rollback plan, assigned people or
confirmed physical controls. Even a future `ready_for_review` result requires
separate human approval before hardware access.

View file

@ -0,0 +1,264 @@
"""Validate an M3 hardware-research plan without authorizing or using hardware."""
from __future__ import annotations
import argparse
from datetime import datetime
import json
import re
from pathlib import Path
PROTOCOL_NAME = "M3 controller identity and status research"
EXPECTED_TARGET = {
"profile_id": "makerspace-naestved-jpt-e2-mopa-100w",
"controller_family": "JCZ",
"usb_vid": "9588",
"usb_pid": "9899",
"device_description": "USBLMCV4",
"lens_field_mm": 110,
"lens_status": "assumed-pending-physical-verification",
}
PROHIBITED_CAPABILITIES = {
"persistent_write",
"job_upload",
"list_execution",
"galvo_motion",
"physical_framing",
"source_arm",
"laser_emission",
}
ABORT_CONDITIONS = {
"unexpected_motion",
"emission_or_source_arm_indication",
"interlock_state_change",
"unexpected_device_reset_or_disconnect",
"unallowlisted_transfer_attempt",
"unexpected_response",
"operator_or_observer_stop",
}
PHYSICAL_CHECKS = (
"emission_inhibited_and_verified",
"work_area_cleared",
"emergency_stop_accessible_and_tested",
"observer_present",
"machine_profile_physically_confirmed",
)
SHA256_ARTIFACT = re.compile(r"^sha256-[0-9a-f]{20}$")
HEX_BYTES = re.compile(r"^(?:[0-9a-f]{2})+$")
OPERATION_KEYS = {
"operation_id",
"purpose",
"evidence_artifact_id",
"evidence_level",
"safety_class",
"direction",
"request_hex",
"expected_response",
"timeout_ms",
"maximum_attempts",
"state_change_expected",
}
def _timestamp_with_timezone(value: object) -> bool:
if not isinstance(value, str):
return False
try:
parsed = datetime.fromisoformat(value.replace("Z", "+00:00"))
except ValueError:
return False
return parsed.tzinfo is not None
def _validate_operation(operation: dict, seen: set[str]) -> list[str]:
errors = []
if set(operation) != OPERATION_KEYS:
errors.append("allowed operation fields do not match the protocol contract")
return errors
operation_id = operation["operation_id"]
if not isinstance(operation_id, str) or not operation_id.strip():
errors.append("allowed operation requires a non-empty operation_id")
elif operation_id in seen:
errors.append(f"duplicate allowed operation: {operation_id}")
else:
seen.add(operation_id)
if not isinstance(operation["purpose"], str) or not operation["purpose"].strip():
errors.append(f"allowed operation {operation_id!r} requires a purpose")
evidence_id = operation["evidence_artifact_id"]
if not isinstance(evidence_id, str) or SHA256_ARTIFACT.fullmatch(evidence_id) is None:
errors.append(f"allowed operation {operation_id!r} lacks public evidence provenance")
if operation["evidence_level"] not in {"observed", "independently-implemented"}:
errors.append(f"allowed operation {operation_id!r} has unsupported evidence level")
if operation["safety_class"] != "read-only-no-motion-no-emission":
errors.append(f"allowed operation {operation_id!r} has an unsafe classification")
if operation["direction"] != "host-to-controller-request-response":
errors.append(f"allowed operation {operation_id!r} has an unsupported direction")
request_hex = operation["request_hex"]
if not isinstance(request_hex, str) or HEX_BYTES.fullmatch(request_hex) is None:
errors.append(f"allowed operation {operation_id!r} lacks exact request bytes")
if not isinstance(operation["expected_response"], str) or not operation[
"expected_response"
].strip():
errors.append(f"allowed operation {operation_id!r} lacks response constraints")
if type(operation["timeout_ms"]) is not int or not 1 <= operation["timeout_ms"] <= 5000:
errors.append(f"allowed operation {operation_id!r} has an invalid timeout")
if type(operation["maximum_attempts"]) is not int or operation["maximum_attempts"] != 1:
errors.append(f"allowed operation {operation_id!r} permits retry")
if operation["state_change_expected"] is not False:
errors.append(f"allowed operation {operation_id!r} expects a state change")
return errors
def validate(path: Path) -> dict:
plan = json.loads(path.resolve().read_text(encoding="utf-8-sig"))
errors = []
incomplete = []
if not isinstance(plan, dict):
return {
"status": "invalid",
"errors": ["protocol plan must be a JSON object"],
"incomplete": [],
"validated_operations": 0,
"hardware_access_authorized": False,
"separate_human_approval_required": True,
}
if plan.get("schema_version") != 1:
errors.append("unsupported protocol schema")
if plan.get("protocol") != PROTOCOL_NAME:
errors.append("unexpected protocol identity")
if plan.get("protocol_status") != "draft":
errors.append("public protocol plan must remain draft")
if plan.get("target") != EXPECTED_TARGET:
errors.append("target does not match the sanitized machine profile")
scope = plan.get("scope", {})
if not isinstance(scope, dict):
errors.append("scope must be an object")
scope = {}
if scope.get("milestone") != "M3":
errors.append("protocol is outside M3")
prohibited = scope.get("prohibited_capabilities", [])
if not isinstance(prohibited, list) or set(prohibited) != PROHIBITED_CAPABILITIES:
errors.append("prohibited capability set does not match the safety boundary")
abort_conditions = plan.get("abort_conditions", [])
if not isinstance(abort_conditions, list) or set(abort_conditions) != ABORT_CONDITIONS:
errors.append("abort-condition set does not match the safety boundary")
roles = plan.get("roles", {})
if not isinstance(roles, dict):
errors.append("roles must be an object")
roles = {}
assigned_roles = []
for key in ("operator", "safety_observer", "protocol_reviewer"):
if not isinstance(roles.get(key), str) or not roles.get(key, "").strip():
incomplete.append(f"role not assigned: {key}")
else:
assigned_roles.append(roles[key].strip().casefold())
if len(assigned_roles) == 3 and len(set(assigned_roles)) != 3:
errors.append("operator, safety observer and protocol reviewer must be distinct")
physical = plan.get("physical_controls", {})
if not isinstance(physical, dict):
errors.append("physical_controls must be an object")
physical = {}
for key in PHYSICAL_CHECKS:
if physical.get(key) is not True:
incomplete.append(f"physical control not confirmed: {key}")
if not isinstance(physical.get("emission_inhibition_method"), str) or not physical.get(
"emission_inhibition_method", ""
).strip():
incomplete.append("emission inhibition method is not recorded")
software = plan.get("software_controls", {})
if not isinstance(software, dict):
errors.append("software_controls must be an object")
software = {}
if software.get("production_backend_absent") is not True:
errors.append("production controller backend must remain absent")
if software.get("unknown_responses_not_replayed") is not True:
errors.append("unknown responses must never be replayed")
if software.get("maximum_attempts_per_operation") != 1:
errors.append("automatic operation retry is prohibited")
for key in ("operation_allowlist_enforced", "reversible_driver_plan_reviewed"):
if software.get(key) is not True:
incomplete.append(f"software control not confirmed: {key}")
operations = plan.get("allowed_operations", [])
if not isinstance(operations, list):
errors.append("allowed_operations must be a list")
operations = []
if not operations:
incomplete.append("no evidence-backed USB operation is allowlisted")
seen = set()
for operation in operations:
if not isinstance(operation, dict):
errors.append("allowed operation must be an object")
else:
errors.extend(_validate_operation(operation, seen))
recovery = plan.get("recovery", {})
if not isinstance(recovery, dict):
errors.append("recovery must be an object")
recovery = {}
if recovery.get("no_automatic_retry_after_abort") is not True:
errors.append("automatic retry after abort is prohibited")
for key in (
"power_down_procedure_reviewed",
"driver_rollback_procedure_reviewed",
"incident_record_destination_prepared",
):
if recovery.get(key) is not True:
incomplete.append(f"recovery control not confirmed: {key}")
evidence = plan.get("evidence_handling", {})
expected_evidence = {
"raw_capture_classification": "private-sensitive",
"raw_capture_destination": "ThePirateRay-PrivateStash",
"public_output": "sanitized-derived-notes-only",
"sha256_manifest_required": True,
"private_identifiers_redacted_publicly": True,
}
if evidence != expected_evidence:
errors.append("evidence handling does not match the public/private boundary")
session = plan.get("session", {})
if not isinstance(session, dict):
errors.append("session must be an object")
session = {}
if not isinstance(session.get("machine_location"), str) or not session.get(
"machine_location", ""
).strip():
incomplete.append("machine location is not recorded")
if not _timestamp_with_timezone(session.get("planned_at")):
incomplete.append("planned_at is not an ISO-8601 timestamp with timezone")
status = "invalid" if errors else "ready_for_review" if not incomplete else "incomplete"
return {
"status": status,
"errors": errors,
"incomplete": incomplete,
"validated_operations": len(operations) if not errors else 0,
"hardware_access_authorized": False,
"separate_human_approval_required": True,
}
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("plan", type=Path)
parser.add_argument("--output", type=Path)
args = parser.parse_args()
result = validate(args.plan)
rendered = json.dumps(result, indent=2, sort_keys=True) + "\n"
if args.output:
args.output.parent.mkdir(parents=True, exist_ok=True)
args.output.write_text(rendered, encoding="utf-8", newline="\n")
print(rendered, end="")
return 0 if result["status"] == "ready_for_review" else 1
if __name__ == "__main__":
raise SystemExit(main())

View file

@ -0,0 +1,75 @@
{
"schema_version": 1,
"protocol": "M3 controller identity and status research",
"protocol_status": "draft",
"target": {
"profile_id": "makerspace-naestved-jpt-e2-mopa-100w",
"controller_family": "JCZ",
"usb_vid": "9588",
"usb_pid": "9899",
"device_description": "USBLMCV4",
"lens_field_mm": 110,
"lens_status": "assumed-pending-physical-verification"
},
"scope": {
"milestone": "M3",
"goal": "read-only controller identity and status evidence",
"prohibited_capabilities": [
"persistent_write",
"job_upload",
"list_execution",
"galvo_motion",
"physical_framing",
"source_arm",
"laser_emission"
]
},
"roles": {
"operator": null,
"safety_observer": null,
"protocol_reviewer": null
},
"physical_controls": {
"emission_inhibited_and_verified": null,
"emission_inhibition_method": null,
"work_area_cleared": null,
"emergency_stop_accessible_and_tested": null,
"observer_present": null,
"machine_profile_physically_confirmed": null
},
"software_controls": {
"production_backend_absent": true,
"operation_allowlist_enforced": null,
"unknown_responses_not_replayed": true,
"maximum_attempts_per_operation": 1,
"reversible_driver_plan_reviewed": null
},
"allowed_operations": [],
"abort_conditions": [
"unexpected_motion",
"emission_or_source_arm_indication",
"interlock_state_change",
"unexpected_device_reset_or_disconnect",
"unallowlisted_transfer_attempt",
"unexpected_response",
"operator_or_observer_stop"
],
"recovery": {
"power_down_procedure_reviewed": null,
"driver_rollback_procedure_reviewed": null,
"incident_record_destination_prepared": null,
"no_automatic_retry_after_abort": true
},
"evidence_handling": {
"raw_capture_classification": "private-sensitive",
"raw_capture_destination": "ThePirateRay-PrivateStash",
"public_output": "sanitized-derived-notes-only",
"sha256_manifest_required": true,
"private_identifiers_redacted_publicly": true
},
"session": {
"machine_location": null,
"planned_at": null,
"notes": ""
}
}

View file

@ -0,0 +1,143 @@
import copy
import json
import tempfile
import unittest
from pathlib import Path
from scripts.validate_m3_hardware_protocol import validate
ROOT = Path(__file__).resolve().parents[1]
TEMPLATE = ROOT / "tests" / "fixtures" / "m3-hardware-protocol-template.json"
class M3HardwareProtocolTests(unittest.TestCase):
def setUp(self):
self.template = json.loads(TEMPLATE.read_text(encoding="utf-8"))
def _write(self, root: Path, plan: dict) -> Path:
path = root / "plan.json"
path.write_text(json.dumps(plan), encoding="utf-8")
return path
def _complete(self) -> dict:
plan = copy.deepcopy(self.template)
plan["roles"] = {
"operator": "Synthetic Operator",
"safety_observer": "Synthetic Observer",
"protocol_reviewer": "Synthetic Reviewer",
}
plan["physical_controls"] = {
key: True for key in plan["physical_controls"]
if key != "emission_inhibition_method"
}
plan["physical_controls"]["emission_inhibition_method"] = (
"Synthetic reviewed isolation method"
)
plan["software_controls"]["operation_allowlist_enforced"] = True
plan["software_controls"]["reversible_driver_plan_reviewed"] = True
plan["allowed_operations"] = [{
"operation_id": "synthetic-read-identity",
"purpose": "Synthetic validator fixture only",
"evidence_artifact_id": "sha256-0123456789abcdef0123",
"evidence_level": "independently-implemented",
"safety_class": "read-only-no-motion-no-emission",
"direction": "host-to-controller-request-response",
"request_hex": "0102",
"expected_response": "Synthetic fixed response contract",
"timeout_ms": 1000,
"maximum_attempts": 1,
"state_change_expected": False,
}]
plan["recovery"].update({
"power_down_procedure_reviewed": True,
"driver_rollback_procedure_reviewed": True,
"incident_record_destination_prepared": True,
})
plan["session"] = {
"machine_location": "Synthetic test location",
"planned_at": "2026-10-21T12:00:00+02:00",
"notes": "No hardware is used by this unit test.",
}
return plan
def test_public_template_is_incomplete_and_never_authorizes_hardware(self):
result = validate(TEMPLATE)
self.assertEqual("incomplete", result["status"])
self.assertFalse(result["hardware_access_authorized"])
self.assertTrue(result["separate_human_approval_required"])
self.assertIn(
"no evidence-backed USB operation is allowlisted",
result["incomplete"],
)
def test_complete_plan_is_only_ready_for_review(self):
with tempfile.TemporaryDirectory(dir=ROOT / ".build") as directory:
result = validate(self._write(Path(directory), self._complete()))
self.assertEqual("ready_for_review", result["status"])
self.assertEqual(1, result["validated_operations"])
self.assertFalse(result["hardware_access_authorized"])
def test_roles_must_be_distinct(self):
with tempfile.TemporaryDirectory(dir=ROOT / ".build") as directory:
plan = self._complete()
plan["roles"]["safety_observer"] = plan["roles"]["operator"]
result = validate(self._write(Path(directory), plan))
self.assertEqual("invalid", result["status"])
self.assertIn(
"operator, safety observer and protocol reviewer must be distinct",
result["errors"],
)
self.assertFalse(result["hardware_access_authorized"])
def test_safety_boundary_cannot_be_narrowed(self):
with tempfile.TemporaryDirectory(dir=ROOT / ".build") as directory:
plan = self._complete()
plan["scope"]["prohibited_capabilities"].remove("laser_emission")
result = validate(self._write(Path(directory), plan))
self.assertEqual("invalid", result["status"])
self.assertIn(
"prohibited capability set does not match the safety boundary",
result["errors"],
)
def test_operation_requires_exact_bytes_provenance_and_no_retry(self):
with tempfile.TemporaryDirectory(dir=ROOT / ".build") as directory:
plan = self._complete()
operation = plan["allowed_operations"][0]
operation["request_hex"] = "unknown"
operation["evidence_artifact_id"] = "private-path"
operation["maximum_attempts"] = 2
result = validate(self._write(Path(directory), plan))
self.assertEqual("invalid", result["status"])
self.assertGreaterEqual(len(result["errors"]), 3)
self.assertFalse(result["hardware_access_authorized"])
def test_motion_or_state_change_operation_is_rejected(self):
with tempfile.TemporaryDirectory(dir=ROOT / ".build") as directory:
plan = self._complete()
operation = plan["allowed_operations"][0]
operation["safety_class"] = "motion"
operation["state_change_expected"] = True
result = validate(self._write(Path(directory), plan))
self.assertEqual("invalid", result["status"])
self.assertTrue(any("unsafe classification" in item for item in result["errors"]))
self.assertTrue(any("expects a state change" in item for item in result["errors"]))
def test_malformed_types_fail_closed_instead_of_crashing(self):
with tempfile.TemporaryDirectory(dir=ROOT / ".build") as directory:
plan = self._complete()
plan["scope"] = None
plan["roles"] = []
operation = plan["allowed_operations"][0]
operation["request_hex"] = 102
operation["evidence_artifact_id"] = False
operation["timeout_ms"] = True
result = validate(self._write(Path(directory), plan))
self.assertEqual("invalid", result["status"])
self.assertGreaterEqual(len(result["errors"]), 6)
self.assertFalse(result["hardware_access_authorized"])
if __name__ == "__main__":
unittest.main()