Checkpoint web parity through Chromium input tasks
Some checks are pending
M6 deployable RC / quick (push) Waiting to run
M6 deployable RC / chromium (push) Blocked by required conditions
M6 deployable RC / release (push) Blocked by required conditions

This commit is contained in:
mes123456
2026-08-19 10:39:03 -04:00
parent 5a11045ca5
commit 380cbed4ff
634 changed files with 41862 additions and 212 deletions

View File

@@ -6,7 +6,14 @@ 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];
@@ -15,12 +22,14 @@ 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;
@@ -30,6 +39,94 @@ export interface ScriptManifestIR {
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<string, unknown> }
| { 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;
@@ -111,11 +208,128 @@ 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.localeCompare(b.id)) }))
.sort((a, b) => a.id.localeCompare(b.id)),
.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<string>();
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<string>(); const complete = new Set<string>();
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<ScriptSignatureVerificationIR> {
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,
@@ -129,7 +343,7 @@ export async function createScriptExecutionAudit(
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(stableJSON(canonicalManifest(parsed)));
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({
@@ -227,16 +441,20 @@ export async function verifyScriptSource(source: ScriptSourceIR): Promise<boolea
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<string>();
const ids = new Set<string>(); 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 dependencies = item.dependencies.map((dependency, dependencyIndex): ScriptDependencyIR => { const dependencyName = `${name}.dependencies[${dependencyIndex}]`; if (!record(dependency)) throw new ScriptingPlatformValidationError("SCRIPT_MANIFEST_INVALID", `${dependencyName} is invalid`); return { id: text(dependency.id, `${dependencyName}.id`), sourceSha256: digest(dependency.sourceSha256, `${dependencyName}.sourceSha256`), sourcePath: path(dependency.sourcePath, `${dependencyName}.sourcePath`) }; });
if (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`);
const dependencyIds = new Set<string>();
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`);
return { id, name: text(item.name, `${name}.name`), entryPath: path(item.entryPath, `${name}.entryPath`), 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, 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 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<string>(); const complete = new Set<string>(); 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));
@@ -249,6 +467,81 @@ export function gateScriptExecution(manifest: unknown, scriptId: string, approve
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<ScriptPermission>): 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")]);