Advance N-023 through N-026 audited parity

This commit is contained in:
mes123456
2026-08-12 15:23:35 -04:00
parent b3cefaeec5
commit 86136139e2
30 changed files with 5573 additions and 66 deletions

View File

@@ -13,7 +13,7 @@
"id": "web-engine-bootstrap",
"fileName": "web_engine.wasm",
"url": "/vendor/blender/web_engine.wasm",
"sha256": "acc2d6808dac4590aa3c3915495a6e24396a946b257a2d137f64ccf4c3ba6fbf",
"sha256": "7176272d381a53d559d9b6ebe7ca8653ed32199ef6ddd4e2779637954f2c9ac3",
"required": true
}
]

File diff suppressed because one or more lines are too long

Binary file not shown.

File diff suppressed because one or more lines are too long

Binary file not shown.

View File

@@ -1,15 +1,17 @@
import { gateRelease, parseReleaseManifest, serializeReleaseManifest } from "../../../protocol/release-gate";
const family = (id: string, dependencies: string[] = []) => ({ id, name: id, status: "BLOCKED", roadmapStatus: "planned", completedSlices: ["schema"], blockedSlices: ["A", "B"], acceptance: [], dependencies });
const evidence = { browser: { chromium: false }, runtime: { offline: true, workerRestart: false, opfsRecovery: false }, performance: { geometry1M: true, geometry10M: false, texture4K: false, texture8K: false, longMedia: false, simulationCache: false }, faults: { oom: false, deviceLoss: false, networkInterrupt: false, malformedBlend: true, zipBomb: true }, provenance: { license: true, sbom: false, sourceOffer: false, deterministicPackage: false } };
const base = { schemaVersion: 2, source: "docs/status/parity-ledger.json", generatedAt: "2026-08-11T00:00:00Z", families: [family("N-015"), family("N-016", ["N-015"])], evidence };
const evidenceRecord = { id: "fixture", fields: ["runtime.offline", "performance.geometry1M", "faults.malformedBlend", "faults.zipBomb", "provenance.license"], command: "fixture", exitCode: 0, durationMs: 1, output: "fixture passed", artifactSha256: ["a".repeat(64)] };
const evidence = { browser: { chromium: false }, runtime: { offline: true, workerRestart: false, opfsRecovery: false }, performance: { geometry1M: true, geometry10M: false, texture4K: false, texture8K: false, longMedia: false, simulationCache: false }, faults: { oom: false, deviceLoss: false, networkInterrupt: false, malformedBlend: true, zipBomb: true }, provenance: { license: true, sbom: false, sourceOffer: false, deterministicPackage: false }, records: [evidenceRecord] };
const base = { schemaVersion: 3, source: "docs/status/parity-ledger.json", sourceSha256: "b".repeat(64), generatedAt: "2026-08-11T00:00:00Z", families: [family("N-015"), family("N-016", ["N-015"])], evidence };
self.onmessage = () => {
const result: Record<string, unknown> = {};
try { const parsed = parseReleaseManifest(base); result.valid = [parsed.families.length, serializeReleaseManifest(base) === serializeReleaseManifest({ ...base, families: [...base.families].reverse() })]; } catch (error) { result.valid = error instanceof Error ? error.message : String(error); }
const gate = gateRelease(base); result.gate = [gate.status, gate.issues.map((issue) => issue.code)];
try { parseReleaseManifest({ ...base, schemaVersion: 1 }); } catch (error) { result.oldSchema = error instanceof Error ? error.message : String(error); }
try { parseReleaseManifest({ ...base, schemaVersion: 2 }); } catch (error) { result.oldSchema = error instanceof Error ? error.message : String(error); }
try { parseReleaseManifest({ ...base, families: [family("N-015", ["N-016"]), family("N-016", ["N-015"])] }); } catch (error) { result.cycle = error instanceof Error ? error.message : String(error); }
try { parseReleaseManifest({ ...base, families: [{ ...family("N-015"), status: "LOCAL_EXACT", completedSlices: [] }] }); } catch (error) { result.status = error instanceof Error ? error.message : String(error); }
try { parseReleaseManifest({ ...base, evidence: { ...evidence, browser: { chromium: true } } }); } catch (error) { result.unbound = error instanceof Error ? error.message : String(error); }
self.postMessage(result);
};

View File

@@ -36,8 +36,11 @@
"test:tracking-mask": "playwright test --config playwright.config.ts -g \"N-022 tracking\"",
"test:mask-main-reader": "node ../tools/web/check-mask-main-reader.mjs",
"test:asset-library": "playwright test --config playwright.config.ts -g \"N-023 asset\"",
"test:library-main-reader": "node ../tools/web/check-library-main-reader.mjs",
"test:editor-workflow": "playwright test --config playwright.config.ts -g \"N-024 editor\"",
"test:editor-main-reader": "node ../tools/web/check-editor-main-reader.mjs",
"test:scripting-platform": "playwright test --config playwright.config.ts -g \"N-025 script\"",
"test:script-main-reader": "node ../tools/web/check-script-main-reader.mjs",
"test:release-gate": "playwright test --config playwright.config.ts -g \"N-026 release\"",
"test:browser-smoke": "playwright test --config playwright.release.config.ts -g \"boots the offline engine\"",
"test:cross-browser-smoke": "npm run test:browser-smoke",
@@ -52,7 +55,10 @@
"test:nonmesh-usd-blender-roundtrip": "USD_DESKTOP_REQUIRED=1 node ../tools/web/check-nonmesh-usd-blender-roundtrip.mjs",
"test:release-performance": "node ../tools/web/check-release-performance.mjs",
"test:malicious-blends": "node ../tools/web/check-malicious-blends.mjs",
"test:release-package": "npm run build && node ../tools/web/check-release-package.mjs",
"test:release-package": "npm run build && npm run release:sbom && node ../tools/web/check-release-package.mjs",
"release:sbom": "node ../tools/web/generate-sbom.mjs",
"release:evidence": "node ../tools/web/collect-release-evidence.mjs",
"test:release-evidence": "node ../tools/web/check-release-evidence.mjs",
"release:offline": "npm run build && node ../tools/web/check-offline-reproducibility.mjs",
"diagnose:depsgraph": "node ../tools/web/diagnose-depsgraph.mjs"
},

View File

@@ -1,7 +1,7 @@
import { blockedGate, capabilityIssue, readyGate, type CapabilityGateResult } from "./capability-gates";
import type { ErrorCode } from "./error";
export const RELEASE_GATE_SCHEMA = 2 as const;
export const RELEASE_GATE_SCHEMA = 3 as const;
export type ParityStatus = "LOCAL_EXACT" | "LOCAL_BOUNDED" | "SERVER" | "BLOCKED";
export interface ParityFamilyEvidenceIR { id: string; name: string; status: ParityStatus; roadmapStatus: "completed" | "in_progress" | "planned"; completedSlices: string[]; blockedSlices: string[]; acceptance: string[]; dependencies: string[] }
export interface ReleaseEvidenceIR {
@@ -10,8 +10,10 @@ export interface ReleaseEvidenceIR {
performance: { geometry1M: boolean; geometry10M: boolean; texture4K: boolean; texture8K: boolean; longMedia: boolean; simulationCache: boolean };
faults: { oom: boolean; deviceLoss: boolean; networkInterrupt: boolean; malformedBlend: boolean; zipBomb: boolean };
provenance: { license: boolean; sbom: boolean; sourceOffer: boolean; deterministicPackage: boolean };
records: ReleaseEvidenceRecordIR[];
}
export interface ReleaseManifestIR { schemaVersion: typeof RELEASE_GATE_SCHEMA; source: string; generatedAt: string; families: ParityFamilyEvidenceIR[]; evidence: ReleaseEvidenceIR }
export interface ReleaseEvidenceRecordIR { id: string; fields: string[]; command: string; exitCode: 0; durationMs: number; output: string; artifactSha256: string[] }
export interface ReleaseManifestIR { schemaVersion: typeof RELEASE_GATE_SCHEMA; source: string; sourceSha256: string; generatedAt: string; families: ParityFamilyEvidenceIR[]; evidence: ReleaseEvidenceIR }
export interface ReleaseGateEvaluationIR { status: "READY" | "BLOCKED"; issueCodes: ErrorCode[]; missing: string[] }
export class ReleaseGateValidationError extends Error {
@@ -28,7 +30,21 @@ function strings(value: unknown, name: string, maximum = 100_000): string[] { if
function parseEvidence(value: unknown): ReleaseEvidenceIR {
if (!record(value) || !record(value.browser) || !record(value.runtime) || !record(value.performance) || !record(value.faults) || !record(value.provenance)) throw new ReleaseGateValidationError("RELEASE_MANIFEST_INVALID", "Release evidence groups are missing");
const group = (name: string, keys: readonly string[]): Record<string, boolean> => { const item = value[name]; if (!record(item)) throw new ReleaseGateValidationError("RELEASE_MANIFEST_INVALID", `evidence.${name} is invalid`); return Object.fromEntries(keys.map((key) => [key, bool(item[key], `evidence.${name}.${key}`)])); };
return { browser: group("browser", ["chromium"]) as ReleaseEvidenceIR["browser"], runtime: group("runtime", ["offline", "workerRestart", "opfsRecovery"]) as ReleaseEvidenceIR["runtime"], performance: group("performance", ["geometry1M", "geometry10M", "texture4K", "texture8K", "longMedia", "simulationCache"]) as ReleaseEvidenceIR["performance"], faults: group("faults", ["oom", "deviceLoss", "networkInterrupt", "malformedBlend", "zipBomb"]) as ReleaseEvidenceIR["faults"], provenance: group("provenance", ["license", "sbom", "sourceOffer", "deterministicPackage"]) as ReleaseEvidenceIR["provenance"] };
if (!Array.isArray(value.records) || value.records.length > 1024) throw new ReleaseGateValidationError("RELEASE_MANIFEST_INVALID", "evidence.records is invalid");
const recordIds = new Set<string>();
const records = value.records.map((item, index): ReleaseEvidenceRecordIR => {
const name = `evidence.records[${index}]`; if (!record(item)) throw new ReleaseGateValidationError("RELEASE_MANIFEST_INVALID", `${name} is invalid`);
const id = text(item.id, `${name}.id`); if (recordIds.has(id)) throw new ReleaseGateValidationError("RELEASE_MANIFEST_INVALID", `Duplicate evidence record ${id}`); recordIds.add(id);
const fields = strings(item.fields, `${name}.fields`, 64); if (new Set(fields).size !== fields.length || fields.some((field) => !/^(browser|runtime|performance|faults|provenance)\.[A-Za-z0-9]+$/.test(field))) throw new ReleaseGateValidationError("RELEASE_MANIFEST_INVALID", `${name}.fields is invalid`);
if (item.exitCode !== 0 || typeof item.durationMs !== "number" || !Number.isSafeInteger(item.durationMs) || item.durationMs < 0 || item.durationMs > 86_400_000) throw new ReleaseGateValidationError("RELEASE_MANIFEST_INVALID", `${name} did not complete successfully`);
const artifactSha256 = strings(item.artifactSha256, `${name}.artifactSha256`, 1024); if (artifactSha256.some((digest) => !/^[a-f0-9]{64}$/.test(digest))) throw new ReleaseGateValidationError("RELEASE_MANIFEST_INVALID", `${name}.artifactSha256 is invalid`);
return { id, fields, command: text(item.command, `${name}.command`, 2048), exitCode: 0, durationMs: item.durationMs, output: text(item.output, `${name}.output`, 4096), artifactSha256 };
});
const parsed = { browser: group("browser", ["chromium"]) as ReleaseEvidenceIR["browser"], runtime: group("runtime", ["offline", "workerRestart", "opfsRecovery"]) as ReleaseEvidenceIR["runtime"], performance: group("performance", ["geometry1M", "geometry10M", "texture4K", "texture8K", "longMedia", "simulationCache"]) as ReleaseEvidenceIR["performance"], faults: group("faults", ["oom", "deviceLoss", "networkInterrupt", "malformedBlend", "zipBomb"]) as ReleaseEvidenceIR["faults"], provenance: group("provenance", ["license", "sbom", "sourceOffer", "deterministicPackage"]) as ReleaseEvidenceIR["provenance"], records };
for (const [groupName, groupValues] of Object.entries(parsed).filter(([name]) => name !== "records") as Array<[string, Record<string, boolean>]>) {
for (const [key, enabled] of Object.entries(groupValues)) if (enabled && !records.some((item) => item.fields.includes(`${groupName}.${key}`))) throw new ReleaseGateValidationError("RELEASE_EVIDENCE_MISSING", `Enabled evidence ${groupName}.${key} has no successful record`);
}
return parsed;
}
function assertDependencies(families: readonly ParityFamilyEvidenceIR[]): void {
@@ -41,7 +57,8 @@ export function parseReleaseManifest(value: unknown): ReleaseManifestIR {
if (!record(value) || value.schemaVersion !== RELEASE_GATE_SCHEMA || !Array.isArray(value.families)) throw new ReleaseGateValidationError("PROTOCOL_MISMATCH", "Unsupported release manifest schema");
const ids = new Set<string>(); const families = value.families.map((item, index): ParityFamilyEvidenceIR => { const name = `families[${index}]`; if (!record(item) || !STATUSES.has(item.status as ParityStatus) || !["completed", "in_progress", "planned"].includes(item.roadmapStatus as string)) throw new ReleaseGateValidationError("RELEASE_MANIFEST_INVALID", `${name} is invalid`); const id = text(item.id, `${name}.id`); if (ids.has(id)) throw new ReleaseGateValidationError("RELEASE_MANIFEST_INVALID", `Duplicate family ${id}`); ids.add(id); const completedSlices = strings(item.completedSlices, `${name}.completedSlices`); const blockedSlices = strings(item.blockedSlices, `${name}.blockedSlices`); if (item.status !== "BLOCKED" && completedSlices.length === 0) throw new ReleaseGateValidationError("RELEASE_MANIFEST_INVALID", `${name} must declare completed slices`); return { id, name: text(item.name, `${name}.name`), status: item.status as ParityStatus, roadmapStatus: item.roadmapStatus as ParityFamilyEvidenceIR["roadmapStatus"], completedSlices, blockedSlices, acceptance: strings(item.acceptance, `${name}.acceptance`), dependencies: strings(item.dependencies, `${name}.dependencies`) }; });
assertDependencies(families);
return { schemaVersion: RELEASE_GATE_SCHEMA, source: text(value.source, "source", 2048), generatedAt: text(value.generatedAt, "generatedAt", 128), families, evidence: parseEvidence(value.evidence) };
const sourceSha256 = text(value.sourceSha256, "sourceSha256", 64); if (!/^[a-f0-9]{64}$/.test(sourceSha256)) throw new ReleaseGateValidationError("RELEASE_MANIFEST_INVALID", "sourceSha256 is invalid");
return { schemaVersion: RELEASE_GATE_SCHEMA, source: text(value.source, "source", 2048), sourceSha256, generatedAt: text(value.generatedAt, "generatedAt", 128), families, evidence: parseEvidence(value.evidence) };
}
export function evaluateReleaseManifest(value: unknown): ReleaseGateEvaluationIR {

View File

@@ -3,6 +3,9 @@ import { parseGreasePencilData, type GreasePencilDataIR } from "./grease-pencil"
import { parseCompositorGraph, type CompositorGraphIR } from "./compositor";
import { parseSequencerTimeline, type SequencerTimelineIR } from "./sequencer";
import { parseTrackingMaskProject, type TrackingMaskProjectIR } from "./tracking-mask";
import { normalizeProjectAssetPath } from "./asset-path";
import { parseEditorWorkflow, type EditorWorkflowIR } from "./editor-workflow";
import { parseScriptSourceInventory, type ScriptSourceInventoryIR } from "./scripting-platform";
export type SceneNodeType =
| "EMPTY"
@@ -474,6 +477,11 @@ export interface SceneSnapshotIR {
greasePencils?: GreasePencilDataIR[];
trackingMasks?: TrackingMaskProjectIR;
trackingMaskStatus?: "AVAILABLE" | "BLOCKED";
libraryStatus?: "AVAILABLE" | "BLOCKED";
editorWorkflow?: EditorWorkflowIR;
editorWorkflowStatus?: "AVAILABLE" | "BLOCKED";
scriptSources?: ScriptSourceInventoryIR;
scriptSourceStatus?: "AVAILABLE" | "BLOCKED";
libraries?: Array<{
id: string;
name: string;
@@ -482,6 +490,8 @@ export interface SceneSnapshotIR {
packedByteLength?: number;
status: "PACKED" | "EXTERNAL_REQUIRED";
errorCode?: "LINKED_LIBRARY_RESOURCE_REQUIRED";
dependencyIds: string[];
readOnly: true;
}>;
animations: AnimationIR[];
nlaTracks?: NlaTrackIR[];
@@ -1055,5 +1065,72 @@ export function parseSceneSnapshotIR(value: unknown): SceneSnapshotIR {
parseTrackingMaskProject(value.trackingMasks);
if (value.trackingMaskStatus !== "AVAILABLE") throw new Error("SceneIR.trackingMaskStatus must be AVAILABLE when trackingMasks is present");
}
if (value.libraryStatus !== undefined && !["AVAILABLE", "BLOCKED"].includes(value.libraryStatus as string)) {
throw new Error("SceneIR.libraryStatus is invalid");
}
if (value.libraries !== undefined) {
const libraries = value.libraries as unknown[];
if (libraries.length > 0 && value.libraryStatus !== "AVAILABLE") throw new Error("SceneIR.libraryStatus must be AVAILABLE when libraries are present");
if (libraries.length > 1024) throw new Error("SceneIR.libraries exceeds the budget");
const libraryIds = new Set<string>();
for (const [index, library] of libraries.entries()) {
if (!isRecord(library)) throw new Error(`SceneIR.libraries[${index}] must be an object`);
const id = requireString(library.id, `libraries[${index}].id`);
if (!id || id.length > 256 || libraryIds.has(id)) throw new Error(`SceneIR.libraries[${index}].id is invalid`);
libraryIds.add(id);
requireString(library.name, `libraries[${index}].name`);
const sourcePath = requireString(library.sourcePath, `libraries[${index}].sourcePath`);
if (typeof library.packed !== "boolean" || library.readOnly !== true || !Array.isArray(library.dependencyIds) ||
library.dependencyIds.length > 1024 || library.dependencyIds.some((dependency) => typeof dependency !== "string") ||
new Set(library.dependencyIds).size !== library.dependencyIds.length ||
!["PACKED", "EXTERNAL_REQUIRED"].includes(library.status as string)) {
throw new Error(`SceneIR.libraries[${index}] is invalid`);
}
let projectPath = true;
try { normalizeProjectAssetPath(sourcePath); } catch { projectPath = false; }
if (!projectPath) {
throw new Error(`SceneIR.libraries[${index}].sourcePath is outside the project`);
}
if (library.status === "PACKED" && (!library.packed || !Number.isSafeInteger(library.packedByteLength) || (library.packedByteLength as number) <= 0)) {
throw new Error(`SceneIR.libraries[${index}] packed payload is invalid`);
}
if (library.status === "EXTERNAL_REQUIRED" && (library.packed || library.errorCode !== "LINKED_LIBRARY_RESOURCE_REQUIRED")) {
throw new Error(`SceneIR.libraries[${index}] external resource state is invalid`);
}
}
for (const [index, library] of libraries.entries()) {
const dependencies = (library as Record<string, unknown>).dependencyIds as string[];
if (dependencies.some((dependency) => !libraryIds.has(dependency))) throw new Error(`SceneIR.libraries[${index}] references a missing dependency`);
}
const byId = new Map(libraries.map((library) => {
const record = library as Record<string, unknown>;
return [record.id as string, record.dependencyIds as string[]] as const;
}));
const active = new Set<string>();
const complete = new Set<string>();
const visit = (id: string): void => {
if (active.has(id)) throw new Error(`SceneIR.libraries dependency cycle includes ${id}`);
if (complete.has(id)) return;
active.add(id);
for (const dependency of byId.get(id) ?? []) visit(dependency);
active.delete(id);
complete.add(id);
};
byId.forEach((_dependencies, id) => visit(id));
}
if (value.editorWorkflowStatus !== undefined && !["AVAILABLE", "BLOCKED"].includes(value.editorWorkflowStatus as string)) {
throw new Error("SceneIR.editorWorkflowStatus is invalid");
}
if (value.editorWorkflow !== undefined) {
parseEditorWorkflow(value.editorWorkflow);
if (value.editorWorkflowStatus !== "AVAILABLE") throw new Error("SceneIR.editorWorkflowStatus must be AVAILABLE when editorWorkflow is present");
}
if (value.scriptSourceStatus !== undefined && !["AVAILABLE", "BLOCKED"].includes(value.scriptSourceStatus as string)) {
throw new Error("SceneIR.scriptSourceStatus is invalid");
}
if (value.scriptSources !== undefined) {
parseScriptSourceInventory(value.scriptSources);
if (value.scriptSourceStatus !== "AVAILABLE") throw new Error("SceneIR.scriptSourceStatus must be AVAILABLE when scriptSources is present");
}
return value as unknown as SceneSnapshotIR;
}

View File

@@ -3,7 +3,8 @@ import { blockedGate, capabilityIssue, readyGate, type CapabilityGateResult } fr
import type { ErrorCode } from "./error";
export const SCRIPTING_PLATFORM_SCHEMA = 1 as const;
export const SCRIPTING_BUDGET = { maxScripts: 1_024, maxPermissions: 64, maxDependencies: 128, maxCpuMs: 60_000, maxMemoryBytes: 512 * 1024 * 1024, maxWallMs: 300_000 } as const;
export const SCRIPT_SOURCE_SCHEMA = 1 as const;
export const SCRIPTING_BUDGET = { maxScripts: 1_024, maxPermissions: 64, maxDependencies: 128, maxCpuMs: 60_000, maxMemoryBytes: 512 * 1024 * 1024, maxWallMs: 300_000, maxSourceBytes: 1024 * 1024, maxSourceLines: 65_536 } as const;
export const SCRIPT_PERMISSIONS = ["READ_MAIN", "WRITE_MAIN", "READ_ASSET", "WRITE_ASSET", "SUBMIT_SERVER_JOB"] as const;
export type ScriptPermission = typeof SCRIPT_PERMISSIONS[number];
@@ -27,6 +28,21 @@ export interface ScriptManifestIR {
addonInstall: false;
}
export interface ScriptingManifestIR { schemaVersion: typeof SCRIPTING_PLATFORM_SCHEMA; scripts: ScriptManifestIR[] }
export interface ScriptSourceIR {
id: string;
name: string;
source: string;
sourceSha256: string;
byteLength: number;
lineCount: number;
sourcePath?: string;
internal: boolean;
moduleAutorunRequested: boolean;
readOnly: true;
executionStatus: "BLOCKED";
errorCode: "SCRIPT_POLICY_DENIED" | "SCRIPT_SANDBOX_UNAVAILABLE";
}
export interface ScriptSourceInventoryIR { schemaVersion: typeof SCRIPT_SOURCE_SCHEMA; sources: ScriptSourceIR[] }
export interface ServerScriptJobIR { scriptId: string; sourceSha256: string; inputBlendSha256: string; outputBlendSha256?: string; status: "QUEUED" | "RUNNING" | "COMPLETE" | "FAILED" }
export class ScriptingPlatformValidationError extends Error {
@@ -41,6 +57,31 @@ function digest(value: unknown, name: string): string { if (typeof value !== "st
function path(value: unknown, name: string): string { try { return normalizeProjectAssetPath(text(value, name, 2048)); } catch { throw new ScriptingPlatformValidationError("SCRIPT_MANIFEST_INVALID", `${name} is outside the project`); } }
function integer(value: unknown, name: string, minimum: number, maximum: number): number { if (typeof value !== "number" || !Number.isSafeInteger(value) || value < minimum || value > maximum) throw new ScriptingPlatformValidationError("SCRIPT_BUDGET_EXCEEDED", `${name} exceeds the budget`); return value; }
export function parseScriptSourceInventory(value: unknown): ScriptSourceInventoryIR {
if (!record(value) || value.schemaVersion !== SCRIPT_SOURCE_SCHEMA || !Array.isArray(value.sources)) throw new ScriptingPlatformValidationError("PROTOCOL_MISMATCH", "Unsupported script source inventory schema");
if (value.sources.length > SCRIPTING_BUDGET.maxScripts) throw new ScriptingPlatformValidationError("SCRIPT_BUDGET_EXCEEDED", "Script source count exceeds the budget");
const ids = new Set<string>(); let totalBytes = 0;
const sources = value.sources.map((item, index): ScriptSourceIR => {
const name = `sources[${index}]`; if (!record(item) || typeof item.source !== "string") throw new ScriptingPlatformValidationError("SCRIPT_MANIFEST_INVALID", `${name} is invalid`);
const id = text(item.id, `${name}.id`); if (!id.startsWith("text:") || ids.has(id)) throw new ScriptingPlatformValidationError("SCRIPT_MANIFEST_INVALID", `${name}.id is invalid`); ids.add(id);
const byteLength = integer(item.byteLength, `${name}.byteLength`, 0, SCRIPTING_BUDGET.maxSourceBytes); totalBytes += byteLength;
if (totalBytes > SCRIPTING_BUDGET.maxSourceBytes || new TextEncoder().encode(item.source).byteLength !== byteLength) throw new ScriptingPlatformValidationError("SCRIPT_BUDGET_EXCEEDED", `${name}.source exceeds or disagrees with the byte budget`);
const moduleAutorunRequested = item.moduleAutorunRequested === true;
if (item.readOnly !== true || item.executionStatus !== "BLOCKED" || item.internal !== (item.sourcePath === undefined) ||
item.errorCode !== (moduleAutorunRequested ? "SCRIPT_POLICY_DENIED" : "SCRIPT_SANDBOX_UNAVAILABLE")) throw new ScriptingPlatformValidationError("SCRIPT_POLICY_DENIED", `${name} execution policy is invalid`);
const sourcePath = item.sourcePath === undefined ? undefined : path(item.sourcePath, `${name}.sourcePath`);
const lineCount = integer(item.lineCount, `${name}.lineCount`, 1, SCRIPTING_BUDGET.maxSourceLines);
if (item.source.split("\n").length !== lineCount) throw new ScriptingPlatformValidationError("SCRIPT_MANIFEST_INVALID", `${name}.lineCount disagrees with source`);
return { id, name: text(item.name, `${name}.name`), source: item.source, sourceSha256: digest(item.sourceSha256, `${name}.sourceSha256`), byteLength, lineCount, sourcePath, internal: item.internal as boolean, moduleAutorunRequested, readOnly: true, executionStatus: "BLOCKED", errorCode: item.errorCode as ScriptSourceIR["errorCode"] };
});
return { schemaVersion: SCRIPT_SOURCE_SCHEMA, sources };
}
export async function verifyScriptSource(source: ScriptSourceIR): Promise<boolean> {
const digestBytes = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(source.source));
return [...new Uint8Array(digestBytes)].map((byte) => byte.toString(16).padStart(2, "0")).join("") === source.sourceSha256;
}
export function parseScriptingManifest(value: unknown): ScriptingManifestIR {
if (!record(value) || value.schemaVersion !== SCRIPTING_PLATFORM_SCHEMA || !Array.isArray(value.scripts)) throw new ScriptingPlatformValidationError("PROTOCOL_MISMATCH", "Unsupported scripting manifest schema");
if (value.scripts.length > SCRIPTING_BUDGET.maxScripts) throw new ScriptingPlatformValidationError("SCRIPT_BUDGET_EXCEEDED", "Script count exceeds the budget");

View File

@@ -690,6 +690,7 @@ test("keeps N-026 release manifest deterministic and blocks missing evidence", a
expect(result.oldSchema).toContain("PROTOCOL_MISMATCH");
expect(result.cycle).toContain("RELEASE_DEPENDENCY_CYCLE");
expect(result.status).toContain("RELEASE_MANIFEST_INVALID");
expect(result.unbound).toContain("RELEASE_EVIDENCE_MISSING");
});
test("keeps N-015 selection history bounded and rejects stale raycast hits", async ({ page }) => {