feat(scum): gate live data on schema evidence
This commit is contained in:
@@ -1054,6 +1054,53 @@ export function validateRuntimeLogEventCatalog(manifest: unknown): string[] {
|
||||
return errors;
|
||||
}
|
||||
|
||||
export function validateSCUMLiveDataManifest(manifest: unknown): string[] {
|
||||
if (typeof manifest !== "object" || manifest === null) return [];
|
||||
type Gate = { capability?: string; gate?: string; adapterVersion?: string; requiredSchemaFingerprint?: string; requiredAssetDigests?: string[]; evidenceStatus?: string; safeReason?: string };
|
||||
type Probe = { capability?: string; targetKey?: string; bounds?: { maxSampleRows?: number; timeoutMs?: number; maxResultBytes?: number } };
|
||||
type Transport = { key?: string; kind?: string; targetKey?: string; capabilities?: string[] };
|
||||
const declaration = manifest as { id?: string; capabilities?: string[]; remoteAccess?: { runCapabilities?: string[] }; runtimeProfiles?: { transportProfiles?: Transport[] }; scumLiveData?: { schemaVersion?: string; probe?: Probe; capabilityGates?: Gate[] } };
|
||||
const liveData = declaration.scumLiveData;
|
||||
if (!liveData) return [];
|
||||
|
||||
const errors: string[] = [];
|
||||
const declaredCapabilities = new Set(declaration.capabilities ?? []);
|
||||
const remoteCapabilities = new Set(declaration.remoteAccess?.runCapabilities ?? []);
|
||||
const probe = liveData.probe;
|
||||
const location = "manifest.scumLiveData";
|
||||
if (liveData.schemaVersion !== "1") errors.push(`${location}.schemaVersion: must be 1`);
|
||||
if (probe?.capability !== "remote.run.db.sqlite.probe") errors.push(`${location}.probe.capability: must be remote.run.db.sqlite.probe`);
|
||||
if (!declaredCapabilities.has("remote.run.db.sqlite.probe") || !remoteCapabilities.has("remote.run.db.sqlite.probe")) errors.push(`${location}.probe: plugin and remoteAccess must declare remote.run.db.sqlite.probe`);
|
||||
const transport = (declaration.runtimeProfiles?.transportProfiles ?? []).find((candidate) => candidate.targetKey === probe?.targetKey || candidate.key === probe?.targetKey);
|
||||
if (!transport) {
|
||||
errors.push(`${location}.probe.targetKey: must reference a declared runtime transport profile or target`);
|
||||
} else {
|
||||
if (transport.kind !== "sqlite") errors.push(`${location}.probe.targetKey: schema probe requires a sqlite transport profile`);
|
||||
if (!transport.capabilities?.includes("remote.run.db.sqlite.probe")) errors.push(`${location}.probe.targetKey: sqlite transport must declare remote.run.db.sqlite.probe`);
|
||||
}
|
||||
if ((probe?.bounds?.maxSampleRows ?? 0) > 3) errors.push(`${location}.probe.bounds.maxSampleRows: redacted samples are limited to 3`);
|
||||
if ((probe?.bounds?.timeoutMs ?? 0) > 10000) errors.push(`${location}.probe.bounds.timeoutMs: must be bounded to 10 seconds or less`);
|
||||
if ((probe?.bounds?.maxResultBytes ?? 0) > 1048576) errors.push(`${location}.probe.bounds.maxResultBytes: must be bounded to 1 MiB or less`);
|
||||
|
||||
const gates = liveData.capabilityGates ?? [];
|
||||
const seen = new Set<string>();
|
||||
for (const [index, gate] of gates.entries()) {
|
||||
const gateLocation = `${location}.capabilityGates[${index}]`;
|
||||
const capability = gate.capability ?? "";
|
||||
if (seen.has(capability)) errors.push(`${gateLocation}.capability: duplicate gate ${capability}`);
|
||||
seen.add(capability);
|
||||
const reasonErrors = unsafeStringReasons(gate.safeReason ?? "");
|
||||
errors.push(...reasonErrors.map((reason) => `${gateLocation}.safeReason: ${reason}`));
|
||||
if (gate.gate === "enabled") {
|
||||
if (gate.evidenceStatus !== "compatible") errors.push(`${gateLocation}.evidenceStatus: enabled gates require compatible evidence`);
|
||||
if (!/^(sha256:)?[a-fA-F0-9]{16,128}$/.test(gate.requiredSchemaFingerprint ?? "")) errors.push(`${gateLocation}.requiredSchemaFingerprint: enabled gates require a schema fingerprint`);
|
||||
if (!Array.isArray(gate.requiredAssetDigests) || gate.requiredAssetDigests.length === 0) errors.push(`${gateLocation}.requiredAssetDigests: enabled gates require immutable asset digests`);
|
||||
}
|
||||
if (gate.gate === "disabled" && gate.evidenceStatus === "compatible") errors.push(`${gateLocation}.evidenceStatus: disabled gates must not claim compatible evidence`);
|
||||
}
|
||||
return errors;
|
||||
}
|
||||
|
||||
type GameClientBridgeSchemaReference = {
|
||||
location: string;
|
||||
ref: string;
|
||||
@@ -1372,6 +1419,7 @@ export function validateManifestFile(manifestPath: string): string[] {
|
||||
errors.push(...validateClientManagerProfiles(manifest));
|
||||
errors.push(...validateDLLExtensionProfiles(manifest));
|
||||
errors.push(...validateGameClientBridgeCatalog(manifest));
|
||||
errors.push(...validateSCUMLiveDataManifest(manifest));
|
||||
errors.push(...validateGameClientBridgeSchemaFiles(manifest, manifestDir));
|
||||
errors.push(...validateGameClientBridgeCompanionConfig(manifest, manifestDir));
|
||||
errors.push(...validateRuntimeLogEventCatalog(manifest));
|
||||
|
||||
Reference in New Issue
Block a user