refactor(scum): declare protected run requests
This commit is contained in:
@@ -26,6 +26,7 @@ import {
|
||||
parseBridgeExecutionResponse,
|
||||
parseAIInvocationResponse,
|
||||
type GameClientBridgeQueryTemplateDeclaration,
|
||||
type GameClientBridgeProtectedRequestDeclaration,
|
||||
type GameClientBridgeCompanionDeclaration,
|
||||
type GamePluginManifest,
|
||||
type RuntimeLogEventDeclaration,
|
||||
@@ -173,17 +174,19 @@ describe("plugin manifest validation", () => {
|
||||
expect(validateManifestFile("examples/scum-server-plugin/manifest.json")).toEqual([]);
|
||||
});
|
||||
|
||||
it("declares a fixed, schema-bound vehicle spawn instead of a raw command surface", () => {
|
||||
it("declares bounded protected SQL and management request surfaces", () => {
|
||||
const pluginDir = path.join(pluginsRoot, "examples/scum-server-plugin");
|
||||
const manifest = JSON.parse(fs.readFileSync(path.join(pluginDir, "manifest.json"), "utf8")) as { gameClientBridge: { commands: Array<{ type: string; payloadSchemaRef: string; resultSchemaRef?: string }>; features: Array<{ key: string; requiredHandlers?: string[] }> } };
|
||||
const command = manifest.gameClientBridge.commands.find((candidate) => candidate.type === "vehicle.spawn");
|
||||
expect(command).toBeDefined();
|
||||
expect(manifest.gameClientBridge.features.find((feature) => feature.key === "vehicle.spawn")?.requiredHandlers).toEqual(["vehicle.spawn"]);
|
||||
const payload = JSON.parse(fs.readFileSync(path.join(pluginDir, command!.payloadSchemaRef), "utf8"));
|
||||
const result = JSON.parse(fs.readFileSync(path.join(pluginDir, command!.resultSchemaRef!), "utf8"));
|
||||
expect(payload).toMatchObject({ additionalProperties: false, required: ["vehicleCode"], properties: { vehicleCode: { enum: ["BPC_Laika_C", "BPC_WolfsWagen_C"] } } });
|
||||
expect(JSON.stringify(payload).toLowerCase()).not.toMatch(/command|rcon|target|credential|socket|shell|sql/);
|
||||
expect(result).toMatchObject({ additionalProperties: false, properties: { outcome: { enum: ["succeeded", "failed", "unknown"] } } });
|
||||
const manifest = JSON.parse(fs.readFileSync(path.join(pluginDir, "manifest.json"), "utf8")) as { gameClientBridge: { commands: Array<{ type: string; payloadSchemaRef: string; resultSchemaRef?: string; protectedRequest?: { kind: string; textField: string; transportKey: string; targetKey: string } }> } };
|
||||
const commands = manifest.gameClientBridge.commands.filter((candidate) => candidate.protectedRequest);
|
||||
expect(commands.map((command) => command.protectedRequest?.kind)).toEqual(expect.arrayContaining(["sql", "rcon", "program"]));
|
||||
for (const command of commands) {
|
||||
expect(command.protectedRequest?.textField).toBe("requestText");
|
||||
expect(command.protectedRequest?.transportKey).toBe(command.protectedRequest?.targetKey);
|
||||
const payload = JSON.parse(fs.readFileSync(path.join(pluginDir, command.payloadSchemaRef), "utf8"));
|
||||
const result = JSON.parse(fs.readFileSync(path.join(pluginDir, command.resultSchemaRef!), "utf8"));
|
||||
expect(payload).toMatchObject({ additionalProperties: false, required: ["requestText"] });
|
||||
expect(result).toMatchObject({ additionalProperties: false, properties: { outcome: { enum: ["succeeded", "failed", "unknown"] } } });
|
||||
}
|
||||
});
|
||||
|
||||
it("declares a frozen SCUM install/adopt template with explicit mapping and verification checks", () => {
|
||||
@@ -191,7 +194,7 @@ describe("plugin manifest validation", () => {
|
||||
const template = manifest.runtimeProfiles.serverDeployments[0];
|
||||
expect(template).toMatchObject({ key: "scum-steamcmd-windows", version: "1.0.0", steamAppId: "3792580", configFormat: "ini" });
|
||||
expect(template.configMappings.map((mapping: any) => mapping.fieldKey)).toEqual(["serverName", "gamePort", "queryPort", "maxPlayers"]);
|
||||
expect(template.verificationChecks.filter((check: any) => check.required)).toHaveLength(5);
|
||||
expect(template.verificationChecks.filter((check: any) => check.required)).toHaveLength(4);
|
||||
});
|
||||
|
||||
it("rejects an SCUM template mapping an undeclared field", () => {
|
||||
@@ -212,18 +215,22 @@ describe("plugin manifest validation", () => {
|
||||
expect(unsafe.some((error) => error.includes("raw host path"))).toBe(true);
|
||||
});
|
||||
|
||||
it("removes direct RCON, database, and DLL extension declarations", () => {
|
||||
it("declares protected database and management transports without direct access", () => {
|
||||
const manifestPath = path.join(pluginsRoot, "examples/scum-server-plugin/manifest.json");
|
||||
const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8")) as {
|
||||
runtimeProfiles?: {
|
||||
lifecycleProfiles?: Array<{ key: string; capabilities?: string[]; transportKeys?: string[] }>;
|
||||
transportProfiles?: Array<{ kind?: string }>;
|
||||
transportProfiles?: Array<{ key?: string; kind?: string; capabilities?: string[] }>;
|
||||
};
|
||||
};
|
||||
const local = manifest.runtimeProfiles?.lifecycleProfiles?.find((profile) => profile.key === "run-local");
|
||||
expect(local?.capabilities).not.toContain("remote.run.rcon.command");
|
||||
expect(local?.transportKeys).not.toContain("rcon");
|
||||
expect(manifest.runtimeProfiles?.transportProfiles?.some((profile) => profile.kind === "sqlite" || profile.kind === "mysql" || profile.kind === "rcon")).toBe(false);
|
||||
expect(manifest.runtimeProfiles?.transportProfiles).toEqual(expect.arrayContaining([
|
||||
expect.objectContaining({ key: "scum-database", kind: "sqlite", capabilities: ["remote.run.protected.sql"] }),
|
||||
expect.objectContaining({ key: "scum-management", kind: "rcon", capabilities: ["remote.run.protected.rcon"] }),
|
||||
expect.objectContaining({ key: "scum-program", kind: "program", capabilities: ["remote.run.program.command"] })
|
||||
]));
|
||||
});
|
||||
|
||||
it("defines a generated SCUM companion config without inline proof or session material", () => {
|
||||
@@ -588,7 +595,7 @@ describe("plugin manifest validation", () => {
|
||||
const manifest = JSON.parse(fs.readFileSync(path.join(pluginDir, "manifest.json"), "utf8")) as {
|
||||
permissions: string[];
|
||||
runtimeProfiles?: {
|
||||
logSources?: Array<{ key: string; retentionDays?: number }>;
|
||||
logSources?: Array<{ key: string; kind?: string; streamKey?: string; retentionDays?: number }>;
|
||||
logEvents?: Array<RuntimeLogEventDeclaration>;
|
||||
};
|
||||
};
|
||||
@@ -597,6 +604,8 @@ describe("plugin manifest validation", () => {
|
||||
const logEvents = manifest.runtimeProfiles?.logEvents ?? [];
|
||||
|
||||
expect(logEvents.map((event) => event.eventType)).toEqual(expect.arrayContaining(expectedTypes));
|
||||
expect(logSources.get("scum-console-stdout")).toMatchObject({ kind: "process.stdout", streamKey: "scum.console.stdout" });
|
||||
expect(logSources.get("scum-console-stderr")).toMatchObject({ kind: "process.stderr", streamKey: "scum.console.stderr" });
|
||||
expect(new Set(logEvents.map((event) => event.key)).size).toBe(logEvents.length);
|
||||
expect(new Set(logEvents.map((event) => event.eventType)).size).toBe(logEvents.length);
|
||||
for (const event of logEvents) {
|
||||
@@ -949,6 +958,17 @@ describe("plugin SDK", () => {
|
||||
expect(request).not.toHaveProperty("hostPath");
|
||||
expect(request).not.toHaveProperty("dsn");
|
||||
});
|
||||
|
||||
it("types protected request declarations while retaining text redaction boundaries", () => {
|
||||
const declaration: GameClientBridgeProtectedRequestDeclaration = { kind: "sql", transportKey: "scum-database", targetKey: "scum-database", textField: "requestText", maxTextBytes: 4096 };
|
||||
expect(declaration).toMatchObject({ kind: "sql", textField: "requestText" });
|
||||
expect(JSON.stringify(declaration).toLowerCase()).not.toMatch(/dsn|hostpath|socket|credential|password/);
|
||||
const errors = validateTemporaryBridgeManifest((manifest) => {
|
||||
manifest.gameClientBridge.commands[0].type = "database.request";
|
||||
manifest.gameClientBridge.commands[0].protectedRequest = { kind: "sql", transportKey: "missing", targetKey: "missing", textField: "requestText", maxTextBytes: 512 };
|
||||
});
|
||||
expect(errors.some((error) => error.includes("protectedRequest.transportKey"))).toBe(true);
|
||||
});
|
||||
it("checks declared bridge permissions", () => {
|
||||
const context: PluginBridgeContext = {
|
||||
pluginId: "game.example",
|
||||
|
||||
Reference in New Issue
Block a user