Skip to content
Open
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
2 changes: 2 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,5 @@
web/src/generated/** text eol=lf
# The module is binary: no line-ending conversion.
web/src/generated/wasm/*.wasm binary
# Preserve the pinned unified diff bytes, including required blank context lines.
web/engine-patches/local-zone-purge.patch -text -whitespace
2 changes: 2 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ jobs:
cache-dependency-path: web/package-lock.json
- run: npm ci
- run: node scripts/verify-engine.mjs
- name: Test engine build provenance
run: node --test scripts/engine-source.test.mjs
- name: Check foundation
if: hashFiles('web/index.html') == ''
run: npm run check:data
Expand Down
1,222 changes: 1,222 additions & 0 deletions web/engine-patches/local-zone-purge.patch

Large diffs are not rendered by default.

26 changes: 19 additions & 7 deletions web/engine-pin.json
Original file line number Diff line number Diff line change
@@ -1,14 +1,26 @@
{
"engineRepository": "https://github.com/orbyn-dev/orbyn-engine",
"engineCommit": "f61b16de7ffa23bce98aae57a6781b6e732f925b",
"artifactRepository": "https://github.com/orbyn-dev/orbyn-planner",
"artifactCommit": "c2d57f8ce673f3f9fcfcfb60ce3dde9aae1d0d16",
"artifactPath": "web/src/generated/wasm",
"license": "MIT OR Apache-2.0",
"sha256": {
"orbyn_wasm.js": "c6391fc3e3898c277615bc32d7ded270b35e78be44d69d69ecf45e6a91a3fc15",
"orbyn_wasm.d.ts": "deb9009a1d83d351b7a3b0c45dda37bb7c25f3689bd3e122d4bfa30c3d58e06b",
"orbyn_wasm_bg.wasm": "b5cf49fb1aff449a8e320fe6e8824f49532cbd29a46823219f66cdf42c0343c5",
"orbyn_wasm_bg.wasm.d.ts": "5a3b393bf533006638e9e4b9df120dc1c7f43d4c1720c808d39fc36ee18c8a23"
"orbyn_wasm.js": "bdfeec85768336b5400faa8c412d13c9d389b1629b513b7d1130263b86abe7ec",
"orbyn_wasm.d.ts": "73fb363678725e45127abe3cc33b290754e9966a73d790bad6a6ed86e314bd47",
"orbyn_wasm_bg.wasm": "7015f110011e70cd9e830012e1d8e1b3d76698ac588dda76086b58592a7c49bb",
"orbyn_wasm_bg.wasm.d.ts": "d0c8bfe530013c5182a0333fbf07b612961eb9b969479e366a3721a78d74f031"
},
"enginePatch": {
"path": "engine-patches/local-zone-purge.patch",
"sha256": "d77ee7f2fc78fe58640a9dd26c4162246b1333ae391b2c24c0db25be8f94629f",
"files": [
"crates/orbyn-client/src/lib.rs",
"crates/orbyn-wasm/README.md",
"crates/orbyn-wasm/src/lib.rs",
"crates/orbyn-wasm/src/tests.rs",
"docs/architecture.md"
]
},
"build": {
"rustc": "rustc 1.99.1 (58cf8f9c3 2026-10-13)",
"wasmBindgen": "0.2.130"
}
}
163 changes: 121 additions & 42 deletions web/scripts/build-engine.mjs
Original file line number Diff line number Diff line change
@@ -1,51 +1,130 @@
import { readFile } from "node:fs/promises";
import { fileURLToPath } from "node:url";
import { fileURLToPath, pathToFileURL } from "node:url";
import { spawnSync } from "node:child_process";
const root = new URL("../", import.meta.url);
const pin = JSON.parse(
await readFile(new URL("engine-pin.json", root), "utf8"),
);
const source = process.argv[2];
if (!source)
throw new Error(
"Usage: node scripts/build-engine.mjs <pinned-engine-checkout> (requires Cargo and matching wasm-bindgen).",
);
const check = spawnSync("git", ["rev-parse", "HEAD"], {
cwd: source,
encoding: "utf8",
});
if (check.status !== 0 || check.stdout.trim() !== pin.engineCommit)
throw new Error("Engine checkout must match engine-pin.json.");
for (const [command, args] of [
[
"cargo",
import { resolve, join } from "node:path";
import { homedir } from "node:os";
import { verifyEngineSource } from "./engine-source.mjs";
export async function buildEngine({
source,
root,
pin,
environment = process.env,
run = spawnSync,
}) {
await verifyEngineSource(source, root, pin);
const bindgen = environment.ORBYN_WASM_BINDGEN ?? "wasm-bindgen";
const cargo = environment.ORBYN_CARGO ?? "cargo";
const compiler = environment.ORBYN_RUSTC ?? environment.RUSTC ?? "rustc";
if (environment.RUSTC_WRAPPER || environment.RUSTC_WORKSPACE_WRAPPER)
throw new Error("Engine build does not accept compiler wrappers.");
// Cargo gives encoded flags priority over other environment flags. Refuse
// ambiguous inputs instead of silently changing caller-supplied build options.
for (const [key, value] of Object.entries(environment)) {
if (
value &&
(key === "RUSTFLAGS" ||
key === "CARGO_ENCODED_RUSTFLAGS" ||
key === "CARGO_BUILD_RUSTFLAGS" ||
/^CARGO_TARGET_.+_RUSTFLAGS$/.test(key))
)
throw new Error(
"Engine build does not accept externally supplied Rust flags.",
);
}
const userHome = environment.USERPROFILE || environment.HOME || homedir();
const roots = [
[
resolve(source, environment.CARGO_HOME || join(userHome, ".cargo")),
"/orbyn-build/cargo",
],
[
resolve(source, environment.RUSTUP_HOME || join(userHome, ".rustup")),
"/orbyn-build/rustup",
],
[resolve(source), "/orbyn-build/engine"],
];
const remaps = [];
for (const [path, replacement] of roots) {
if (/[=\x1f]/.test(path))
throw new Error(
"Engine build paths cannot contain '=' or the flags separator.",
);
for (const spelling of new Set([path, path.replaceAll("\\", "/")]))
remaps.push(`--remap-path-prefix=${spelling}=${replacement}`);
}
// Cargo must receive the same compiler that was checked, regardless of its own toolchain selection.
const env = {
...environment,
RUSTC: compiler,
CARGO_ENCODED_RUSTFLAGS: remaps.join("\x1f"),
};
if (pin.build) {
for (const [command, expected] of [
[bindgen, `wasm-bindgen ${pin.build.wasmBindgen}`],
[compiler, pin.build.rustc],
]) {
const result = run(command, ["--version"], {
cwd: source,
encoding: "utf8",
env,
});
if (
result.error ||
result.status !== 0 ||
result.stdout.trim() !== expected
)
throw new Error("Engine build tool version differs from reviewed pin.");
}
}
const target = environment.CARGO_TARGET_DIR
? resolve(source, environment.CARGO_TARGET_DIR)
: resolve(source, "target");
for (const [command, args] of [
[
"build",
"--locked",
"-p",
"orbyn-wasm",
"--release",
"--target",
"wasm32-unknown-unknown",
cargo,
[
"build",
"--locked",
"-p",
"orbyn-wasm",
"--release",
"--target",
"wasm32-unknown-unknown",
],
],
],
[
"wasm-bindgen",
[
"--target",
"web",
"--out-dir",
fileURLToPath(new URL("src/generated/wasm/", root)),
"target/wasm32-unknown-unknown/release/orbyn_wasm.wasm",
bindgen,
[
"--target",
"web",
"--out-dir",
resolve(root, "src/generated/wasm"),
resolve(target, "wasm32-unknown-unknown/release/orbyn_wasm.wasm"),
],
],
],
]) {
const result = spawnSync(command, args, { cwd: source, stdio: "inherit" });
if (result.error || result.status !== 0)
]) {
const result = run(command, args, { cwd: source, stdio: "inherit", env });
if (result.error || result.status !== 0)
throw new Error(
`${command} failed; verify installed Rust target and the wasm-bindgen version in Cargo.lock.`,
);
}
}
if (
process.argv[1] &&
import.meta.url === pathToFileURL(resolve(process.argv[1])).href
) {
const root = fileURLToPath(new URL("../", import.meta.url));
const pin = JSON.parse(
await readFile(resolve(root, "engine-pin.json"), "utf8"),
);
const source = process.argv[2];
if (!source)
throw new Error(
`${command} failed; verify installed Rust target and the wasm-bindgen version in Cargo.lock.`,
"Usage: node scripts/build-engine.mjs <pinned-engine-checkout> (requires Cargo and matching wasm-bindgen).",
);
await buildEngine({ source, root, pin });
console.log(
"Built engine artifacts. Review the generated diff and update verified hashes deliberately; do not assume byte-for-byte reproducibility across Rust versions.",
);
}
console.log(
"Built engine artifacts. Review the generated diff and update verified hashes deliberately; do not assume byte-for-byte reproducibility across Rust versions.",
);
71 changes: 71 additions & 0 deletions web/scripts/engine-source.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { readFile } from "node:fs/promises";
import { createHash } from "node:crypto";
import { spawnSync } from "node:child_process";
import { resolve, relative, isAbsolute } from "node:path";
export const sha256 = (bytes) =>
createHash("sha256").update(bytes).digest("hex");
export async function readEnginePatch(root, pin) {
if (!pin.enginePatch) return null;
const patch = pin.enginePatch;
if (
typeof patch.path !== "string" ||
!/^[a-f0-9]{64}$/.test(patch.sha256) ||
!Array.isArray(patch.files) ||
!patch.files.length ||
patch.files.some((file) => typeof file !== "string")
)
throw new Error("Invalid engine patch manifest.");
const path = resolve(root, patch.path),
local = relative(root, path);
if (local.startsWith("..") || isAbsolute(local))
throw new Error("Engine patch must be inside the web package.");
const bytes = await readFile(path);
if (sha256(bytes) !== patch.sha256)
throw new Error("Engine source patch hash mismatch.");
return bytes;
}
export async function verifyEngineSource(source, root, pin) {
const git = (args) => {
const result = spawnSync("git", args, {
cwd: source,
maxBuffer: 16 * 1024 * 1024,
});
if (result.error || result.status !== 0)
throw new Error("Cannot verify engine source checkout.");
return result.stdout;
};
if (git(["rev-parse", "HEAD"]).toString().trim() !== pin.engineCommit)
throw new Error("Engine checkout must match engine-pin.json.");
// Ignore no source files: untracked inputs could influence a build even when the tracked diff matches.
if (git(["ls-files", "--others", "--exclude-standard"]).length)
throw new Error(
"Engine source has untracked inputs. Use a clean isolated checkout.",
);
const diff = git([
"-c",
"core.autocrlf=false",
"diff",
"--binary",
"--no-ext-diff",
"--no-textconv",
"HEAD",
"--",
]);
const patch = await readEnginePatch(root, pin);
if (patch ? !diff.equals(patch) : diff.length !== 0)
throw new Error(
"Engine source changes must exactly match the reviewed patch.",
);
if (patch) {
const files = git(["diff", "--name-only", "-z", "HEAD", "--"])
.toString()
.split("\0")
.filter(Boolean)
.sort();
if (
JSON.stringify(files) !==
JSON.stringify([...pin.enginePatch.files].sort())
)
throw new Error("Engine patch file allowlist mismatch.");
}
}
Loading
Loading