import { normalizeProjectAssetPath } from "./asset-path"; import { blockedGate, capabilityIssue, readyGate, type CapabilityGateResult } from "./capability-gates"; import type { ErrorCode } from "./error"; export const SCRIPTING_PLATFORM_SCHEMA = 1 as const; export const SCRIPT_SOURCE_SCHEMA = 1 as const; export const SCRIPT_EXECUTION_AUDIT_SCHEMA = 1 as const; export const SCRIPT_EXECUTION_AUDIT_LOG_SCHEMA = 1 as const; export const SCRIPT_TRUST_POLICY_SCHEMA = 1 as const; export const SCRIPT_SANDBOX_SCOPE_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, maxAuditEntries: 65_536 } as const; export const SCRIPT_TRUST_POLICY_BUDGET = { maxKeys: 1_024, maxClockSkewMs: 300_000 } as const; export const SCRIPT_SANDBOX_BUDGET = { maxCpuMs: 60_000, maxWallMs: 300_000, maxMemoryBytes: 512 * 1024 * 1024, maxMessageBytes: 1 * 1024 * 1024, maxOutputBytes: 16 * 1024 * 1024 } as const; export const SCRIPT_HOST_CALL_SCHEMA = 1 as const; export const SCRIPT_HOST_CALLS = ["READ_MAIN", "READ_ASSET", "WRITE_MAIN", "WRITE_ASSET", "SUBMIT_SERVER_JOB"] as const; export const SCRIPT_SANDBOX_JOB_SCHEMA = 1 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]; export interface ScriptDependencyIR { id: string; sourceSha256: string; sourcePath: string } export interface ScriptManifestIR { id: string; name: string; entryPath: string; sourceByteLength: number; sourceSha256: string; publisher: string; signature: string; keyId: string; permissions: ScriptPermission[]; dependencies: ScriptDependencyIR[]; module: false; cpuMs: number; memoryBytes: number; wallMs: number; network: false; autorun: false; driverExpressions: false; addonInstall: false; } export interface ScriptingManifestIR { schemaVersion: typeof SCRIPTING_PLATFORM_SCHEMA; scripts: ScriptManifestIR[] } export interface ScriptTrustKeyIR { keyId: string; publisher: string; algorithm: "ED25519"; publicKey: string; status: "ACTIVE" | "REVOKED"; notBefore: string; notAfter: string; revokedAt?: string; replaces?: string; } export interface ScriptTrustPolicyIR { schemaVersion: typeof SCRIPT_TRUST_POLICY_SCHEMA; issuer: string; issuedAt: string; expiresAt: string; maxClockSkewMs: number; keys: ScriptTrustKeyIR[]; } export interface ScriptSignerResolutionIR { status: "ELIGIBLE" | "BLOCKED"; keyId: string; publisher: string; trust: "ACTIVE" | "REVOKED" | "NOT_FOUND" | "PUBLISHER_MISMATCH" | "POLICY_NOT_YET_VALID" | "POLICY_EXPIRED" | "KEY_NOT_YET_VALID" | "KEY_EXPIRED"; cryptographicVerification: "REQUIRED"; } export interface ScriptSignatureVerificationIR { status: "VERIFIED" | "BLOCKED"; code: "SCRIPT_SIGNATURE_VERIFIED" | "SCRIPT_SIGNATURE_INVALID" | "SCRIPT_POLICY_DENIED"; keyId: string; sourceSha256: string; inputSha256: string; } export interface ScriptPermissionResolutionIR { status: "ALLOWED" | "BLOCKED"; code: "SCRIPT_PERMISSIONS_ALLOWED" | "SCRIPT_POLICY_DENIED"; scriptId: string; declared: ScriptPermission[]; requested: ScriptPermission[]; granted: ScriptPermission[]; } export interface ScriptSandboxScopeIR { schemaVersion: typeof SCRIPT_SANDBOX_SCOPE_SCHEMA; dom: false; hostWorker: false; opfs: false; indexedDB: false; network: false; } export interface ScriptSandboxBudgetIR { schemaVersion: typeof SCRIPT_SANDBOX_SCOPE_SCHEMA; cpuMs: number; wallMs: number; memoryBytes: number; maxMessageBytes: number; maxOutputBytes: number; } export type ScriptHostCallName = typeof SCRIPT_HOST_CALLS[number]; export type ScriptHostCallParameters = | { revision: number } | { path: string; expectedSha256: string } | { revision: number; operation: string; payload: Record } | { path: string; byteLength: number; sha256: string } | { inputBlendSha256: string; settingsSha256: string }; export interface ScriptHostCallIR { schemaVersion: typeof SCRIPT_HOST_CALL_SCHEMA; requestId: string; scriptId: string; call: ScriptHostCallName; permission: ScriptPermission; parameters: ScriptHostCallParameters; execution: "DISABLED"; } export interface ScriptSandboxJobIR { schemaVersion: typeof SCRIPT_SANDBOX_JOB_SCHEMA; jobId: string; workerGeneration: number; baseRevision: number; mainRevisionBefore: number; mainRevisionAfter: number; status: "CRASHED" | "TIMED_OUT" | "CANCELLED"; errorCode: "SCRIPT_SANDBOX_CRASHED" | "SCRIPT_SANDBOX_TIMEOUT" | "SCRIPT_SANDBOX_CANCELLED"; temporaryBytes: 0; publishedResults: 0; lateResults: 0; committed: false; execution: "DISABLED"; } 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 interface ScriptExecutionAuditIR { schemaVersion: typeof SCRIPT_EXECUTION_AUDIT_SCHEMA; requestId: string; requestedAt: string; scriptId: string; sourceSha256: string; manifestSha256: string; permissions: ScriptPermission[]; budget: { cpuMs: number; memoryBytes: number; wallMs: number }; approvedKey: boolean; decision: "DENY"; reason: "SCRIPT_SIGNATURE_INVALID" | "SCRIPT_SANDBOX_UNAVAILABLE"; requestSha256: string; } export interface ScriptExecutionAuditLogEntryIR { sequence: number; previousEntrySha256: string | null; audit: ScriptExecutionAuditIR; entrySha256: string; } export interface ScriptExecutionAuditLogIR { schemaVersion: typeof SCRIPT_EXECUTION_AUDIT_LOG_SCHEMA; entries: ScriptExecutionAuditLogEntryIR[]; } export class ScriptingPlatformValidationError extends Error { readonly code: ErrorCode; constructor(code: ErrorCode, message: string) { super(`${code}: ${message}`); this.name = "ScriptingPlatformValidationError"; this.code = code; } } const SHA256 = /^[a-f0-9]{64}$/; const HEX_SIGNATURE = /^[a-f0-9]{128}$/; function record(value: unknown): value is Record { return typeof value === "object" && value !== null && !Array.isArray(value); } function text(value: unknown, name: string, maximum = 256): string { if (typeof value !== "string" || value.length === 0 || value.length > maximum) throw new ScriptingPlatformValidationError("SCRIPT_MANIFEST_INVALID", `${name} is invalid`); return value; } function digest(value: unknown, name: string): string { if (typeof value !== "string" || !SHA256.test(value)) throw new ScriptingPlatformValidationError("SCRIPT_MANIFEST_INVALID", `${name} must be a lowercase SHA-256 digest`); return value; } 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; } function stableJSON(value: unknown): string { if (Array.isArray(value)) return `[${value.map(stableJSON).join(",")}]`; if (record(value)) return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${stableJSON(value[key])}`).join(",")}}`; return JSON.stringify(value); } async function sha256(value: string): Promise { const bytes = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(value)); return [...new Uint8Array(bytes)].map((byte) => byte.toString(16).padStart(2, "0")).join(""); } function canonicalAuditRequest(audit: Omit): Omit { return { ...audit, permissions: [...audit.permissions].sort(), budget: { ...audit.budget } }; } function isoDate(value: unknown, name: string): string { const result = text(value, name, 64); const date = new Date(result); if (!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/.test(result) || !Number.isFinite(date.getTime()) || date.toISOString() !== result) throw new ScriptingPlatformValidationError("SCRIPT_MANIFEST_INVALID", `${name} is invalid`); return result; } function auditRequestId(value: unknown, name: string): string { const result = text(value, name); if (!/^[-A-Za-z0-9:_./]{1,256}$/.test(result)) throw new ScriptingPlatformValidationError("SCRIPT_MANIFEST_INVALID", `${name} is invalid`); return result; } function canonicalManifest(manifest: ScriptingManifestIR): ScriptingManifestIR { return { schemaVersion: manifest.schemaVersion, scripts: manifest.scripts .map((script) => ({ ...script, permissions: [...script.permissions].sort(), dependencies: script.dependencies.map((dependency) => ({ ...dependency })).sort((a, b) => a.id < b.id ? -1 : a.id > b.id ? 1 : 0) })) .sort((a, b) => a.id < b.id ? -1 : a.id > b.id ? 1 : 0), }; } export function canonicalizeScriptingManifest(value: unknown): ScriptingManifestIR { return canonicalManifest(parseScriptingManifest(value)); } export function serializeScriptingManifest(value: unknown): string { return stableJSON(canonicalizeScriptingManifest(value)); } export function serializeScriptSignatureInput(value: unknown, scriptId: string): string { const parsed = canonicalizeScriptingManifest(value); const script = parsed.scripts.find((item) => item.id === scriptId); if (!script) throw new ScriptingPlatformValidationError("SCRIPT_MANIFEST_INVALID", `Unknown script ${scriptId}`); return stableJSON({ schemaVersion: SCRIPTING_PLATFORM_SCHEMA, script: { ...script, signature: "" } }); } export function parseScriptTrustPolicy(value: unknown): ScriptTrustPolicyIR { if (!record(value) || value.schemaVersion !== SCRIPT_TRUST_POLICY_SCHEMA || !Array.isArray(value.keys)) throw new ScriptingPlatformValidationError("PROTOCOL_MISMATCH", "Unsupported script trust policy schema"); const issuer = text(value.issuer, "trustPolicy.issuer", 256); const issuedAt = isoDate(value.issuedAt, "trustPolicy.issuedAt"); const expiresAt = isoDate(value.expiresAt, "trustPolicy.expiresAt"); if (expiresAt <= issuedAt) throw new ScriptingPlatformValidationError("SCRIPT_MANIFEST_INVALID", "trustPolicy.expiresAt must be after issuedAt"); const maxClockSkewMs = integer(value.maxClockSkewMs, "trustPolicy.maxClockSkewMs", 0, SCRIPT_TRUST_POLICY_BUDGET.maxClockSkewMs); if (value.keys.length > SCRIPT_TRUST_POLICY_BUDGET.maxKeys) throw new ScriptingPlatformValidationError("SCRIPT_BUDGET_EXCEEDED", "Trust policy key count exceeds the budget"); const keyIds = new Set(); const keys = value.keys.map((item, index): ScriptTrustKeyIR => { const name = `trustPolicy.keys[${index}]`; if (!record(item)) throw new ScriptingPlatformValidationError("SCRIPT_MANIFEST_INVALID", `${name} is invalid`); const keyId = text(item.keyId, `${name}.keyId`, 128); if (keyIds.has(keyId)) throw new ScriptingPlatformValidationError("SCRIPT_MANIFEST_INVALID", `${name}.keyId is duplicated`); keyIds.add(keyId); if (item.algorithm !== "ED25519" || typeof item.publicKey !== "string" || !/^[a-f0-9]{64}$/.test(item.publicKey)) throw new ScriptingPlatformValidationError("SCRIPT_SIGNATURE_INVALID", `${name} has an unsupported public key`); const publisher = text(item.publisher, `${name}.publisher`, 256); const notBefore = isoDate(item.notBefore, `${name}.notBefore`); const notAfter = isoDate(item.notAfter, `${name}.notAfter`); if (notAfter <= notBefore) throw new ScriptingPlatformValidationError("SCRIPT_MANIFEST_INVALID", `${name} validity window is invalid`); if (item.status !== "ACTIVE" && item.status !== "REVOKED") throw new ScriptingPlatformValidationError("SCRIPT_POLICY_DENIED", `${name}.status is invalid`); const revokedAt = item.revokedAt === undefined ? undefined : isoDate(item.revokedAt, `${name}.revokedAt`); if (item.status === "REVOKED" ? revokedAt === undefined : revokedAt !== undefined) throw new ScriptingPlatformValidationError("SCRIPT_POLICY_DENIED", `${name}.revokedAt does not match status`); if (revokedAt !== undefined && (revokedAt < notBefore || revokedAt > notAfter)) throw new ScriptingPlatformValidationError("SCRIPT_MANIFEST_INVALID", `${name}.revokedAt is outside the key validity window`); const replaces = item.replaces === undefined ? undefined : text(item.replaces, `${name}.replaces`, 128); return { keyId, publisher, algorithm: "ED25519", publicKey: item.publicKey, status: item.status, notBefore, notAfter, ...(revokedAt === undefined ? {} : { revokedAt }), ...(replaces === undefined ? {} : { replaces }) }; }); const byId = new Map(keys.map((key) => [key.keyId, key])); const active = new Set(); const complete = new Set(); const visit = (keyId: string): void => { if (active.has(keyId)) throw new ScriptingPlatformValidationError("SCRIPT_MANIFEST_INVALID", `Trust key rotation cycle includes ${keyId}`); if (complete.has(keyId)) return; const key = byId.get(keyId); if (!key) return; active.add(keyId); if (key.replaces !== undefined) { const predecessor = byId.get(key.replaces); if (!predecessor) throw new ScriptingPlatformValidationError("SCRIPT_MANIFEST_INVALID", `${keyId} replaces missing key ${key.replaces}`); if (predecessor.publisher !== key.publisher) throw new ScriptingPlatformValidationError("SCRIPT_POLICY_DENIED", `${keyId} crosses publisher rotation boundary`); visit(predecessor.keyId); } active.delete(keyId); complete.add(keyId); }; keys.forEach((key) => visit(key.keyId)); return { schemaVersion: SCRIPT_TRUST_POLICY_SCHEMA, issuer, issuedAt, expiresAt, maxClockSkewMs, keys }; } export function canonicalizeScriptTrustPolicy(value: unknown): ScriptTrustPolicyIR { const parsed = parseScriptTrustPolicy(value); return { ...parsed, keys: [...parsed.keys].sort((a, b) => a.keyId < b.keyId ? -1 : a.keyId > b.keyId ? 1 : 0) }; } export function serializeScriptTrustPolicy(value: unknown): string { return stableJSON(canonicalizeScriptTrustPolicy(value)); } export function resolveScriptSigner(manifest: unknown, scriptId: string, policy: unknown, at: string): ScriptSignerResolutionIR { const parsedManifest = parseScriptingManifest(manifest); const script = parsedManifest.scripts.find((item) => item.id === scriptId); if (!script) throw new ScriptingPlatformValidationError("SCRIPT_MANIFEST_INVALID", `Unknown script ${scriptId}`); const parsedPolicy = parseScriptTrustPolicy(policy); const key = parsedPolicy.keys.find((item) => item.keyId === script.keyId); const blocked = (trust: ScriptSignerResolutionIR["trust"]): ScriptSignerResolutionIR => ({ status: "BLOCKED", keyId: script.keyId, publisher: script.publisher, trust, cryptographicVerification: "REQUIRED" }); if (!key) return blocked("NOT_FOUND"); const requestedAt = isoDate(at, "signer.at"); const policyStart = new Date(parsedPolicy.issuedAt).getTime() - parsedPolicy.maxClockSkewMs; const policyEnd = new Date(parsedPolicy.expiresAt).getTime() + parsedPolicy.maxClockSkewMs; const requestedTime = new Date(requestedAt).getTime(); if (requestedTime < policyStart) return blocked("POLICY_NOT_YET_VALID"); if (requestedTime > policyEnd) return blocked("POLICY_EXPIRED"); if (key.publisher !== script.publisher) return blocked("PUBLISHER_MISMATCH"); if (key.status === "REVOKED") return blocked("REVOKED"); if (requestedAt < key.notBefore) return blocked("KEY_NOT_YET_VALID"); if (requestedAt > key.notAfter) return blocked("KEY_EXPIRED"); return { status: "ELIGIBLE", keyId: key.keyId, publisher: key.publisher, trust: "ACTIVE", cryptographicVerification: "REQUIRED" }; } function hexBytes(value: string): Uint8Array { const bytes = new Uint8Array(value.length / 2); for (let index = 0; index < bytes.length; index += 1) bytes[index] = Number.parseInt(value.slice(index * 2, index * 2 + 2), 16); return bytes; } export async function verifyScriptManifestSignature(manifest: unknown, scriptId: string, policy: unknown, at: string): Promise { const parsedManifest = parseScriptingManifest(manifest); const script = parsedManifest.scripts.find((item) => item.id === scriptId); if (!script) throw new ScriptingPlatformValidationError("SCRIPT_MANIFEST_INVALID", `Unknown script ${scriptId}`); const resolution = resolveScriptSigner(parsedManifest, scriptId, policy, at); const input = serializeScriptSignatureInput(parsedManifest, scriptId); const inputSha256 = await sha256(input); if (resolution.status !== "ELIGIBLE") return { status: "BLOCKED", code: "SCRIPT_POLICY_DENIED", keyId: script.keyId, sourceSha256: script.sourceSha256, inputSha256 }; const signer = parseScriptTrustPolicy(policy).keys.find((key) => key.keyId === script.keyId); if (!signer) return { status: "BLOCKED", code: "SCRIPT_SIGNATURE_INVALID", keyId: script.keyId, sourceSha256: script.sourceSha256, inputSha256 }; try { const key = await crypto.subtle.importKey("raw", hexBytes(signer.publicKey) as unknown as BufferSource, { name: "Ed25519" }, false, ["verify"]); const valid = await crypto.subtle.verify("Ed25519", key, hexBytes(script.signature) as unknown as BufferSource, new TextEncoder().encode(input) as unknown as BufferSource); return { status: valid ? "VERIFIED" : "BLOCKED", code: valid ? "SCRIPT_SIGNATURE_VERIFIED" : "SCRIPT_SIGNATURE_INVALID", keyId: script.keyId, sourceSha256: script.sourceSha256, inputSha256 }; } catch { return { status: "BLOCKED", code: "SCRIPT_SIGNATURE_INVALID", keyId: script.keyId, sourceSha256: script.sourceSha256, inputSha256 }; } } export async function createScriptExecutionAudit( manifest: unknown, scriptId: string, approvedKeyIds: ReadonlySet, options: { requestId?: string; requestedAt?: string } = {}, ): Promise { const parsed = parseScriptingManifest(manifest); const script = parsed.scripts.find((item) => item.id === scriptId); if (!script) throw new ScriptingPlatformValidationError("SCRIPT_MANIFEST_INVALID", `Unknown script ${scriptId}`); const requestId = auditRequestId(options.requestId ?? `script-audit:${scriptId}:${Date.now()}`, "requestId"); const requestedAt = isoDate(options.requestedAt ?? new Date().toISOString(), "requestedAt"); const approvedKey = approvedKeyIds.has(script.keyId); const reason = approvedKey ? "SCRIPT_SANDBOX_UNAVAILABLE" : "SCRIPT_SIGNATURE_INVALID"; const manifestSha256 = await sha256(serializeScriptingManifest(parsed)); const request = canonicalAuditRequest({ requestId, requestedAt, scriptId, sourceSha256: script.sourceSha256, manifestSha256, permissions: [...script.permissions], budget: { cpuMs: script.cpuMs, memoryBytes: script.memoryBytes, wallMs: script.wallMs }, approvedKey, decision: "DENY", reason }); const requestSha256 = await sha256(stableJSON(request)); return Object.freeze({ schemaVersion: SCRIPT_EXECUTION_AUDIT_SCHEMA, requestId, requestedAt, scriptId, sourceSha256: script.sourceSha256, manifestSha256, permissions: Object.freeze([...script.permissions].sort()) as unknown as ScriptPermission[], budget: Object.freeze({ cpuMs: script.cpuMs, memoryBytes: script.memoryBytes, wallMs: script.wallMs }), approvedKey, decision: "DENY", reason, requestSha256, }); } export async function parseScriptExecutionAudit(value: unknown): Promise { if (!record(value) || value.schemaVersion !== SCRIPT_EXECUTION_AUDIT_SCHEMA || !Array.isArray(value.permissions) || !record(value.budget)) throw new ScriptingPlatformValidationError("SCRIPT_MANIFEST_INVALID", "Script execution audit is invalid"); const permissions = value.permissions.map((permission, index) => text(permission, `audit.permissions[${index}]`, 64) as ScriptPermission); if (permissions.length > SCRIPTING_BUDGET.maxPermissions || new Set(permissions).size !== permissions.length || permissions.some((permission) => !SCRIPT_PERMISSIONS.includes(permission)) || permissions.some((permission, index) => index > 0 && permissions[index - 1] > permission)) throw new ScriptingPlatformValidationError("SCRIPT_MANIFEST_INVALID", "Audit permissions are invalid or not canonical"); if (typeof value.approvedKey !== "boolean" || value.decision !== "DENY" || !["SCRIPT_SIGNATURE_INVALID", "SCRIPT_SANDBOX_UNAVAILABLE"].includes(value.reason as string) || value.reason !== (value.approvedKey ? "SCRIPT_SANDBOX_UNAVAILABLE" : "SCRIPT_SIGNATURE_INVALID")) throw new ScriptingPlatformValidationError("SCRIPT_POLICY_DENIED", "Audit decision is inconsistent with the default-deny policy"); const request = canonicalAuditRequest({ requestId: auditRequestId(value.requestId, "audit.requestId"), requestedAt: isoDate(value.requestedAt, "audit.requestedAt"), scriptId: text(value.scriptId, "audit.scriptId"), sourceSha256: digest(value.sourceSha256, "audit.sourceSha256"), manifestSha256: digest(value.manifestSha256, "audit.manifestSha256"), permissions, budget: { cpuMs: integer(value.budget.cpuMs, "audit.budget.cpuMs", 1, SCRIPTING_BUDGET.maxCpuMs), memoryBytes: integer(value.budget.memoryBytes, "audit.budget.memoryBytes", 1, SCRIPTING_BUDGET.maxMemoryBytes), wallMs: integer(value.budget.wallMs, "audit.budget.wallMs", 1, SCRIPTING_BUDGET.maxWallMs) }, approvedKey: value.approvedKey, decision: "DENY", reason: value.reason as ScriptExecutionAuditIR["reason"], }); const requestSha256 = digest(value.requestSha256, "audit.requestSha256"); if (await sha256(stableJSON(request)) !== requestSha256) throw new ScriptingPlatformValidationError("SCRIPT_MANIFEST_INVALID", "Audit request digest does not match its canonical content"); return { schemaVersion: SCRIPT_EXECUTION_AUDIT_SCHEMA, ...request, requestSha256 }; } export async function parseScriptExecutionAuditLog(value: unknown): Promise { if (!record(value) || value.schemaVersion !== SCRIPT_EXECUTION_AUDIT_LOG_SCHEMA || !Array.isArray(value.entries)) throw new ScriptingPlatformValidationError("PROTOCOL_MISMATCH", "Unsupported script execution audit log schema"); if (value.entries.length > SCRIPTING_BUDGET.maxAuditEntries) throw new ScriptingPlatformValidationError("SCRIPT_BUDGET_EXCEEDED", "Script audit log exceeds the entry budget"); const entries: ScriptExecutionAuditLogEntryIR[] = []; const requestIds = new Set(); for (const [index, entryValue] of value.entries.entries()) { if (!record(entryValue) || !record(entryValue.audit) || entryValue.sequence !== index + 1) throw new ScriptingPlatformValidationError("SCRIPT_MANIFEST_INVALID", `Audit log entry ${index} has an invalid sequence`); const audit = await parseScriptExecutionAudit(entryValue.audit); if (requestIds.has(audit.requestId)) throw new ScriptingPlatformValidationError("SCRIPT_MANIFEST_INVALID", `Audit request ${audit.requestId} is replayed`); if (entries.length > 0 && audit.requestedAt <= entries[entries.length - 1].audit.requestedAt) throw new ScriptingPlatformValidationError("SCRIPT_MANIFEST_INVALID", "Audit log timestamps are not strictly increasing"); const previousEntrySha256 = index === 0 ? null : entries[index - 1].entrySha256; if (entryValue.previousEntrySha256 !== previousEntrySha256) throw new ScriptingPlatformValidationError("SCRIPT_MANIFEST_INVALID", `Audit log entry ${index} breaks the hash chain`); const entrySha256 = digest(entryValue.entrySha256, `entries[${index}].entrySha256`); if (await sha256(stableJSON({ sequence: index + 1, previousEntrySha256, audit })) !== entrySha256) throw new ScriptingPlatformValidationError("SCRIPT_MANIFEST_INVALID", `Audit log entry ${index} digest does not match`); requestIds.add(audit.requestId); entries.push({ sequence: index + 1, previousEntrySha256, audit, entrySha256 }); } return { schemaVersion: SCRIPT_EXECUTION_AUDIT_LOG_SCHEMA, entries }; } export async function appendScriptExecutionAudit(logValue: unknown, auditValue: unknown): Promise { const log = await parseScriptExecutionAuditLog(logValue); const audit = await parseScriptExecutionAudit(auditValue); if (log.entries.length >= SCRIPTING_BUDGET.maxAuditEntries) throw new ScriptingPlatformValidationError("SCRIPT_BUDGET_EXCEEDED", "Script audit log exceeds the entry budget"); if (log.entries.some((entry) => entry.audit.requestId === audit.requestId)) throw new ScriptingPlatformValidationError("SCRIPT_MANIFEST_INVALID", `Audit request ${audit.requestId} is replayed`); const previous = log.entries.at(-1); if (previous && audit.requestedAt <= previous.audit.requestedAt) throw new ScriptingPlatformValidationError("SCRIPT_MANIFEST_INVALID", "Audit log timestamps must be strictly increasing"); const sequence = log.entries.length + 1; const previousEntrySha256 = previous?.entrySha256 ?? null; const entrySha256 = await sha256(stableJSON({ sequence, previousEntrySha256, audit })); return parseScriptExecutionAuditLog({ schemaVersion: SCRIPT_EXECUTION_AUDIT_LOG_SCHEMA, entries: [...log.entries, { sequence, previousEntrySha256, audit, entrySha256 }] }); } 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(); 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 { 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"); const ids = new Set(); let totalSourceBytes = 0; const scripts = value.scripts.map((item, index): ScriptManifestIR => { const name = `scripts[${index}]`; if (!record(item) || !Array.isArray(item.permissions) || !Array.isArray(item.dependencies)) throw new ScriptingPlatformValidationError("SCRIPT_MANIFEST_INVALID", `${name} is invalid`); const id = text(item.id, `${name}.id`); if (ids.has(id)) throw new ScriptingPlatformValidationError("SCRIPT_MANIFEST_INVALID", `Duplicate script ${id}`); ids.add(id); if (item.permissions.length > SCRIPTING_BUDGET.maxPermissions || item.permissions.some((permission) => !SCRIPT_PERMISSIONS.includes(permission as ScriptPermission)) || new Set(item.permissions).size !== item.permissions.length) throw new ScriptingPlatformValidationError("SCRIPT_POLICY_DENIED", `${name}.permissions are invalid or exceed the allowlist`); if (item.dependencies.length > SCRIPTING_BUDGET.maxDependencies) throw new ScriptingPlatformValidationError("SCRIPT_BUDGET_EXCEEDED", `${name}.dependencies exceed the budget`); const dependencyIds = new Set(); const dependencies = item.dependencies.map((dependency, dependencyIndex): ScriptDependencyIR => { const dependencyName = `${name}.dependencies[${dependencyIndex}]`; if (!record(dependency)) throw new ScriptingPlatformValidationError("SCRIPT_MANIFEST_INVALID", `${dependencyName} is invalid`); const dependencyId = text(dependency.id, `${dependencyName}.id`); if (dependencyIds.has(dependencyId)) throw new ScriptingPlatformValidationError("SCRIPT_MANIFEST_INVALID", `${dependencyName}.id is duplicated`); dependencyIds.add(dependencyId); return { id: dependencyId, sourceSha256: digest(dependency.sourceSha256, `${dependencyName}.sourceSha256`), sourcePath: path(dependency.sourcePath, `${dependencyName}.sourcePath`) }; }); if (item.module !== false || item.network !== false || item.autorun !== false || item.driverExpressions !== false || item.addonInstall !== false) throw new ScriptingPlatformValidationError(item.driverExpressions === true ? "DRIVER_EXECUTION_BLOCKED" : item.addonInstall === true ? "ADDON_INSTALL_BLOCKED" : "SCRIPT_POLICY_DENIED", `${name} requests a denied execution policy`); if (typeof item.signature !== "string" || !HEX_SIGNATURE.test(item.signature)) throw new ScriptingPlatformValidationError("SCRIPT_SIGNATURE_INVALID", `${name}.signature is invalid`); const sourceByteLength = integer(item.sourceByteLength, `${name}.sourceByteLength`, 0, SCRIPTING_BUDGET.maxSourceBytes); totalSourceBytes += sourceByteLength; if (!Number.isSafeInteger(totalSourceBytes) || totalSourceBytes > SCRIPTING_BUDGET.maxSourceBytes) throw new ScriptingPlatformValidationError("SCRIPT_BUDGET_EXCEEDED", "Manifest source bytes exceed the total budget"); return { id, name: text(item.name, `${name}.name`), entryPath: path(item.entryPath, `${name}.entryPath`), sourceByteLength, sourceSha256: digest(item.sourceSha256, `${name}.sourceSha256`), publisher: text(item.publisher, `${name}.publisher`), signature: item.signature, keyId: text(item.keyId, `${name}.keyId`, 128), permissions: [...item.permissions] as ScriptPermission[], dependencies, module: false, cpuMs: integer(item.cpuMs, `${name}.cpuMs`, 1, SCRIPTING_BUDGET.maxCpuMs), memoryBytes: integer(item.memoryBytes, `${name}.memoryBytes`, 1, SCRIPTING_BUDGET.maxMemoryBytes), wallMs: integer(item.wallMs, `${name}.wallMs`, 1, SCRIPTING_BUDGET.maxWallMs), network: false, autorun: false, driverExpressions: false, addonInstall: false }; }); const scriptIds = new Set(scripts.map((script) => script.id)); const active = new Set(); const complete = new Set(); const visit = (id: string): void => { if (active.has(id)) throw new ScriptingPlatformValidationError("SCRIPT_MANIFEST_INVALID", `Script dependency cycle includes ${id}`); if (complete.has(id)) return; const script = scripts.find((item) => item.id === id); if (!script) return; active.add(id); for (const dependency of script.dependencies) { if (!scriptIds.has(dependency.id)) throw new ScriptingPlatformValidationError("SCRIPT_MANIFEST_INVALID", `${id} references missing script ${dependency.id}`); visit(dependency.id); } active.delete(id); complete.add(id); }; scripts.forEach((script) => visit(script.id)); return { schemaVersion: SCRIPTING_PLATFORM_SCHEMA, scripts }; } export function gateScriptExecution(manifest: unknown, scriptId: string, approvedKeyIds: ReadonlySet): CapabilityGateResult { const parsed = parseScriptingManifest(manifest); const script = parsed.scripts.find((item) => item.id === scriptId); if (!script) throw new ScriptingPlatformValidationError("SCRIPT_MANIFEST_INVALID", `Unknown script ${scriptId}`); if (!approvedKeyIds.has(script.keyId)) return blockedGate("N-025", `SCRIPT_${script.id}`, [capabilityIssue("SCRIPT_SIGNATURE_INVALID", `Script ${script.id} is not signed by an approved key`)]); return blockedGate("N-025", `SCRIPT_${script.id}`, [capabilityIssue("SCRIPT_SANDBOX_UNAVAILABLE", "Local Python/Native execution requires an isolated sandbox")]); } export function resolveScriptPermissions(manifest: unknown, scriptId: string, requested: unknown = []): ScriptPermissionResolutionIR { const parsed = parseScriptingManifest(manifest); const script = parsed.scripts.find((item) => item.id === scriptId); if (!script) throw new ScriptingPlatformValidationError("SCRIPT_MANIFEST_INVALID", `Unknown script ${scriptId}`); const declared = [...script.permissions].sort(); const requestedList = Array.isArray(requested) ? requested : []; const requestedValid = requestedList.every((permission): permission is ScriptPermission => typeof permission === "string" && SCRIPT_PERMISSIONS.includes(permission as ScriptPermission)); const requestedUnique = new Set(requestedList).size === requestedList.length; const requestedCanonical = [...requestedList].filter((permission): permission is ScriptPermission => typeof permission === "string" && SCRIPT_PERMISSIONS.includes(permission as ScriptPermission)).sort(); const allowed = requestedValid && requestedUnique && requestedCanonical.every((permission) => declared.includes(permission)); return { status: allowed ? "ALLOWED" : "BLOCKED", code: allowed ? "SCRIPT_PERMISSIONS_ALLOWED" : "SCRIPT_POLICY_DENIED", scriptId, declared, requested: requestedCanonical, granted: allowed ? requestedCanonical : [], }; } export function parseScriptSandboxScope(value: unknown): ScriptSandboxScopeIR { if (!record(value) || value.schemaVersion !== SCRIPT_SANDBOX_SCOPE_SCHEMA) throw new ScriptingPlatformValidationError("PROTOCOL_MISMATCH", "Unsupported script sandbox scope schema"); const denied = ["dom", "hostWorker", "opfs", "indexedDB", "network"] as const; if (denied.some((name) => value[name] !== false)) throw new ScriptingPlatformValidationError("SCRIPT_POLICY_DENIED", "Script sandbox scope must deny browser and host capabilities"); return { schemaVersion: SCRIPT_SANDBOX_SCOPE_SCHEMA, dom: false, hostWorker: false, opfs: false, indexedDB: false, network: false }; } export function parseScriptSandboxBudget(value: unknown): ScriptSandboxBudgetIR { if (!record(value) || value.schemaVersion !== SCRIPT_SANDBOX_SCOPE_SCHEMA) throw new ScriptingPlatformValidationError("PROTOCOL_MISMATCH", "Unsupported script sandbox budget schema"); return { schemaVersion: SCRIPT_SANDBOX_SCOPE_SCHEMA, cpuMs: integer(value.cpuMs, "sandbox.cpuMs", 1, SCRIPT_SANDBOX_BUDGET.maxCpuMs), wallMs: integer(value.wallMs, "sandbox.wallMs", 1, SCRIPT_SANDBOX_BUDGET.maxWallMs), memoryBytes: integer(value.memoryBytes, "sandbox.memoryBytes", 1, SCRIPT_SANDBOX_BUDGET.maxMemoryBytes), maxMessageBytes: integer(value.maxMessageBytes, "sandbox.maxMessageBytes", 1, SCRIPT_SANDBOX_BUDGET.maxMessageBytes), maxOutputBytes: integer(value.maxOutputBytes, "sandbox.maxOutputBytes", 1, SCRIPT_SANDBOX_BUDGET.maxOutputBytes), }; } export function parseScriptHostCall(value: unknown, declaredPermissions: ReadonlySet): ScriptHostCallIR { if (!record(value) || value.schemaVersion !== SCRIPT_HOST_CALL_SCHEMA) throw new ScriptingPlatformValidationError("PROTOCOL_MISMATCH", "Unsupported script host call schema"); const requestId = auditRequestId(value.requestId, "hostCall.requestId"); const scriptId = text(value.scriptId, "hostCall.scriptId"); if (!SCRIPT_HOST_CALLS.includes(value.call as ScriptHostCallName)) throw new ScriptingPlatformValidationError("SCRIPT_POLICY_DENIED", "Host call is not allowlisted"); const call = value.call as ScriptHostCallName; if (value.permission !== call || !declaredPermissions.has(call)) throw new ScriptingPlatformValidationError("SCRIPT_POLICY_DENIED", "Host call permission is not declared"); if (!record(value.parameters)) throw new ScriptingPlatformValidationError("SCRIPT_MANIFEST_INVALID", "Host call parameters must be a structured object"); const parameters = value.parameters; const keys = Object.keys(parameters).sort(); const exact = (expected: string[]): void => { if (keys.length !== expected.length || keys.some((key, index) => key !== expected[index])) throw new ScriptingPlatformValidationError("SCRIPT_MANIFEST_INVALID", "Host call parameters contain unknown fields"); }; let normalized: ScriptHostCallParameters; if (call === "READ_MAIN") { exact(["revision"]); normalized = { revision: integer(parameters.revision, "hostCall.parameters.revision", 0, Number.MAX_SAFE_INTEGER) }; } else if (call === "READ_ASSET") { exact(["expectedSha256", "path"]); normalized = { path: path(parameters.path, "hostCall.parameters.path"), expectedSha256: digest(parameters.expectedSha256, "hostCall.parameters.expectedSha256") }; } else if (call === "WRITE_MAIN") { exact(["operation", "payload", "revision"]); if (!record(parameters.payload)) throw new ScriptingPlatformValidationError("SCRIPT_MANIFEST_INVALID", "hostCall.parameters.payload must be an object"); normalized = { revision: integer(parameters.revision, "hostCall.parameters.revision", 0, Number.MAX_SAFE_INTEGER), operation: text(parameters.operation, "hostCall.parameters.operation", 128), payload: { ...parameters.payload } }; } else if (call === "WRITE_ASSET") { exact(["byteLength", "path", "sha256"]); normalized = { path: path(parameters.path, "hostCall.parameters.path"), byteLength: integer(parameters.byteLength, "hostCall.parameters.byteLength", 0, SCRIPT_SANDBOX_BUDGET.maxOutputBytes), sha256: digest(parameters.sha256, "hostCall.parameters.sha256") }; } else { exact(["inputBlendSha256", "settingsSha256"]); normalized = { inputBlendSha256: digest(parameters.inputBlendSha256, "hostCall.parameters.inputBlendSha256"), settingsSha256: digest(parameters.settingsSha256, "hostCall.parameters.settingsSha256") }; } return { schemaVersion: SCRIPT_HOST_CALL_SCHEMA, requestId, scriptId, call, permission: call, parameters: normalized, execution: "DISABLED" }; } export function terminateScriptSandboxJob(value: unknown, reason: "CRASH" | "TIMEOUT" | "CANCEL"): ScriptSandboxJobIR { if (!record(value) || value.schemaVersion !== SCRIPT_SANDBOX_JOB_SCHEMA) throw new ScriptingPlatformValidationError("PROTOCOL_MISMATCH", "Unsupported script sandbox job schema"); const jobId = auditRequestId(value.jobId, "sandbox.jobId"); const workerGeneration = integer(value.workerGeneration, "sandbox.workerGeneration", 1, Number.MAX_SAFE_INTEGER); const baseRevision = integer(value.baseRevision, "sandbox.baseRevision", 0, Number.MAX_SAFE_INTEGER); const mainRevisionBefore = integer(value.mainRevisionBefore, "sandbox.mainRevisionBefore", 0, Number.MAX_SAFE_INTEGER); if (baseRevision !== mainRevisionBefore || value.status !== "RUNNING") throw new ScriptingPlatformValidationError("SCRIPT_MANIFEST_INVALID", "Sandbox termination must start from the current running Main revision"); const errorCode = reason === "CRASH" ? "SCRIPT_SANDBOX_CRASHED" : reason === "TIMEOUT" ? "SCRIPT_SANDBOX_TIMEOUT" : "SCRIPT_SANDBOX_CANCELLED"; return { schemaVersion: SCRIPT_SANDBOX_JOB_SCHEMA, jobId, workerGeneration, baseRevision, mainRevisionBefore, mainRevisionAfter: mainRevisionBefore, status: reason === "CRASH" ? "CRASHED" : reason === "TIMEOUT" ? "TIMED_OUT" : "CANCELLED", errorCode, temporaryBytes: 0, publishedResults: 0, lateResults: 0, committed: false, execution: "DISABLED" }; } export function rejectLateScriptSandboxResult(value: unknown): never { if (!record(value) || value.schemaVersion !== SCRIPT_SANDBOX_JOB_SCHEMA || !["CRASHED", "TIMED_OUT", "CANCELLED"].includes(value.status as string)) throw new ScriptingPlatformValidationError("SCRIPT_MANIFEST_INVALID", "Late sandbox result does not reference a terminated job"); throw new ScriptingPlatformValidationError("SCRIPT_SANDBOX_LATE_RESULT", "Sandbox result arrived after job termination"); } export function gateServerScriptJob(value: unknown, manifest: unknown, inputBlendSha256: string): CapabilityGateResult { const parsed = parseScriptingManifest(manifest); if (!record(value)) throw new ScriptingPlatformValidationError("SCRIPT_MANIFEST_INVALID", "Server script job is invalid"); const script = parsed.scripts.find((item) => item.id === value.scriptId); if (!script || script.sourceSha256 !== value.sourceSha256 || !SHA256.test(inputBlendSha256)) throw new ScriptingPlatformValidationError("ASSET_SOURCE_HASH_MISMATCH", "Server script job source hash is invalid"); return blockedGate("N-025", `SERVER_SCRIPT_${script.id}`, [capabilityIssue("SERVER_JOB_UNAVAILABLE", "Server Blender job endpoint is not configured")]); } export function platformCapabilities(scope: typeof globalThis = globalThis): Record { return { worker: typeof scope.Worker === "function" ? "AVAILABLE" : "UNAVAILABLE", webgpu: "gpu" in scope.navigator ? "PROBE_REQUIRED" : "UNAVAILABLE", offscreenCanvas: "OffscreenCanvas" in scope ? "AVAILABLE" : "UNAVAILABLE", opfs: scope.navigator.storage && typeof (scope.navigator.storage as StorageManager & { getDirectory?: unknown }).getDirectory === "function" ? "PROBE_REQUIRED" : "UNAVAILABLE", nativeWindow: "BLOCKED", cuda: "BLOCKED", metal: "BLOCKED", hip: "BLOCKED", optix: "BLOCKED", }; }