Declare SCUM Run data targets

This commit is contained in:
npc0-hue
2026-08-12 17:44:53 +08:00
parent d854d6fda3
commit 2246859984
17 changed files with 235 additions and 10 deletions
@@ -925,6 +925,21 @@
]
}
],
"dataTargets": [
{
"key": "scum-database",
"kind": "sqlite.snapshot",
"transportKey": "scum-database",
"sourceRootKey": "server-root",
"sourcePath": "SCUM/Saved/SaveFiles/SCUM.db",
"workspaceKey": "databases/scum-database",
"refreshPolicy": "on-demand-snapshot",
"maxBytes": 1073741824,
"platforms": [
"windows"
]
}
],
"clientManagers": [
{
"key": "scum-client-manager",
@@ -133,6 +133,12 @@
"items": { "$ref": "#/$defs/runtimeTransportProfile" },
"uniqueItems": true
},
"dataTargets": {
"type": "array",
"items": { "$ref": "#/$defs/runtimeDataTarget" },
"uniqueItems": true,
"maxItems": 64
},
"clientManagers": {
"type": "array",
"items": { "$ref": "#/$defs/runtimeClientManagerProfile" },
@@ -838,6 +844,22 @@
"capabilities": { "type": "array", "items": { "$ref": "#/$defs/runCapability" }, "uniqueItems": true, "minItems": 1 }
}
},
"runtimeDataTarget": {
"type": "object",
"required": ["key", "kind", "transportKey", "sourceRootKey", "sourcePath", "workspaceKey", "refreshPolicy", "maxBytes"],
"additionalProperties": false,
"properties": {
"key": { "$ref": "#/$defs/logicalKey" },
"kind": { "const": "sqlite.snapshot" },
"transportKey": { "$ref": "#/$defs/logicalKey" },
"sourceRootKey": { "$ref": "#/$defs/logicalKey" },
"sourcePath": { "$ref": "#/$defs/relativePathRef" },
"workspaceKey": { "type": "string", "pattern": "^databases/[a-z0-9][a-z0-9._/-]{0,119}$" },
"refreshPolicy": { "const": "on-demand-snapshot" },
"maxBytes": { "type": "integer", "minimum": 1, "maximum": 1073741824 },
"platforms": { "type": "array", "items": { "enum": ["windows", "linux", "darwin"] }, "uniqueItems": true }
}
},
"relativePathRef": {
"type": "string",
"pattern": "^(?!/)(?![A-Za-z]:)(?!.*://)(?!.*\\.\\.)[a-zA-Z0-9_./-]+$",
+14 -1
View File
@@ -1064,6 +1064,7 @@ export function validateSCUMLiveDataManifest(manifest: unknown, manifestDir?: st
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[] };
type DataTarget = { key?: string; kind?: string; transportKey?: string; sourceRootKey?: string; sourcePath?: string; workspaceKey?: string; refreshPolicy?: string; maxBytes?: number; platforms?: string[] };
type RuntimeLogSource = { key?: string };
type AssetFile = { path?: string };
type Asset = { key?: string; adapterVersion?: string; assetPath?: string; digest?: string };
@@ -1075,7 +1076,7 @@ export function validateSCUMLiveDataManifest(manifest: unknown, manifestDir?: st
type MapAsset = Asset & { requiredSchemaFingerprint?: string; metadataSchemaRef?: string; transformAssetPath?: string; transformDigest?: string; worldBounds?: { minX?: number; minY?: number; maxX?: number; maxY?: number }; image?: { width?: number; height?: number } };
type GiftCatalog = Asset & { catalogVersion?: string; itemSchemaRef?: string; transportTemplateKeys?: string[] };
type LiveData = { schemaVersion?: string; probe?: Probe; capabilityGates?: Gate[]; logParsers?: LogParser[]; sqliteQueries?: SQLiteQuery[]; syncCadences?: SyncCadence[]; typedRconTemplates?: TypedRCON[]; guardedMutations?: GuardedMutation[]; mapAssets?: MapAsset[]; giftCatalogs?: GiftCatalog[] };
const declaration = manifest as { id?: string; capabilities?: string[]; permissions?: string[]; assetFiles?: AssetFile[]; remoteAccess?: { runCapabilities?: string[]; databaseEngines?: string[] }; runtimeProfiles?: { transportProfiles?: Transport[]; logSources?: RuntimeLogSource[] }; scumLiveData?: LiveData };
const declaration = manifest as { id?: string; capabilities?: string[]; permissions?: string[]; assetFiles?: AssetFile[]; remoteAccess?: { runCapabilities?: string[]; databaseEngines?: string[] }; runtimeProfiles?: { transportProfiles?: Transport[]; dataTargets?: DataTarget[]; logSources?: RuntimeLogSource[] }; scumLiveData?: LiveData };
const liveData = declaration.scumLiveData;
if (!liveData) return [];
@@ -1085,6 +1086,7 @@ export function validateSCUMLiveDataManifest(manifest: unknown, manifestDir?: st
const remoteCapabilities = new Set(declaration.remoteAccess?.runCapabilities ?? []);
const remoteDatabaseEngines = new Set(declaration.remoteAccess?.databaseEngines ?? []);
const transportProfiles = declaration.runtimeProfiles?.transportProfiles ?? [];
const dataTargets = declaration.runtimeProfiles?.dataTargets ?? [];
const logSources = new Set((declaration.runtimeProfiles?.logSources ?? []).map((source) => source.key ?? ""));
const assetFiles = new Set((declaration.assetFiles ?? []).map((asset) => asset.path ?? ""));
const probe = liveData.probe;
@@ -1157,6 +1159,17 @@ export function validateSCUMLiveDataManifest(manifest: unknown, manifestDir?: st
}
};
const transportByKey = (key?: string): Transport | undefined => transportProfiles.find((candidate) => candidate.key === key);
const probeDataTarget = dataTargets.find((candidate) => candidate.key === probe?.targetKey);
const expectedProbeWorkspaceKey = `databases/${String(probe?.targetKey ?? "").replace(/^databases\//, "")}`;
if (!probeDataTarget) {
errors.push(`${location}.probe.targetKey: must reference a declared runtime data target`);
} else {
const probeDataTargetTransport = transportByKey(probeDataTarget.transportKey);
if (probeDataTarget.kind !== "sqlite.snapshot" || probeDataTarget.workspaceKey !== expectedProbeWorkspaceKey || probeDataTarget.refreshPolicy !== "on-demand-snapshot" || !probeDataTargetTransport || probeDataTargetTransport.kind !== "sqlite" || !probeDataTargetTransport.capabilities?.includes("remote.run.db.sqlite.probe")) {
errors.push(`${location}.probe.targetKey: must reference a sqlite snapshot data target for the generated Run workspace`);
}
if (!Number.isInteger(probeDataTarget.maxBytes) || (probeDataTarget.maxBytes ?? 0) < 1 || (probeDataTarget.maxBytes ?? 0) > 1073741824) errors.push(`${location}.probe.targetKey.maxBytes: must be between 1 and 1073741824`);
}
const requireRefs = (entry: Record<string, unknown>, entryLocation: string, refs: string[]): void => {
for (const field of refs) {
const ref = entry[field];
+14 -1
View File
@@ -655,11 +655,23 @@ export interface RuntimeLogEventDeclaration {
export interface RuntimeTransportProfile {
key: string;
kind: "file" | "ftp" | "rsync" | "mysql" | "sqlite" | "rcon";
kind: "file" | "ftp" | "rsync" | "mysql" | "sqlite" | "rcon" | "program";
targetKey?: string;
capabilities: RunCapability[];
}
export interface RuntimeDataTargetDeclaration {
key: string;
kind: "sqlite.snapshot";
transportKey: string;
sourceRootKey: string;
sourcePath: string;
workspaceKey: string;
refreshPolicy: "on-demand-snapshot";
maxBytes: number;
platforms?: RuntimePlatform[];
}
export interface RuntimeClientManagerProfile {
key: string;
displayName?: string;
@@ -739,6 +751,7 @@ export interface GamePluginRuntimeProfiles {
logSources?: RuntimeLogSource[];
logEvents?: RuntimeLogEventDeclaration[];
transportProfiles?: RuntimeTransportProfile[];
dataTargets?: RuntimeDataTargetDeclaration[];
clientManagers?: RuntimeClientManagerProfile[];
dllExtensions?: RuntimeDLLExtensionProfile[];
}
+12
View File
@@ -212,11 +212,19 @@ describe("plugin manifest validation", () => {
expect(manifest.capabilities).toContain("remote.run.db.sqlite.probe");
expect(manifest.remoteAccess.runCapabilities).toContain("remote.run.db.sqlite.probe");
expect(manifest.scumLiveData.probe).toMatchObject({ capability: "remote.run.db.sqlite.probe", targetKey: "scum-database" });
expect(manifest.runtimeProfiles?.dataTargets?.find((target) => target.key === "scum-database")).toMatchObject({ kind: "sqlite.snapshot", transportKey: "scum-database", sourceRootKey: "server-root", sourcePath: "SCUM/Saved/SaveFiles/SCUM.db", workspaceKey: "databases/scum-database", refreshPolicy: "on-demand-snapshot" });
expect(manifest.scumLiveData.capabilityGates.map((gate) => gate.capability)).toEqual(expect.arrayContaining(["players.read", "squads.read", "vehicles.read", "flags.read", "positions.read", "profile-xml.write", "economy-command.write", "gift-command.write"]));
expect(manifest.scumLiveData.capabilityGates.every((gate) => gate.gate === "disabled" && gate.evidenceStatus === "missing")).toBe(true);
expect(JSON.stringify(manifest.scumLiveData).toLowerCase()).not.toMatch(/select\s+.+from|sqlite:\/\/|mysql:\/\/|password|credential|socket|hostpath/);
});
it("requires SCUM schema probe targets to have generated Run workspace data targets", () => {
const errors = validateTemporaryScumCompanionManifest((manifest) => {
manifest.runtimeProfiles.dataTargets = [];
});
expect(errors.some((error) => error.includes("probe.targetKey") && error.includes("runtime data target"))).toBe(true);
});
it("rejects enabling SCUM live-data gates without compatible evidence and immutable digests", () => {
const errors = validateTemporaryScumCompanionManifest((manifest) => {
manifest.scumLiveData.capabilityGates[1] = { ...manifest.scumLiveData.capabilityGates[1], gate: "enabled", evidenceStatus: "missing" };
@@ -338,6 +346,7 @@ describe("plugin manifest validation", () => {
runtimeProfiles?: {
lifecycleProfiles?: Array<{ key: string; capabilities?: string[]; transportKeys?: string[] }>;
transportProfiles?: Array<{ key?: string; kind?: string; capabilities?: string[] }>;
dataTargets?: Array<Record<string, unknown>>;
};
};
const local = manifest.runtimeProfiles?.lifecycleProfiles?.find((profile) => profile.key === "run-local");
@@ -348,6 +357,9 @@ describe("plugin manifest validation", () => {
expect.objectContaining({ key: "scum-management", kind: "rcon", capabilities: ["remote.run.protected.rcon"] }),
expect.objectContaining({ key: "scum-program", kind: "program", capabilities: ["remote.run.program.command"] })
]));
expect(manifest.runtimeProfiles?.dataTargets).toEqual(expect.arrayContaining([
expect.objectContaining({ key: "scum-database", kind: "sqlite.snapshot", workspaceKey: "databases/scum-database", refreshPolicy: "on-demand-snapshot" })
]));
});
it("defines a generated SCUM companion config without inline proof or session material", () => {