build: pin upstream contracts and license notices

This commit is contained in:
Codex 2026-10-21 14:27:34 +02:00
parent ffbff9805c
commit 415b00e157
19 changed files with 598 additions and 14 deletions

View file

@ -107,6 +107,8 @@ project workspace or development destination.
- [Rayforge material-recipe model spike](docs/research/rayforge-recipe-model-spike.md) - [Rayforge material-recipe model spike](docs/research/rayforge-recipe-model-spike.md)
- [M1 no-device build and acceptance checkpoint](docs/research/m1-no-device-acceptance.md) - [M1 no-device build and acceptance checkpoint](docs/research/m1-no-device-acceptance.md)
- [Proposed Rayforge/BalorMK reuse decision](docs/adr/0002-reuse-rayforge-and-balormk.md) - [Proposed Rayforge/BalorMK reuse decision](docs/adr/0002-reuse-rayforge-and-balormk.md)
- [Proposed project license and upstream boundary](docs/adr/0003-project-license-and-upstream-boundary.md)
- [Upstream contract and license audit](docs/research/upstream-contract-and-license-audit.md)
- [Live USB enumeration](docs/research/live-usb-enumeration-2026-10-17.md) - [Live USB enumeration](docs/research/live-usb-enumeration-2026-10-17.md)
- [LightBurn device-export analysis](docs/research/lightburn-device-exports-2026-10-19.md) - [LightBurn device-export analysis](docs/research/lightburn-device-exports-2026-10-19.md)
- [Sanitized machine profile](docs/research/machine-profile.yaml) - [Sanitized machine profile](docs/research/machine-profile.yaml)

View file

@ -19,7 +19,10 @@ The repository now provides:
- a non-executing verifier for externally produced Windows artifacts; - a non-executing verifier for externally produced Windows artifacts;
- a checksummed acceptance-kit generator containing only allowlisted public - a checksummed acceptance-kit generator containing only allowlisted public
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;
- a proposed MIT project-license decision in ADR 0003; Kaz has not accepted
it, so the repository remains unlicensed.
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`:
@ -55,6 +58,7 @@ order:
4. `scripts/validate_m1_manual_acceptance.py`. 4. `scripts/validate_m1_manual_acceptance.py`.
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. Until either useful input is the private known-working `.lbrn2` collection or Kaz's explicit
external gate arrives, ADR 0002 remains Proposed and no controller backend, decision on ADR 0003. Until either external gate arrives, ADR 0002 remains
USB discovery, physical framing, galvo motion or laser output is authorized. Proposed and no controller backend, USB discovery, physical framing, galvo
motion or laser output is authorized.

View file

@ -123,6 +123,10 @@ Subsequent offline evidence:
over addon lifecycle, workflow, layers, recipes, encoding, negative cases, over addon lifecycle, workflow, layers, recipes, encoding, negative cases,
cancellation/recovery and synthetic imports. The packaged Windows GUI and a cancellation/recovery and synthetic imports. The packaged Windows GUI and a
known-working private project remain external gates. known-working private project remain external gates.
- [Upstream contract and license audit](../research/upstream-contract-and-license-audit.md):
both pins and license notices are now machine-verifiable; the documented
addon lifecycle is usable, but driver registration and cleanup still rely
on an internal registry seam that should be replaced upstream.
These results satisfy the current model, buffering and no-device integration These results satisfy the current model, buffering and no-device integration
proofs. They do not yet satisfy the packaged human-interaction or real-project proofs. They do not yet satisfy the packaged human-interaction or real-project

View file

@ -0,0 +1,63 @@
# ADR 0003: Project license and upstream attribution boundary
- Status: Proposed
- Date: 2026-10-21
## Context
The Pirate Ray is intended to become open source, but the repository currently
has no project license. Rayforge and MeerK40t are both pinned under MIT terms,
and the leading architecture is a Rayforge addon informed by BalorMK behavior.
ADR 0002 forbids copied upstream source until license compatibility and exact
attribution are recorded.
## Proposed decision
1. License original The Pirate Ray code under MIT.
2. Keep exact upstream repositories, revisions, license notices and contract
hashes in the public repository.
3. Include applicable notices in every source or binary distribution set.
4. Treat Rayforge's documented addon API as the preferred host boundary.
5. Isolate any unavoidable internal Rayforge registry access behind one
compatibility adapter and remove it when a supported upstream API exists.
6. Use BalorMK as a pinned behavioral oracle until a separately reviewed
decision authorizes adaptation of specific MIT-licensed implementation
files.
## Why MIT is proposed
MIT matches both principal upstreams, is compatible with contributing the
addon upstream, and imposes a small, auditable notice obligation. It does not
solve safety, warranty, trademark or third-party-data questions; those remain
separate project responsibilities.
## Consequences
- A root project `LICENSE` can be added only after Kaz accepts this ADR.
- Source provenance becomes part of code review and release acceptance.
- A Windows installer may not be published alone; its verified license
sidecar and checkpoint manifest are part of the distribution set.
- BalorMK code adaptation must identify source files and preserve the
MeerK40t notice.
- A Rayforge upgrade fails closed when a pinned license, source hash or
required symbol changes.
## Alternatives considered
- **Remain unlicensed:** acceptable for research snapshots but blocks the
intended open-source product and upstream code adaptation.
- **Apache-2.0:** provides an explicit patent grant but adds notice and license
complexity without a current upstream requirement.
- **GPL-family license:** may be appropriate for stronger reciprocity, but it
is a larger product-policy decision and is not required by the current MIT
dependencies.
## Acceptance
Kaz must explicitly choose the project license. Until then this ADR remains
Proposed, the README's no-license statement remains authoritative, and no root
`LICENSE` file is created.
See the
[upstream contract and license audit](../research/upstream-contract-and-license-audit.md)
for the evidence and API-stability assessment.

View file

@ -51,11 +51,13 @@ python scripts/verify_rayforge_windows_checkpoint.py `
--output .build/artifacts/rayforge-windows-verification.json --output .build/artifacts/rayforge-windows-verification.json
``` ```
The verifier requires portable artifact names, checks sizes and SHA-256 The verifier requires checkpoint schema 2 and portable artifact names, checks
digests, rejects unsafe ZIP paths, confirms the pinned Rayforge commit and sizes and SHA-256 digests, rejects unsafe ZIP paths, confirms the pinned
compares every packaged addon file with its recorded digest. It never executes Rayforge commit, compares every packaged addon file with its recorded digest,
the application or installer, and a passing result retains the manual GUI and requires the pinned Rayforge license both inside the ZIP and as a sidecar.
interaction gate. It never executes the application or installer, and a passing result retains
the manual GUI interaction gate. The installer, manifest and license sidecar
form one distribution set.
## Automated acceptance ## Automated acceptance

View file

@ -0,0 +1,91 @@
# Upstream contract and license audit
- Date: 2026-10-21
- Scope: pinned Rayforge and MeerK40t revisions
- Hardware access: none
- Legal status: engineering record, not legal advice
## Result
Both pinned checkouts declare MIT licensing and pass the machine-readable
contract check. Their exact repositories, commits, normalized license hashes,
source-file hashes and required symbols are recorded in
`upstreams/rayforge.json` and `upstreams/meerk40t.json`.
Run the check against separately obtained source trees:
```powershell
python scripts/check_upstream_contracts.py `
--rayforge <rayforge-checkout> `
--meerk40t <meerk40t-checkout> `
--output .build/artifacts/upstream-contracts.json
```
The current pinned trees pass. The command reads source and Git metadata only;
it imports neither project and cannot discover or open hardware.
## License observations
| Project | Pin | Declared license | Notice observation |
| --- | --- | --- | --- |
| Rayforge | `c28918c87f8273dd1d244f5a057addc192807fa5` | MIT | `pyproject.toml` names Samuel Abels as author; the pinned `LICENSE` text contains the MIT grant and warranty disclaimer but no explicit copyright line |
| MeerK40t | `d511944e08788e88077727f682957759884ff5ca` | MIT | The pinned notice states `Copyright (c) 2021 meerk40t` |
Exact notice copies live under `third_party/licenses/`. They are notices, not
a license grant for The Pirate Ray's own code. No upstream source is vendored
by adding them.
MIT permits use, modification and redistribution, but requires the copyright
and permission notice to accompany copies or substantial portions. Therefore:
- every Rayforge-based ZIP and installer distribution set must include the
pinned Rayforge notice;
- a distribution containing adapted or copied BalorMK/MeerK40t code must also
include the MeerK40t notice;
- attribution must name the upstream project and exact revision without
implying endorsement;
- The Pirate Ray still needs its own explicit license before original project
code is offered under open-source terms.
Checkpoint schema 2 now requires a verified Rayforge license sidecar and the
same notice inside the portable ZIP. The installer must be distributed with
its manifest and sidecar; installer-only publication is not accepted.
## API stability assessment
| Seam | Classification | Assessment |
| --- | --- | --- |
| Rayforge addon API version 23, `hookimpl`, `on_unload` | Documented addon API | Suitable compatibility boundary at the pinned revision |
| `register_machines` hook | Documented but unusable for this driver today | The hook exists, but the supplied manager does not expose driver-class registration |
| `NoDeviceDriver`, `OpsEncoder`, `EncodedOutput`, `MachineCodeOpMap` | Imported host interfaces | Required by the addon; pin and contract-test them before upgrades |
| `register_driver`, `drivers`, `driver_by_classname` | Internal registry | Functional but unsuitable as a permanent public contract; `register_driver` is not exported in `__all__` and no symmetric unregister function exists |
| Addon-manager registry cleanup | Host lifecycle implementation | Other registries support addon-owned cleanup, but the driver registry is not part of that mechanism |
| BalorMK `GalvoController` and `MockConnection` | Application-internal reference/test seam | Valid as a pinned behavioral oracle; not treated as a stable library API |
The immediate upstream contribution should be a narrow, addon-aware driver
registry contract with symmetric registration and cleanup. Until upstream
accepts such a contract, all internal registry access must remain isolated in
one compatibility adapter and guarded by the hashes/symbol tests.
## Code-origin boundary
The current PirateRay addon is a no-device clean-room spike. It imports
Rayforge host interfaces and compares independently generated traces with the
pinned BalorMK mock behavior. It does not vendor Rayforge or MeerK40t source.
Before adapting any BalorMK implementation code:
1. accept a project license compatible with MIT;
2. record the exact source files and revisions used;
3. preserve the MeerK40t notice in source and distributions;
4. keep transport code separate from the independently tested encoder; and
5. review the resulting diff for copied comments, identifiers and substantial
structure rather than assuming a rewrite has no attribution duty.
## Recommendation requiring owner decision
MIT is the simplest project-license candidate because both principal
upstreams use MIT and the intended addon is meant for upstream contribution.
This audit does not select it. Kaz must explicitly approve the project license
before a root `LICENSE` file is added or upstream implementation code is
adapted.

View file

@ -331,6 +331,10 @@ hashed screenshots stay outside the public repository pending sanitization.
A reproducible kit generator copies only allowlisted synthetic inputs into the A reproducible kit generator copies only allowlisted synthetic inputs into the
ignored build area, records their hashes and refuses to overwrite an existing 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.
Rayforge's documented addon lifecycle is viable, but production driver
registration still needs a supported symmetric registry API; project license
selection remains an explicit owner decision in ADR 0003.
## Decision rules ## Decision rules

View file

@ -71,7 +71,7 @@ evidence for:
the pinned BalorMK mock values; the pinned BalorMK mock values;
- fail-closed profile and encoder negative cases; - fail-closed profile and encoder negative cases;
- Rayforge workflow-model serialization and preview behavior for power, - Rayforge workflow-model serialization and preview behavior for power,
frequency and pulse width. frequency and pulse width;
- independent layer visibility/output state and ordered serialization; - independent layer visibility/output state and ordered serialization;
- material-recipe matching plus a fail-closed PirateRay provenance and exact - material-recipe matching plus a fail-closed PirateRay provenance and exact
machine/source/lens/correction compatibility gate; machine/source/lens/correction compatibility gate;
@ -84,7 +84,10 @@ evidence for:
pass missing steps, connected hardware, changed evidence or stop conditions; pass missing steps, connected hardware, changed evidence or stop conditions;
- a deterministic acceptance-kit generator that copies only four allowlisted - a deterministic acceptance-kit generator that copies only four allowlisted
public synthetic inputs, hashes them and refuses to overwrite an existing public synthetic inputs, hashes them and refuses to overwrite an existing
run directory. run directory;
- pinned Rayforge and MeerK40t license/source contracts with drift tests, plus
checkpoint schema 2 requiring the Rayforge MIT notice in the portable ZIP
and beside the installer manifest.
The remaining M1 gates are deliberately explicit: The remaining M1 gates are deliberately explicit:

View file

@ -74,6 +74,25 @@ if (-not (Test-Path -LiteralPath $installer)) {
} }
New-Item -ItemType Directory -Force -Path $ArtifactRoot | Out-Null New-Item -ItemType Directory -Force -Path $ArtifactRoot | Out-Null
$licenseDirectory = Join-Path $bundleDirectory "THIRD_PARTY_LICENSES"
$rayforgeLicense = Join-Path $checkout "LICENSE"
$normalizedLicense = [IO.File]::ReadAllText($rayforgeLicense).Replace("`r`n", "`n").Replace("`r", "`n")
$normalizedLicenseBytes = [Text.Encoding]::UTF8.GetBytes($normalizedLicense)
$rayforgeLicenseHash = [Convert]::ToHexString(
[Security.Cryptography.SHA256]::HashData($normalizedLicenseBytes)
).ToLowerInvariant()
if ($rayforgeLicenseHash -ne $upstreamManifest.license_file.sha256_lf) {
throw "Pinned Rayforge license hash mismatch"
}
$rayforgeNotice = Join-Path $repositoryRoot "third_party\licenses\Rayforge-LICENSE.txt"
$bundledRayforgeLicense = Join-Path $licenseDirectory "Rayforge-LICENSE.txt"
$sidecarRayforgeLicense = Join-Path $ArtifactRoot "Rayforge-LICENSE.txt"
if ($PSCmdlet.ShouldProcess($licenseDirectory, "Stage Rayforge license notice")) {
New-Item -ItemType Directory -Force -Path $licenseDirectory | Out-Null
Copy-Item -LiteralPath $rayforgeNotice -Destination $bundledRayforgeLicense -Force
Copy-Item -LiteralPath $rayforgeNotice -Destination $sidecarRayforgeLicense -Force
}
$archive = Join-Path $ArtifactRoot "$bundleName.zip" $archive = Join-Path $ArtifactRoot "$bundleName.zip"
if ($PSCmdlet.ShouldProcess($archive, "Create portable checkpoint archive")) { if ($PSCmdlet.ShouldProcess($archive, "Create portable checkpoint archive")) {
Compress-Archive -LiteralPath $bundleDirectory -DestinationPath $archive -Force Compress-Archive -LiteralPath $bundleDirectory -DestinationPath $archive -Force
@ -88,7 +107,7 @@ $addonFiles = Get-ChildItem -LiteralPath $addonSource -Recurse -File |
} }
} }
$result = [ordered]@{ $result = [ordered]@{
schema_version = 1 schema_version = 2
checkpoint = "Rayforge Windows no-device bundle" checkpoint = "Rayforge Windows no-device bundle"
rayforge_commit = $verifiedCommit rayforge_commit = $verifiedCommit
addon = "the_pirate_ray_jcz" addon = "the_pirate_ray_jcz"
@ -106,6 +125,15 @@ $result = [ordered]@{
sha256 = (Get-FileHash -Algorithm SHA256 -LiteralPath $installer).Hash.ToLowerInvariant() sha256 = (Get-FileHash -Algorithm SHA256 -LiteralPath $installer).Hash.ToLowerInvariant()
bytes = (Get-Item -LiteralPath $installer).Length bytes = (Get-Item -LiteralPath $installer).Length
} }
license_files = @(
[ordered]@{
upstream = "Rayforge"
path = Split-Path -Leaf $sidecarRayforgeLicense
bundle_path = "THIRD_PARTY_LICENSES/Rayforge-LICENSE.txt"
sha256 = (Get-FileHash -Algorithm SHA256 -LiteralPath $sidecarRayforgeLicense).Hash.ToLowerInvariant()
bytes = (Get-Item -LiteralPath $sidecarRayforgeLicense).Length
}
)
manual_interaction_required = $true manual_interaction_required = $true
} }
$manifestPath = Join-Path $ArtifactRoot "rayforge-windows-checkpoint.json" $manifestPath = Join-Path $ArtifactRoot "rayforge-windows-checkpoint.json"

View file

@ -0,0 +1,127 @@
"""Verify pinned upstream licenses, source hashes, and required API symbols."""
from __future__ import annotations
import argparse
import ast
import hashlib
import json
import subprocess
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
def _sha256(path: Path) -> str:
return hashlib.sha256(path.read_bytes()).hexdigest()
def _text_sha256_lf(path: Path) -> str:
normalized = path.read_text(encoding="utf-8").replace("\r\n", "\n").replace("\r", "\n")
return hashlib.sha256(normalized.encode("utf-8")).hexdigest()
def _git_commit(path: Path) -> str:
return subprocess.run(
["git", "-C", str(path), "rev-parse", "HEAD"],
check=True,
capture_output=True,
text=True,
).stdout.strip()
def _symbols(path: Path) -> set[str]:
tree = ast.parse(path.read_text(encoding="utf-8"), filename=str(path))
symbols: set[str] = set()
for node in tree.body:
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)):
symbols.add(node.name)
if isinstance(node, ast.ClassDef):
for child in node.body:
if isinstance(child, (ast.FunctionDef, ast.AsyncFunctionDef)):
symbols.add(f"{node.name}.{child.name}")
if isinstance(node, (ast.Assign, ast.AnnAssign)):
targets = node.targets if isinstance(node, ast.Assign) else [node.target]
for target in targets:
if isinstance(target, ast.Name):
symbols.add(target.id)
return symbols
def inspect_tree(manifest: dict, checkout: Path, observed_commit: str) -> dict:
failures = []
if observed_commit != manifest["commit"]:
failures.append(
f"commit mismatch: expected {manifest['commit']}, found {observed_commit}"
)
license_record = manifest["license_file"]
license_path = checkout / license_record["path"]
if not license_path.is_file():
failures.append(f"missing license file: {license_record['path']}")
elif _text_sha256_lf(license_path) != license_record["sha256_lf"]:
failures.append(f"license hash mismatch: {license_record['path']}")
contracts = []
for record in manifest["contract_files"]:
path = checkout / record["path"]
missing_symbols = []
if not path.is_file():
failures.append(f"missing contract file: {record['path']}")
else:
if _sha256(path) != record["sha256"]:
failures.append(f"contract hash mismatch: {record['path']}")
available = _symbols(path)
missing_symbols = sorted(set(record["required_symbols"]) - available)
if missing_symbols:
failures.append(
f"missing symbols in {record['path']}: {', '.join(missing_symbols)}"
)
contracts.append({
"path": record["path"],
"stability": record["stability"],
"required_symbols": record["required_symbols"],
"missing_symbols": missing_symbols,
})
return {
"name": manifest["name"],
"repository": manifest["repository"],
"commit": observed_commit,
"license": manifest["license"],
"license_sha256_lf": license_record["sha256_lf"],
"status": "passed" if not failures else "failed",
"failures": failures,
"contracts": contracts,
}
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--rayforge", type=Path, required=True)
parser.add_argument("--meerk40t", type=Path, required=True)
parser.add_argument("--output", type=Path, required=True)
args = parser.parse_args()
results = []
for name, checkout in (("rayforge", args.rayforge), ("meerk40t", args.meerk40t)):
manifest = json.loads(
(ROOT / "upstreams" / f"{name}.json").read_text(encoding="utf-8")
)
resolved = checkout.resolve()
results.append(inspect_tree(manifest, resolved, _git_commit(resolved)))
report = {
"status": "passed" if all(item["status"] == "passed" for item in results) else "failed",
"hardware_access": False,
"upstreams": results,
}
rendered = json.dumps(report, indent=2, sort_keys=True) + "\n"
args.output.parent.mkdir(parents=True, exist_ok=True)
args.output.write_text(rendered, encoding="utf-8", newline="\n")
print(f"status={report['status']}")
return 0 if report["status"] == "passed" else 1
if __name__ == "__main__":
raise SystemExit(main())

View file

@ -21,6 +21,11 @@ def _digest(data: bytes) -> str:
return hashlib.sha256(data).hexdigest() return hashlib.sha256(data).hexdigest()
def _text_digest_lf(data: bytes) -> str:
normalized = data.decode("utf-8").replace("\r\n", "\n").replace("\r", "\n")
return hashlib.sha256(normalized.encode("utf-8")).hexdigest()
def _artifact_path(root: Path, value: str) -> Path: def _artifact_path(root: Path, value: str) -> Path:
relative = PurePosixPath(value) relative = PurePosixPath(value)
if relative.is_absolute() or ".." in relative.parts or len(relative.parts) != 1: if relative.is_absolute() or ".." in relative.parts or len(relative.parts) != 1:
@ -42,7 +47,7 @@ def _check_file(root: Path, record: dict, label: str) -> Path:
def verify(manifest_path: Path) -> dict: def verify(manifest_path: Path) -> dict:
manifest_path = manifest_path.resolve() manifest_path = manifest_path.resolve()
manifest = json.loads(manifest_path.read_text(encoding="utf-8-sig")) manifest = json.loads(manifest_path.read_text(encoding="utf-8-sig"))
if manifest.get("schema_version") != 1: if manifest.get("schema_version") != 2:
raise ValueError("unsupported checkpoint schema") raise ValueError("unsupported checkpoint schema")
if manifest.get("rayforge_commit") != UPSTREAM["commit"]: if manifest.get("rayforge_commit") != UPSTREAM["commit"]:
raise ValueError("Rayforge commit does not match the repository pin") raise ValueError("Rayforge commit does not match the repository pin")
@ -54,6 +59,13 @@ def verify(manifest_path: Path) -> dict:
artifact_root = manifest_path.parent artifact_root = manifest_path.parent
bundle = _check_file(artifact_root, manifest["bundle"], "bundle") bundle = _check_file(artifact_root, manifest["bundle"], "bundle")
installer = _check_file(artifact_root, manifest["installer"], "installer") installer = _check_file(artifact_root, manifest["installer"], "installer")
license_records = manifest.get("license_files", [])
if len(license_records) != 1 or license_records[0].get("upstream") != "Rayforge":
raise ValueError("checkpoint must include the Rayforge license sidecar")
license_path = _check_file(artifact_root, license_records[0], "Rayforge license")
expected_license_hash = UPSTREAM["license_file"]["sha256_lf"]
if _text_digest_lf(license_path.read_bytes()) != expected_license_hash:
raise ValueError("Rayforge license does not match the pinned upstream")
packaged_root = PurePosixPath(manifest["addon_packaged_root"]) packaged_root = PurePosixPath(manifest["addon_packaged_root"])
if packaged_root.is_absolute() or ".." in packaged_root.parts: if packaged_root.is_absolute() or ".." in packaged_root.parts:
raise ValueError("unsafe packaged addon root") raise ValueError("unsafe packaged addon root")
@ -70,6 +82,15 @@ def verify(manifest_path: Path) -> dict:
executable = manifest["bundle"]["path"].removesuffix(".zip") + ".exe" executable = manifest["bundle"]["path"].removesuffix(".zip") + ".exe"
if not any(path.name == executable for path in files): if not any(path.name == executable for path in files):
raise ValueError("bundle application executable is missing") raise ValueError("bundle application executable is missing")
bundle_license = PurePosixPath(license_records[0]["bundle_path"])
license_matches = [
path for path in files
if tuple(path.parts[-len(bundle_license.parts):]) == bundle_license.parts
]
if len(license_matches) != 1:
raise ValueError("Rayforge license is missing from the portable bundle")
if _text_digest_lf(archive.read(files[license_matches[0]])) != expected_license_hash:
raise ValueError("bundled Rayforge license does not match the pinned upstream")
verified_addon_files = 0 verified_addon_files = 0
for record in manifest["addon_source_files"]: for record in manifest["addon_source_files"]:
@ -100,6 +121,7 @@ def verify(manifest_path: Path) -> dict:
"verified_addon_files": verified_addon_files, "verified_addon_files": verified_addon_files,
"manual_interaction_required": True, "manual_interaction_required": True,
"installer_name": installer.name, "installer_name": installer.name,
"license_sidecar": license_path.name,
} }

View file

@ -44,6 +44,9 @@ class M1AcceptancePipelineTests(unittest.TestCase):
self.assertIn("rayforge-addon-the-pirate-ray-jcz", source) self.assertIn("rayforge-addon-the-pirate-ray-jcz", source)
self.assertIn("Compress-Archive", source) self.assertIn("Compress-Archive", source)
self.assertIn("Get-FileHash -Algorithm SHA256", source) self.assertIn("Get-FileHash -Algorithm SHA256", source)
self.assertIn("schema_version = 2", source)
self.assertIn("Rayforge-LICENSE.txt", source)
self.assertIn("license_files", source)
def test_acceptance_has_no_hardware_commands(self): def test_acceptance_has_no_hardware_commands(self):
source = ACCEPTANCE.read_text(encoding="utf-8").lower() source = ACCEPTANCE.read_text(encoding="utf-8").lower()
@ -60,6 +63,7 @@ class M1AcceptancePipelineTests(unittest.TestCase):
self.assertIn("zipfile.ZipFile", source) self.assertIn("zipfile.ZipFile", source)
self.assertIn("Rayforge commit does not match", source) self.assertIn("Rayforge commit does not match", source)
self.assertIn('"executed_binaries": False', source) self.assertIn('"executed_binaries": False', source)
self.assertIn("license sidecar", source)
self.assertNotIn("subprocess", source) self.assertNotIn("subprocess", source)

View file

@ -0,0 +1,87 @@
import copy
import hashlib
import json
import tempfile
import unittest
from pathlib import Path
from scripts.check_upstream_contracts import ROOT, inspect_tree
class UpstreamContractTests(unittest.TestCase):
def _tree(self, root: Path) -> tuple[dict, str]:
license_path = root / "LICENSE"
api_path = root / "package" / "api.py"
api_path.parent.mkdir(parents=True)
license_path.write_text("synthetic MIT text\n", encoding="utf-8")
api_path.write_text(
"VERSION = 3\nclass Registry:\n def unregister(self):\n pass\n",
encoding="utf-8",
)
commit = "a" * 40
manifest = {
"name": "Synthetic upstream",
"repository": "https://example.invalid/upstream.git",
"commit": commit,
"license": "MIT",
"license_file": {
"path": "LICENSE",
"sha256_lf": hashlib.sha256(b"synthetic MIT text\n").hexdigest(),
},
"contract_files": [{
"path": "package/api.py",
"sha256": hashlib.sha256(api_path.read_bytes()).hexdigest(),
"stability": "documented-api",
"required_symbols": ["VERSION", "Registry", "Registry.unregister"],
}],
}
return manifest, commit
def test_matching_tree_passes(self):
with tempfile.TemporaryDirectory(dir=ROOT / ".build") as directory:
root = Path(directory)
manifest, commit = self._tree(root)
result = inspect_tree(manifest, root, commit)
self.assertEqual(result["status"], "passed")
self.assertEqual(result["failures"], [])
def test_commit_hash_and_symbol_drift_fail(self):
with tempfile.TemporaryDirectory(dir=ROOT / ".build") as directory:
root = Path(directory)
manifest, _ = self._tree(root)
changed = copy.deepcopy(manifest)
changed["contract_files"][0]["required_symbols"].append("Registry.missing")
result = inspect_tree(changed, root, "b" * 40)
self.assertEqual(result["status"], "failed")
self.assertTrue(any("commit mismatch" in item for item in result["failures"]))
self.assertTrue(any("Registry.missing" in item for item in result["failures"]))
def test_license_drift_fails(self):
with tempfile.TemporaryDirectory(dir=ROOT / ".build") as directory:
root = Path(directory)
manifest, commit = self._tree(root)
(root / "LICENSE").write_text("changed\n", encoding="utf-8")
result = inspect_tree(manifest, root, commit)
self.assertEqual(result["status"], "failed")
self.assertTrue(any("license hash mismatch" in item for item in result["failures"]))
def test_real_manifests_pin_mit_license_and_stability(self):
for name in ("rayforge", "meerk40t"):
manifest = json.loads(
(ROOT / "upstreams" / f"{name}.json").read_text(encoding="utf-8")
)
self.assertEqual(manifest["license"], "MIT")
self.assertEqual(len(manifest["commit"]), 40)
self.assertEqual(len(manifest["license_file"]["sha256_lf"]), 64)
self.assertTrue(manifest["contract_files"])
self.assertTrue(all(item["stability"] for item in manifest["contract_files"]))
notice = ROOT / "third_party" / "licenses" / f"{manifest['name']}-LICENSE.txt"
normalized = notice.read_text(encoding="utf-8").replace("\r\n", "\n").replace("\r", "\n")
self.assertEqual(
hashlib.sha256(normalized.encode("utf-8")).hexdigest(),
manifest["license_file"]["sha256_lf"],
)
if __name__ == "__main__":
unittest.main()

View file

@ -19,17 +19,28 @@ class WindowsCheckpointVerifierTests(unittest.TestCase):
"rayforge-addon.yaml": b"name: the_pirate_ray_jcz\n", "rayforge-addon.yaml": b"name: the_pirate_ray_jcz\n",
"pirate_ray_jcz/worker.py": b"TRANSPORT = None\n", "pirate_ray_jcz/worker.py": b"TRANSPORT = None\n",
} }
upstream = json.loads(
(ROOT / "upstreams" / "rayforge.json").read_text(encoding="utf-8")
)
license_data = (ROOT / "third_party" / "licenses" / "Rayforge-LICENSE.txt").read_bytes()
self.assertEqual(hashlib.sha256(license_data).hexdigest(), upstream["license_file"]["sha256_lf"])
bundle = root / bundle_name bundle = root / bundle_name
with zipfile.ZipFile(bundle, "w") as archive: with zipfile.ZipFile(bundle, "w") as archive:
archive.writestr(f"{bundle_name[:-4]}/{executable}", b"synthetic") archive.writestr(f"{bundle_name[:-4]}/{executable}", b"synthetic")
for relative, data in addon_files.items(): for relative, data in addon_files.items():
archive.writestr(f"{bundle_name[:-4]}/_internal/{ADDON_ROOT}/{relative}", data) archive.writestr(f"{bundle_name[:-4]}/_internal/{ADDON_ROOT}/{relative}", data)
archive.writestr(
f"{bundle_name[:-4]}/THIRD_PARTY_LICENSES/Rayforge-LICENSE.txt",
license_data,
)
if unsafe_member: if unsafe_member:
archive.writestr(unsafe_member, b"unsafe") archive.writestr(unsafe_member, b"unsafe")
installer = root / f"{bundle_name[:-4]}-installer.exe" installer = root / f"{bundle_name[:-4]}-installer.exe"
installer.write_bytes(b"synthetic installer, never executed") installer.write_bytes(b"synthetic installer, never executed")
license_sidecar = root / "Rayforge-LICENSE.txt"
license_sidecar.write_bytes(license_data)
manifest = { manifest = {
"schema_version": 1, "schema_version": 2,
"checkpoint": "Rayforge Windows no-device bundle", "checkpoint": "Rayforge Windows no-device bundle",
"rayforge_commit": UPSTREAM["commit"], "rayforge_commit": UPSTREAM["commit"],
"addon": "the_pirate_ray_jcz", "addon": "the_pirate_ray_jcz",
@ -50,6 +61,13 @@ class WindowsCheckpointVerifierTests(unittest.TestCase):
"sha256": hashlib.sha256(installer.read_bytes()).hexdigest(), "sha256": hashlib.sha256(installer.read_bytes()).hexdigest(),
"bytes": installer.stat().st_size, "bytes": installer.stat().st_size,
}, },
"license_files": [{
"upstream": "Rayforge",
"path": license_sidecar.name,
"bundle_path": "THIRD_PARTY_LICENSES/Rayforge-LICENSE.txt",
"sha256": hashlib.sha256(license_data).hexdigest(),
"bytes": len(license_data),
}],
"manual_interaction_required": True, "manual_interaction_required": True,
} }
path = root / "rayforge-windows-checkpoint.json" path = root / "rayforge-windows-checkpoint.json"
@ -86,6 +104,14 @@ class WindowsCheckpointVerifierTests(unittest.TestCase):
with self.assertRaisesRegex(ValueError, "one filename"): with self.assertRaisesRegex(ValueError, "one filename"):
verify(manifest) verify(manifest)
def test_missing_license_sidecar_is_rejected(self):
with tempfile.TemporaryDirectory(dir=ROOT / ".build") as directory:
manifest = self._checkpoint(Path(directory))
data = json.loads(manifest.read_text(encoding="utf-8"))
(manifest.parent / data["license_files"][0]["path"]).unlink()
with self.assertRaises(FileNotFoundError):
verify(manifest)
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()

15
third_party/README.md vendored Normal file
View file

@ -0,0 +1,15 @@
# Third-party notices
This directory contains exact license notices for pinned upstream projects
used by the offline research and integration work. It does not vendor their
source code.
| Project | Pinned revision | License notice |
| --- | --- | --- |
| Rayforge | `c28918c87f8273dd1d244f5a057addc192807fa5` | `licenses/Rayforge-LICENSE.txt` |
| MeerK40t | `d511944e08788e88077727f682957759884ff5ca` | `licenses/MeerK40t-LICENSE.txt` |
The expected UTF-8/LF-normalized hashes and source repositories are recorded in
`upstreams/rayforge.json` and `upstreams/meerk40t.json`. Inclusion of a notice
does not mean upstream source is included in The Pirate Ray, nor does it select
a license for The Pirate Ray's own code.

View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2021 meerk40t
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -0,0 +1,17 @@
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

27
upstreams/meerk40t.json Normal file
View file

@ -0,0 +1,27 @@
{
"name": "MeerK40t",
"repository": "https://github.com/meerk40t/meerk40t.git",
"commit": "d511944e08788e88077727f682957759884ff5ca",
"license": "MIT",
"license_file": {
"path": "LICENSE",
"sha256_lf": "b0cb1df6963e70b7dc3735244360ef6686c4d2aa6288b326a0f7c93014f39cd6",
"copyright_notice_present": true,
"notice": "Copyright (c) 2021 meerk40t"
},
"contract_files": [
{
"path": "meerk40t/balormk/controller.py",
"sha256": "257f42e1b48b7cff45df63e2ad6db4becb87140c61a1e04f1168a5edabc11fcf",
"stability": "application-internal-reference",
"required_symbols": ["GalvoController", "GalvoController.connect_if_needed", "GalvoController.send", "listEndOfList", "listMarkTo", "listQSwitchPeriod"]
},
{
"path": "meerk40t/balormk/mock_connection.py",
"sha256": "6c5f361a2474bc5da2660809029ee01f6f8723fe87e7cceb79ce69e80872aad2",
"stability": "application-internal-test-seam",
"required_symbols": ["MockConnection", "MockConnection.set_implied_response"]
}
],
"purpose": "Pinned BalorMK reference and forced-mock oracle for offline JCZ validation"
}

View file

@ -3,5 +3,42 @@
"repository": "https://github.com/barebaric/rayforge.git", "repository": "https://github.com/barebaric/rayforge.git",
"commit": "c28918c87f8273dd1d244f5a057addc192807fa5", "commit": "c28918c87f8273dd1d244f5a057addc192807fa5",
"license": "MIT", "license": "MIT",
"license_file": {
"path": "LICENSE",
"sha256_lf": "f9c4c77baa3828004ee54b8a4f2db2e88ed44a6237a493965bf551fac0fcb62d",
"copyright_notice_present": false
},
"contract_files": [
{
"path": "rayforge/core/hooks.py",
"sha256": "e9f488361f120a848e89dd25eb122f9aedfb2313bfce8e60f1d252d92df7cfdc",
"stability": "documented-addon-api",
"required_symbols": ["MINIMUM_API_VERSION", "PLUGIN_API_VERSION", "RayforgeSpecs.on_unload", "RayforgeSpecs.register_machines"]
},
{
"path": "rayforge/machine/driver/__init__.py",
"sha256": "06e733b8814118e3b3bfda28a6b2d6d157c5562802bd5a2294574f3b305488ef",
"stability": "internal-registry",
"required_symbols": ["drivers", "driver_by_classname", "register_driver", "get_driver_cls"]
},
{
"path": "rayforge/machine/driver/driver.py",
"sha256": "8114ec5d2cf63a4eb98eea3db68d612fb00f1ca7add89f433c8f90758f103a5e",
"stability": "imported-host-interface",
"required_symbols": ["Driver"]
},
{
"path": "rayforge/pipeline/encoder/base.py",
"sha256": "b18f87dc57655379ebdbce4c64b0472b589c7182b3a20423fa93ef22c9393734",
"stability": "imported-host-interface",
"required_symbols": ["EncodedOutput", "MachineCodeOpMap", "OpsEncoder"]
},
{
"path": "rayforge/addon_mgr/addon_manager.py",
"sha256": "6f734a0246f35a7f18d8e4fa60b38f66193d9a5ece0982829cc269133e6ddd0b",
"stability": "host-lifecycle-implementation",
"required_symbols": ["AddonManager", "AddonManager._unregister_addon_items"]
}
],
"purpose": "Pinned host application for offline reuse and JCZ addon validation" "purpose": "Pinned host application for offline reuse and JCZ addon validation"
} }