Align SCUM adapter release gates

This commit is contained in:
npc0-hue
2026-08-13 12:16:58 +08:00
parent 381b59ecae
commit 36ed8822c3
5 changed files with 79 additions and 23 deletions
+21 -1
View File
@@ -1123,7 +1123,7 @@ export function validateSCUMLiveDataManifest(manifest: unknown, manifestDir?: st
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 (capability !== "schema-probe" && (!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`);
}
@@ -1136,6 +1136,21 @@ export function validateSCUMLiveDataManifest(manifest: unknown, manifestDir?: st
if (!digestPattern.test(asset.digest ?? "")) errors.push(`${assetLocation}.digest: must be a sha256 digest`);
if (asset.assetPath && asset.digest) validateSCUMAssetDigest(asset.assetPath, asset.digest, `${assetLocation}.digest`);
};
const gateByCapability = new Map(gates.map((gate) => [gate.capability ?? "", gate]));
const requireCapabilityGateMatch = (capability: string | undefined, adapterVersion: string | undefined, schemaFingerprint: string | undefined, digests: Array<string | undefined>, itemLocation: string): void => {
const safeCapability = capability ?? "";
const gate = gateByCapability.get(safeCapability);
if (!gate) {
errors.push(`${itemLocation}: capability ${safeCapability} must have a release gate`);
return;
}
if (gate.adapterVersion !== adapterVersion) errors.push(`${itemLocation}: capability gate adapterVersion must match the asset adapterVersion`);
if (schemaFingerprint && gate.requiredSchemaFingerprint !== schemaFingerprint) errors.push(`${itemLocation}: capability gate schema fingerprint must match the asset requirement`);
const gateDigests = new Set(gate.requiredAssetDigests ?? []);
for (const digest of digests) {
if (digestPattern.test(digest ?? "") && !gateDigests.has(digest ?? "")) errors.push(`${itemLocation}: capability gate must include asset digest ${digest}`);
}
};
const validateSCUMAssetDigest = (assetPath: string, expectedDigest: string, digestLocation: string): void => {
if (!manifestDir || !isSafeRelativePathRef(assetPath) || !digestPattern.test(expectedDigest)) return;
const absoluteAssetPath = path.resolve(manifestDir, assetPath);
@@ -1265,6 +1280,7 @@ export function validateSCUMLiveDataManifest(manifest: unknown, manifestDir?: st
requireAsset(query, itemLocation);
if (!readCapabilities.has(query.capability ?? "")) errors.push(`${itemLocation}.capability: must be a read capability`);
if (!fingerprintPattern.test(query.requiredSchemaFingerprint ?? "")) errors.push(`${itemLocation}.requiredSchemaFingerprint: must be a schema fingerprint`);
requireCapabilityGateMatch(query.capability, query.adapterVersion, query.requiredSchemaFingerprint, [query.digest], itemLocation);
requireRefs(query as Record<string, unknown>, itemLocation, ["parameterSchemaRef", "resultSchemaRef"]);
if (!Number.isInteger(query.maxRows) || (query.maxRows ?? 0) < 1 || (query.maxRows ?? 0) > 500) errors.push(`${itemLocation}.maxRows: must be between 1 and 500`);
if (!Number.isInteger(query.timeoutMs) || (query.timeoutMs ?? 0) < 1 || (query.timeoutMs ?? 0) > 60000) errors.push(`${itemLocation}.timeoutMs: must be between 1 and 60000`);
@@ -1288,6 +1304,7 @@ export function validateSCUMLiveDataManifest(manifest: unknown, manifestDir?: st
requireAsset(template, itemLocation);
if (!["economy-command.write", "gift-command.write"].includes(template.capability ?? "")) errors.push(`${itemLocation}.capability: must be economy-command.write or gift-command.write`);
if (template.requiredSchemaFingerprint && !fingerprintPattern.test(template.requiredSchemaFingerprint)) errors.push(`${itemLocation}.requiredSchemaFingerprint: must be a schema fingerprint`);
requireCapabilityGateMatch(template.capability, template.adapterVersion, template.requiredSchemaFingerprint, [template.digest], itemLocation);
if (template.permission !== "server.game-client.command" || !declaredPermissions.has("server.game-client.command")) errors.push(`${itemLocation}.permission: must require declared server.game-client.command`);
requireRefs(template as Record<string, unknown>, itemLocation, ["payloadSchemaRef", "resultSchemaRef", "confirmationSchemaRef"]);
if (!Number.isInteger(template.timeoutMs) || (template.timeoutMs ?? 0) < 1 || (template.timeoutMs ?? 0) > 60000) errors.push(`${itemLocation}.timeoutMs: must be between 1 and 60000`);
@@ -1302,6 +1319,7 @@ export function validateSCUMLiveDataManifest(manifest: unknown, manifestDir?: st
requireAsset(mutation, itemLocation);
if (mutation.capability !== "profile-xml.write") errors.push(`${itemLocation}.capability: must be profile-xml.write`);
if (!fingerprintPattern.test(mutation.requiredSchemaFingerprint ?? "")) errors.push(`${itemLocation}.requiredSchemaFingerprint: must be a schema fingerprint`);
requireCapabilityGateMatch(mutation.capability, mutation.adapterVersion, mutation.requiredSchemaFingerprint, [mutation.digest], itemLocation);
if (mutation.permission !== "server.game-client.maintenance" || !declaredPermissions.has("server.game-client.maintenance")) errors.push(`${itemLocation}.permission: must require declared server.game-client.maintenance`);
requireRefs(mutation as Record<string, unknown>, itemLocation, ["payloadSchemaRef", "resultSchemaRef", "confirmationSchemaRef"]);
if (!Number.isInteger(mutation.timeoutMs) || (mutation.timeoutMs ?? 0) < 1 || (mutation.timeoutMs ?? 0) > 60000) errors.push(`${itemLocation}.timeoutMs: must be between 1 and 60000`);
@@ -1317,6 +1335,7 @@ export function validateSCUMLiveDataManifest(manifest: unknown, manifestDir?: st
const itemLocation = `${location}.mapAssets[${index}]`;
requireAsset(mapAsset, itemLocation);
if (!fingerprintPattern.test(mapAsset.requiredSchemaFingerprint ?? "")) errors.push(`${itemLocation}.requiredSchemaFingerprint: must be a schema fingerprint`);
requireCapabilityGateMatch("positions.read", mapAsset.adapterVersion, mapAsset.requiredSchemaFingerprint, [mapAsset.digest, mapAsset.transformDigest], itemLocation);
requireRefs(mapAsset as Record<string, unknown>, itemLocation, ["metadataSchemaRef"]);
if (!mapAsset.transformAssetPath || !isSafeRelativePathRef(mapAsset.transformAssetPath)) errors.push(`${itemLocation}.transformAssetPath: must be a contained package-relative path`);
if (mapAsset.transformAssetPath && !assetFiles.has(mapAsset.transformAssetPath)) errors.push(`${itemLocation}.transformAssetPath: must be declared in manifest.assetFiles`);
@@ -1332,6 +1351,7 @@ export function validateSCUMLiveDataManifest(manifest: unknown, manifestDir?: st
const itemLocation = `${location}.giftCatalogs[${index}]`;
requireAsset(catalog, itemLocation);
if (!adapterPattern.test(catalog.catalogVersion ?? "")) errors.push(`${itemLocation}.catalogVersion: must be a bounded catalog version`);
requireCapabilityGateMatch("gift-command.write", catalog.adapterVersion, undefined, [catalog.digest], itemLocation);
requireRefs(catalog as Record<string, unknown>, itemLocation, ["itemSchemaRef"]);
if (!Array.isArray(catalog.transportTemplateKeys) || catalog.transportTemplateKeys.length === 0) errors.push(`${itemLocation}.transportTemplateKeys: must reference at least one typed RCON transport template`);
for (const templateKey of catalog.transportTemplateKeys ?? []) {