feat(scum): gate live data on schema evidence

This commit is contained in:
npc0-hue
2026-08-11 13:28:36 +08:00
parent e5d26e5420
commit a58da47c60
18 changed files with 1265 additions and 3 deletions
+48
View File
@@ -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));