Remove SCUM server-management client path

This commit is contained in:
npc0-hue
2026-09-02 17:56:49 +08:00
parent 6e614d3fa3
commit a82f1ff01a
52 changed files with 245 additions and 5131 deletions
+21 -206
View File
@@ -26,9 +26,7 @@ import {
parseBridgeExecutionResponse,
parseAIInvocationResponse,
type GameClientBridgeQueryTemplateDeclaration,
type GameClientBridgeCompanionDeclaration,
type GamePluginManifest,
type RuntimeClientManagerProfile,
type PluginLifecycleActionDeclaration,
type PluginBridgeContext
} from "../sdk/index.js";
@@ -74,16 +72,6 @@ function writeFixtureJSON(fixtureDir: string, relativePath: string, value: unkno
fs.writeFileSync(target, `${JSON.stringify(value, null, 2)}\n`, "utf8");
}
function listProductionGoFiles(directory: string): string[] {
return fs.readdirSync(directory, { withFileTypes: true }).flatMap((entry) => {
const target = path.join(directory, entry.name);
if (entry.isDirectory()) {
return listProductionGoFiles(target);
}
return entry.isFile() && entry.name.endsWith(".go") && !entry.name.endsWith("_test.go") ? [target] : [];
});
}
function validateTemporaryBridgeManifest(mutate?: (manifest: MutableBridgeManifest, fixtureDir: string) => void): string[] {
const fixtureDir = fs.mkdtempSync(path.join(os.tmpdir(), "browser-bridge-manifest-"));
try {
@@ -129,8 +117,8 @@ function validateTemporaryBridgeManifest(mutate?: (manifest: MutableBridgeManife
}
}
function validateTemporaryScumCompanionManifest(mutate: (manifest: Record<string, any>, fixtureDir: string) => void): string[] {
const fixtureDir = fs.mkdtempSync(path.join(os.tmpdir(), "browser-scum-companion-manifest-"));
function validateTemporaryScumManifest(mutate: (manifest: Record<string, any>, fixtureDir: string) => void): string[] {
const fixtureDir = fs.mkdtempSync(path.join(os.tmpdir(), "browser-scum-manifest-"));
try {
fs.cpSync(path.join(pluginsRoot, "examples/scum-server-plugin"), fixtureDir, { recursive: true });
const manifestPath = path.join(fixtureDir, "manifest.json");
@@ -209,18 +197,18 @@ describe("plugin manifest validation", () => {
});
it("rejects lifecycle executable assets missing from the manifest seed declaration", () => {
const errors = validateTemporaryScumCompanionManifest((manifest) => {
const errors = validateTemporaryScumManifest((manifest) => {
manifest.assetFiles = manifest.assetFiles.filter((file: { path: string }) => file.path !== "bin/scum-start.cmd");
});
expect(errors.some((error) => error.includes("lifecycleAction.start.executableKey") && error.includes("manifest.assetFiles"))).toBe(true);
});
it("rejects unsupported or unsafe inline create-field declarations", () => {
const malformed = validateTemporaryScumCompanionManifest((manifest) => {
const malformed = validateTemporaryScumManifest((manifest) => {
manifest.server.createFields[0].type = "path";
});
expect(malformed.some((error) => error.includes("createFields") && error.includes("type"))).toBe(true);
const unsafe = validateTemporaryScumCompanionManifest((manifest) => {
const unsafe = validateTemporaryScumManifest((manifest) => {
manifest.server.createFields[0].defaultValue = "/srv/hidden-server";
});
expect(unsafe.some((error) => error.includes("raw host path"))).toBe(true);
@@ -244,167 +232,6 @@ describe("plugin manifest validation", () => {
]));
});
it("defines a generated SCUM companion config without inline proof or session material", () => {
const pluginDir = path.join(pluginsRoot, "examples/scum-server-plugin");
const manifest = JSON.parse(fs.readFileSync(path.join(pluginDir, "manifest.json"), "utf8")) as {
gameClientBridge: { companion?: GameClientBridgeCompanionDeclaration };
};
const companion = manifest.gameClientBridge.companion;
expect(companion).toMatchObject({
profileKey: "scum-client-manager",
configTemplateKey: "client-config",
configFormat: "yaml",
platformBaseUrlSource: "run-control",
registrationProof: "hmac-sha256",
proofMaterialSource: "component-package",
proofMaterialEnv: "SCUM_COMPONENT_PROOF",
sessionMode: "component-session",
tlsPolicy: "verify-system-roots",
heartbeatIntervalSeconds: 30,
requestTimeoutSeconds: 15
});
const schema = JSON.parse(fs.readFileSync(path.join(pluginDir, companion!.configSchemaRef), "utf8"));
const example = JSON.parse(fs.readFileSync(path.join(pluginDir, "schemas/companion/config.generated.example.json"), "utf8"));
const validate = new Ajv2020({ strict: false, validateFormats: false }).compile(schema);
expect(validate(example), JSON.stringify(validate.errors)).toBe(true);
expect(JSON.stringify(example)).not.toMatch(/authKey|componentKey|credential|password|sessionToken|secret|\/api\/v1\/scum-clients\//i);
expect(example).toMatchObject({ proof: { materialEnv: "SCUM_COMPONENT_PROOF" }, session: { mode: "component-session" }, tls: { policy: "verify-system-roots" } });
});
it("rejects unsafe SCUM companion bootstrap policy and inline session material", () => {
const policyErrors = validateTemporaryScumCompanionManifest((manifest) => {
manifest.gameClientBridge.companion.tlsPolicy = "skip-verification";
});
expect(policyErrors.some((error) => error.includes("tlsPolicy") || error.includes("secure component registration/session/TLS policy"))).toBe(true);
const materialErrors = validateTemporaryScumCompanionManifest((_manifest, fixtureDir) => {
const examplePath = path.join(fixtureDir, "schemas/companion/config.generated.example.json");
const example = JSON.parse(fs.readFileSync(examplePath, "utf8"));
example.proof.sessionToken = "inline-session-material";
writeFixtureJSON(fixtureDir, "schemas/companion/config.generated.example.json", example);
});
expect(materialErrors.some((error) => error.includes("inline proof/session material") || error.includes("additional properties"))).toBe(true);
const environmentErrors = validateTemporaryScumCompanionManifest((manifest) => {
manifest.gameClientBridge.companion.proofMaterialEnv = "LD_PRELOAD";
});
expect(environmentErrors.some((error) => error.includes("proofMaterialEnv"))).toBe(true);
});
it("rejects generated SCUM companion configs with incomplete capabilities or unsafe Platform URLs", () => {
const capabilityErrors = validateTemporaryScumCompanionManifest((_manifest, fixtureDir) => {
const examplePath = path.join(fixtureDir, "schemas/companion/config.generated.example.json");
const example = JSON.parse(fs.readFileSync(examplePath, "utf8"));
example.capabilities = ["component.register", "component.heartbeat", "component.health", "game-client.bridge"];
writeFixtureJSON(fixtureDir, "schemas/companion/config.generated.example.json", example);
});
expect(capabilityErrors.some((error) => error.includes("capabilities"))).toBe(true);
for (const unsafeURL of ["https://user:raw-token@example.test?session=raw-token#fragment", "https://?missing-host"]) {
const urlErrors = validateTemporaryScumCompanionManifest((_manifest, fixtureDir) => {
const examplePath = path.join(fixtureDir, "schemas/companion/config.generated.example.json");
const example = JSON.parse(fs.readFileSync(examplePath, "utf8"));
example.platform.baseUrl = unsafeURL;
writeFixtureJSON(fixtureDir, "schemas/companion/config.generated.example.json", example);
});
expect(urlErrors.some((error) => error.includes("platform.baseUrl"))).toBe(true);
}
});
it("cross-validates generated SCUM companion config against its declaration and runtime profile", () => {
const proofErrors = validateTemporaryScumCompanionManifest((manifest) => {
manifest.gameClientBridge.companion.proofMaterialEnv = "OTHER_COMPONENT_PROOF";
});
expect(proofErrors.some((error) => error.includes("proof.materialEnv") && error.includes("proofMaterialEnv"))).toBe(true);
const profileErrors = validateTemporaryScumCompanionManifest((manifest) => {
const manager = manifest.runtimeProfiles.clientManagers.find((candidate: Record<string, unknown>) => candidate.key === "scum-client-manager");
manager.health.requiredCapabilities = manager.health.requiredCapabilities.filter((capability: string) => capability !== "logs.stream");
});
expect(profileErrors.some((error) => error.includes("capabilities") && error.includes("requiredCapabilities"))).toBe(true);
const sessionErrors = validateTemporaryScumCompanionManifest((_manifest, fixtureDir) => {
const examplePath = path.join(fixtureDir, "schemas/companion/config.generated.example.json");
const example = JSON.parse(fs.readFileSync(examplePath, "utf8"));
example.session.mode = "legacy-shared-token";
writeFixtureJSON(fixtureDir, "schemas/companion/config.generated.example.json", example);
});
expect(sessionErrors.some((error) => error.includes("session"))).toBe(true);
});
it("rejects unsafe optional fields declared only by the SCUM companion config schema", () => {
const schemaErrors = validateTemporaryScumCompanionManifest((_manifest, fixtureDir) => {
const schemaPath = path.join(fixtureDir, "schemas/companion/config.schema.json");
const schema = JSON.parse(fs.readFileSync(schemaPath, "utf8"));
schema.properties.hostPath = { type: "string", minLength: 1, maxLength: 200 };
writeFixtureJSON(fixtureDir, "schemas/companion/config.schema.json", schema);
});
expect(schemaErrors.some((error) => error.includes("hostPath") && error.includes("raw host path"))).toBe(true);
});
it("rejects legacy companion endpoints, insecure TLS flags, and credential keys", () => {
const cases = [
{
name: "legacy shared-token endpoint",
mutate(schema: Record<string, any>, _example: Record<string, any>): void {
schema.description = "legacy /api/v1/scum-clients/commands endpoint";
}
},
{
name: "insecure TLS flag",
mutate(schema: Record<string, any>, _example: Record<string, any>): void {
schema.description = "InsecureSkipVerify";
}
},
{
name: "inline credential key",
mutate(schema: Record<string, any>, example: Record<string, any>): void {
schema.properties.credential = { type: "string", minLength: 1, maxLength: 200 };
example.credential = "legacy-shared-value";
}
}
];
for (const testCase of cases) {
const errors = validateTemporaryScumCompanionManifest((_manifest, fixtureDir) => {
const schemaPath = path.join(fixtureDir, "schemas/companion/config.schema.json");
const examplePath = path.join(fixtureDir, "schemas/companion/config.generated.example.json");
const schema = JSON.parse(fs.readFileSync(schemaPath, "utf8")) as Record<string, any>;
const example = JSON.parse(fs.readFileSync(examplePath, "utf8")) as Record<string, any>;
testCase.mutate(schema, example);
writeFixtureJSON(fixtureDir, "schemas/companion/config.schema.json", schema);
writeFixtureJSON(fixtureDir, "schemas/companion/config.generated.example.json", example);
});
expect(errors, testCase.name).toContain(
"manifest.gameClientBridge.companion.configSchemaRef: companion config must not contain legacy endpoints, insecure TLS, or inline proof/session material"
);
}
});
it("keeps the production SCUM companion free of unsafe legacy transport and updater patterns", () => {
const companionDir = path.join(pluginsRoot, "examples/scum-server-plugin/companion");
const forbiddenPatterns = [
{ name: "legacy shared-token endpoint", pattern: /\/api\/v1\/scum-clients\//i },
{ name: "legacy shared credential", pattern: /\b(?:SCUMClientCredential|scum_client_credential)\b/i },
{ name: "disabled TLS verification", pattern: /\bInsecureSkipVerify\s*:\s*true\b/ },
{ name: "arbitrary process or shell execution", pattern: /(?:\b(?:os\/exec|exec\.Command(?:Context)?|os\.StartProcess|syscall\.Exec)\b|\b(?:bash|zsh|powershell|pwsh|cmd(?:\.exe)?)\s+-[a-z/])/i },
{ name: "direct socket transport", pattern: /(?:\bnet\.(?:Dial|DialTimeout)\s*\(|\b(?:tcp|unix|ws):\/\/)/i },
{ name: "arbitrary URL self-update or download", pattern: /(?:\b(?:self_?update|update_?url|download_?url|updater)\b|\bhttp\.(?:Get|DefaultClient\.Get)\s*\(|\b(?:curl|wget)\b)/i }
];
const productionFiles = listProductionGoFiles(companionDir);
expect(productionFiles.length).toBeGreaterThan(0);
for (const file of productionFiles) {
const source = fs.readFileSync(file, "utf8");
const relativeFile = path.relative(companionDir, file);
for (const forbidden of forbiddenPatterns) {
const match = source.match(forbidden.pattern);
expect(match ? `${relativeFile}: ${match[0]}` : null, forbidden.name).toBeNull();
}
}
});
it("covers the SCUM 4.1 bridge and lifecycle declarations", () => {
const manifestPath = path.join(pluginsRoot, "examples/scum-server-plugin/manifest.json");
const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8")) as {
@@ -450,7 +277,6 @@ describe("plugin manifest validation", () => {
expect(installAction.environment?.SERVER_TEMPLATE).toBe("scum-server");
expect(manifest.permissions).toEqual(expect.arrayContaining(["server.game-client.read", "server.game-client.command", "server.game-client.maintenance"]));
expect(manifest.gameClientBridge.commands.map((command) => command.type)).toEqual(expect.arrayContaining([
"companion.diagnostics",
"player.lookup",
"reward.deliver",
"player.notify",
@@ -459,7 +285,8 @@ describe("plugin manifest validation", () => {
"restart.prepare",
"maintenance.prepare"
]));
expect(manifest.gameClientBridge.snapshots.map((snapshot) => snapshot.type)).toEqual(expect.arrayContaining(["companion.health", "online.sessions", "players", "squads", "vehicles", "flags"]));
expect(manifest.gameClientBridge.snapshots.map((snapshot) => snapshot.type)).toEqual(expect.arrayContaining(["online.sessions", "players", "squads", "vehicles", "flags"]));
expect(manifest.runtimeProfiles?.clientManagers).toBeUndefined();
expect(manifest.gameClientBridge.queryTemplates.find((template) => template.key === "scum.vehicles")?.projections).toEqual(expect.arrayContaining([
expect.objectContaining({ collection: "scum_trade_goods", mergeExisting: true, fixedValues: expect.objectContaining({ catalogType: "vehicle", type: "21", typeName: "其他载具" }) })
]));
@@ -473,8 +300,8 @@ describe("plugin manifest validation", () => {
expect(manifest.fileWorkspace?.directories.map((directory) => `${directory.key}:${directory.scope}`)).toEqual(expect.arrayContaining(["scum-config:config", "scum-logs:logs"]));
expect(manifest.fileWorkspace?.files.map((file) => file.key)).toEqual(expect.arrayContaining(["scum-server-settings", "scum-admin-users", "scum-chat-log", "scum-performance-log"]));
expect(manifest.fileWorkspace?.configFields.map((field) => field.key)).toEqual(expect.arrayContaining(["server-name", "game-port", "query-port", "max-players", "welcome-message"]));
expect(manifest.runtimeProfiles?.lifecycleProfiles?.find((profile) => profile.key === "scum-client")?.capabilities).not.toContain("remote.run.rcon.command");
expect(manifest.runtimeProfiles?.logSources?.map((source) => source.key)).toEqual(expect.arrayContaining(["scum-chat-events", "scum-server-events", "scum-login-events", "scum-trade-events", "scum-client-events"]));
expect(manifest.runtimeProfiles?.lifecycleProfiles?.find((profile) => profile.key === "scum-client")).toBeUndefined();
expect(manifest.runtimeProfiles?.logSources?.map((source) => source.key)).toEqual(expect.arrayContaining(["scum-chat-events", "scum-server-events", "scum-login-events", "scum-trade-events"]));
});
it("declares bounded and permissioned SCUM bridge commands", () => {
@@ -492,7 +319,6 @@ describe("plugin manifest validation", () => {
};
};
const expected = {
"companion.diagnostics": { permission: "server.game-client.read" },
"player.lookup": { permission: "server.game-client.read" },
"reward.deliver": { permission: "server.game-client.command" },
"player.notify": { permission: "server.game-client.command" },
@@ -559,7 +385,7 @@ describe("plugin manifest validation", () => {
pages: Array<{ pageKey: string; snapshotTypes?: string[] }>;
};
};
const expectedTypes = ["companion.health", "online.sessions", "players", "squads", "vehicles", "flags"];
const expectedTypes = ["online.sessions", "players", "squads", "vehicles", "flags"];
const snapshotsByType = new Map(manifest.gameClientBridge.snapshots.map((snapshot) => [snapshot.type, snapshot]));
expect([...snapshotsByType.keys()]).toEqual(expect.arrayContaining(expectedTypes));
@@ -639,7 +465,7 @@ describe("plugin manifest validation", () => {
const sqliteTransport = manifest.runtimeProfiles?.transportProfiles?.find((profile) => profile.key === "scum-database");
expect(sqliteTransport).toMatchObject({ kind: "sqlite", targetKey: "scum-database" });
expect(sqliteTransport?.capabilities).toEqual(expect.arrayContaining(["remote.run.db.sqlite.query", "remote.run.db.sqlite.execute"]));
expect(manifest.runtimeProfiles?.dataTargets).toEqual(expect.arrayContaining([expect.objectContaining({ key: "scum-database", kind: "sqlite.snapshot", sourcePath: "SCUM/Saved/SaveFiles/SCUM.db", workspaceKey: "databases/scum-database" })]));
expect(manifest.runtimeProfiles?.dataTargets).toBeUndefined();
for (const key of expectedKeys) {
const template = templatesByKey.get(key)!;
expect(template.engine).toBe("sqlite");
@@ -703,22 +529,11 @@ describe("plugin manifest validation", () => {
});
it("aligns the SCUM Client Manager declaration with the real Go bootstrap", () => {
it("does not declare a SCUM client-manager profile", () => {
const manifest = JSON.parse(fs.readFileSync(path.join(pluginsRoot, "examples/scum-server-plugin/manifest.json"), "utf8")) as {
runtimeProfiles?: { clientManagers?: Array<{
key: string;
build?: { workspaceRef?: string; entryRef?: string };
configTemplates?: Array<{ key?: string; templateRef?: string; outputRef?: string }>;
deployment?: { arguments?: string[] };
health?: { intervalSeconds?: number; degradedAfterSeconds?: number; offlineAfterSeconds?: number };
}> };
runtimeProfiles?: { clientManagers?: unknown[] };
};
const manager = manifest.runtimeProfiles?.clientManagers?.find((profile) => profile.key === "scum-client-manager");
expect(manager?.build).toMatchObject({ entryRef: "main.go" });
expect(manager?.build).not.toHaveProperty("workspaceRef");
expect(manager?.configTemplates).toEqual([{ key: "client-config", templateRef: "config.yaml.example", outputRef: "config.yaml" }]);
expect(manager?.deployment?.arguments).toBeUndefined();
expect(manager?.health).toMatchObject({ intervalSeconds: 30, degradedAfterSeconds: 90, offlineAfterSeconds: 120 });
expect(manifest.runtimeProfiles?.clientManagers).toBeUndefined();
});
it("accepts the Minecraft server plugin manifest", () => {
@@ -953,7 +768,7 @@ describe("plugin SDK", () => {
it("builds safe game-client bridge requests without component transport material", () => {
const request = createGameClientBridgeQueueRequest({
profileKey: "scum-client",
profileKey: "example-client",
commandType: "diagnostic.ping",
payload: { message: "hello" },
idempotencyKey: "diagnostic-1",
@@ -1302,18 +1117,18 @@ describe("plugin SDK", () => {
action: "logs.backfill.request",
payload: { sourceKey: "chat-log", limit: "500" }
});
expect(createClientManagerRequest({ requestId: "client-1", context, operation: "generate", profileKey: "scum-client-manager", targetOS: "windows", targetArch: "amd64", idempotencyKey: "idem-client" })).toMatchObject({
expect(createClientManagerRequest({ requestId: "client-1", context, operation: "generate", profileKey: "example-client-manager", targetOS: "windows", targetArch: "amd64", idempotencyKey: "idem-client" })).toMatchObject({
action: "client-manager.request",
payload: { operation: "generate", profileKey: "scum-client-manager" }
payload: { operation: "generate", profileKey: "example-client-manager" }
});
expect(JSON.stringify(createClientManagerRequest({ requestId: "client-2", context, operation: "reset-key", profileKey: "scum-client-manager", idempotencyKey: "idem-reset" }))).not.toContain("secret");
expect(createClientManagerRequest({ requestId: "client-3", context, operation: "deploy", profileKey: "scum-client-manager", installationId: "cm-install-1", artifactId: "artifact-1", expectedDeploymentGeneration: 2, idempotencyKey: "idem-deploy" })).toMatchObject({
expect(JSON.stringify(createClientManagerRequest({ requestId: "client-2", context, operation: "reset-key", profileKey: "example-client-manager", idempotencyKey: "idem-reset" }))).not.toContain("secret");
expect(createClientManagerRequest({ requestId: "client-3", context, operation: "deploy", profileKey: "example-client-manager", installationId: "cm-install-1", artifactId: "artifact-1", expectedDeploymentGeneration: 2, idempotencyKey: "idem-deploy" })).toMatchObject({
action: "client-manager.request",
payload: { operation: "deploy", installationId: "cm-install-1", artifactId: "artifact-1", expectedDeploymentGeneration: "2" }
});
expect(parseClientManagerLifecycleStatus({
installationId: "cm-install-1",
profileKey: "scum-client-manager",
profileKey: "example-client-manager",
status: "online",
phase: "healthy",
targetOS: "windows",
@@ -1326,7 +1141,7 @@ describe("plugin SDK", () => {
})).toMatchObject({ installationId: "cm-install-1", deploymentGeneration: 2, actions: ["stop", "restart", "update", "uninstall"] });
expect(parseClientManagerLifecycleStatus({
installationId: "cm-install-1",
profileKey: "scum-client-manager",
profileKey: "example-client-manager",
status: "online",
deploymentGeneration: "2",
actions: "stop",