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

@@ -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");