refactor(scum): declare protected run requests

This commit is contained in:
npc0-hue
2026-07-29 22:37:16 +08:00
parent d7465bfd32
commit 99be8f0f3a
28 changed files with 497 additions and 152 deletions
@@ -5,12 +5,13 @@ feature gate: no SCUM game, database, UE4SS build, or source revision controls
plugin availability.
The Companion declares availability from its server-bound typed ports and
runtime schema probes. Notification and fixed vehicle spawning can use a local
typed transport, but callers never supply a command, socket, credential, path,
or raw transport reply. Vehicle spawning creates only the private
`#spawnvehicle <vehicleCode>` template from the plugin allowlist.
runtime schema probes. Plugin-generated SQL, RCON, and management-program
request text is declared through the protected Platform-to-Run transport, not
executed by the Companion. Callers never receive a socket, credential, path,
or raw transport reply. A management-program request is not host OS shell
access.
Semantic events come from bounded Run stdout/stderr records. Unknown records
create diagnostics and never produce fabricated events. Run database access is
limited to typed allowlisted projections and safe mutations; DSNs, rows, SQL,
and credentials do not leave Run.
Semantic events come from bounded Run stdout/stderr console records. Unknown
records create diagnostics and never produce fabricated events. DSNs, rows,
connections, and credentials do not leave Run; request text is protected and
redacted from browser and audit projections.
@@ -68,7 +68,7 @@ func TestRuntimeAdapterUsesOnlyLogicalConfigValuesAndRedactsDiagnostics(t *testi
}
}
func TestVersionedUE4SSNotificationIsFixedTypedAndRedacted(t *testing.T) {
func TestUE4SSNotificationIsTypedAndRedacted(t *testing.T) {
port := &notificationPortFixture{accepted: true}
adapter := RuntimeAdapter{BoundServerID: "server-1", Notification: port}
result, err := adapter.NotifyPlayer(context.Background(), map[string]any{"playerId": "76561198000000001", "message": "Moon \"gift\""})
@@ -10,7 +10,8 @@ import (
)
// SafeAdapter is intentionally narrow: it receives typed values only and has
// no raw RCON, SQL, host-path, credential, or shell access.
// no direct transport, host-path, credential, or shell access. Protected SQL,
// RCON, and management-program text is forwarded to Run by Platform, not here.
type SafeAdapter interface {
ReadConfiguration(context.Context) (map[string]any, error)
PatchConfiguration(context.Context, map[string]any) (map[string]any, error)
@@ -47,6 +47,9 @@
"remote.run.process.start",
"remote.run.process.stop",
"remote.run.logs.transfer",
"remote.run.protected.sql",
"remote.run.protected.rcon",
"remote.run.program.command",
"client-manager.deploy",
"client-manager.control",
"client-manager.update",
@@ -67,7 +70,10 @@
"remote.run.files.write",
"remote.run.process.start",
"remote.run.process.stop",
"remote.run.logs.transfer"
"remote.run.logs.transfer",
"remote.run.protected.sql",
"remote.run.protected.rcon",
"remote.run.program.command"
],
"logTransfer": true
},
@@ -190,6 +196,39 @@
"resultSchemaRef": "schemas/bridge/game-state-patch.result.schema.json",
"timeoutSeconds": 120,
"maxPayloadBytes": 4096
},
{
"type": "database.request",
"title": "Execute approved SCUM database request",
"permission": "server.game-client.maintenance",
"approvalLevel": "platform-admin",
"payloadSchemaRef": "schemas/bridge/protected-request.payload.schema.json",
"resultSchemaRef": "schemas/bridge/protected-request.result.schema.json",
"timeoutSeconds": 120,
"maxPayloadBytes": 16384,
"protectedRequest": { "kind": "sql", "transportKey": "scum-database", "targetKey": "scum-database", "textField": "requestText", "maxTextBytes": 16384 }
},
{
"type": "management.rcon.request",
"title": "Execute approved SCUM management command",
"permission": "server.game-client.command",
"approvalLevel": "operator",
"payloadSchemaRef": "schemas/bridge/protected-request.payload.schema.json",
"resultSchemaRef": "schemas/bridge/protected-request.result.schema.json",
"timeoutSeconds": 120,
"maxPayloadBytes": 8192,
"protectedRequest": { "kind": "rcon", "transportKey": "scum-management", "targetKey": "scum-management", "textField": "requestText", "maxTextBytes": 8192 }
},
{
"type": "management.program.request",
"title": "Execute approved SCUM management program request",
"permission": "server.game-client.maintenance",
"approvalLevel": "platform-admin",
"payloadSchemaRef": "schemas/bridge/protected-request.payload.schema.json",
"resultSchemaRef": "schemas/bridge/protected-request.result.schema.json",
"timeoutSeconds": 120,
"maxPayloadBytes": 8192,
"protectedRequest": { "kind": "program", "transportKey": "scum-program", "targetKey": "scum-program", "textField": "requestText", "maxTextBytes": 8192 }
}
],
"snapshots": [
@@ -271,7 +310,10 @@
"vehicle.spawn",
"event.start",
"restart.prepare",
"maintenance.prepare"
"maintenance.prepare",
"database.request",
"management.rcon.request",
"management.program.request"
],
"snapshotTypes": ["companion.health", "online.sessions", "players", "squads", "vehicles", "flags"],
"featureKeys": ["config.manage", "player.intelligence", "reward.delivery", "state.patch", "vehicle.spawn", "trajectory.collect"]
@@ -482,7 +524,6 @@
],
"discoveryMarkers": [
{ "key": "scum-executable", "kind": "file.exists", "targetKey": "scum/server-executable", "required": true },
{ "key": "scum-version", "kind": "command.version", "targetKey": "scum/server-executable", "required": true },
{ "key": "scum-steam-app", "kind": "steam.app", "targetKey": "server/install-root", "expected": "3792580", "required": true },
{ "key": "scum-config", "kind": "file.exists", "targetKey": "scum/server-settings", "expected": "ServerSettings.ini", "required": true },
{ "key": "scum-game-port", "kind": "port.open", "targetKey": "game-port", "required": true },
@@ -490,7 +531,6 @@
],
"verificationChecks": [
{ "key": "executable", "kind": "executable.present", "targetKey": "scum/server-executable", "required": true },
{ "key": "version", "kind": "version.matches", "targetKey": "scum/server-executable", "required": true },
{ "key": "game-port", "kind": "port.bound", "targetKey": "game-port", "required": true },
{ "key": "config", "kind": "config.readable", "targetKey": "scum/server-settings", "required": true },
{ "key": "process", "kind": "process.healthy", "targetKey": "scum/server-executable", "required": true }
@@ -498,6 +538,22 @@
}
],
"logSources": [
{
"key": "scum-console-stdout",
"kind": "process.stdout",
"targetKey": "scum/server-process",
"streamKey": "scum.console.stdout",
"cursorKind": "sequence",
"retentionDays": 30
},
{
"key": "scum-console-stderr",
"kind": "process.stderr",
"targetKey": "scum/server-process",
"streamKey": "scum.console.stderr",
"cursorKind": "sequence",
"retentionDays": 30
},
{
"key": "scum-chat-events",
"kind": "file.tail",
@@ -686,6 +742,24 @@
"remote.rsync.read",
"remote.rsync.write"
]
},
{
"key": "scum-database",
"kind": "sqlite",
"targetKey": "scum-database",
"capabilities": ["remote.run.protected.sql"]
},
{
"key": "scum-management",
"kind": "rcon",
"targetKey": "scum-management",
"capabilities": ["remote.run.protected.rcon"]
},
{
"key": "scum-program",
"kind": "program",
"targetKey": "scum-program",
"capabilities": ["remote.run.program.command"]
}
],
"clientManagers": [
@@ -742,10 +816,6 @@
"offlineAfterSeconds": 120,
"requiredCapabilities": ["component.register", "component.heartbeat", "component.health", "component.control", "game-client.bridge", "logs.stream"]
},
"compatibility": {
"minimumVersion": "1.0.0",
"allowDowngrade": false
},
"updatePolicy": {
"strategy": "manual-staged",
"requireApproval": true,
@@ -0,0 +1,7 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"additionalProperties": false,
"required": ["requestText"],
"properties": { "requestText": { "type": "string", "minLength": 1, "maxLength": 16384 } }
}
@@ -0,0 +1,7 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"additionalProperties": false,
"required": ["outcome"],
"properties": { "outcome": { "enum": ["succeeded", "failed", "unknown"] }, "diagnostic": { "type": "string", "maxLength": 512 } }
}
@@ -299,7 +299,20 @@
"payloadSchemaRef": { "$ref": "#/$defs/relativeJsonRef" },
"resultSchemaRef": { "$ref": "#/$defs/relativeJsonRef" },
"timeoutSeconds": { "type": "integer", "minimum": 1, "maximum": 3600 },
"maxPayloadBytes": { "type": "integer", "minimum": 1, "maximum": 65536 }
"maxPayloadBytes": { "type": "integer", "minimum": 1, "maximum": 65536 },
"protectedRequest": { "$ref": "#/$defs/gameClientBridgeProtectedRequest" }
}
},
"gameClientBridgeProtectedRequest": {
"type": "object",
"required": ["kind", "transportKey", "targetKey", "textField", "maxTextBytes"],
"additionalProperties": false,
"properties": {
"kind": { "enum": ["sql", "rcon", "program"] },
"transportKey": { "$ref": "#/$defs/logicalKey" },
"targetKey": { "$ref": "#/$defs/logicalKey" },
"textField": { "type": "string", "pattern": "^[A-Za-z][A-Za-z0-9._-]{0,79}$" },
"maxTextBytes": { "type": "integer", "minimum": 1, "maximum": 16384 }
}
},
"gameClientBridgeSnapshot": {
@@ -384,6 +397,9 @@
"remote.run.db.sqlite.query",
"remote.run.logs.transfer",
"remote.run.rcon.command",
"remote.run.protected.sql",
"remote.run.protected.rcon",
"remote.run.program.command",
"client-manager.deploy",
"client-manager.control",
"client-manager.update",
@@ -605,7 +621,7 @@
"prerequisites": { "type": "array", "items": { "$ref": "#/$defs/runtimeServerPrerequisite" }, "maxItems": 16 },
"configMappings": { "type": "array", "items": { "$ref": "#/$defs/runtimeServerConfigMapping" }, "minItems": 1, "maxItems": 32, "uniqueItems": true },
"discoveryMarkers": { "type": "array", "items": { "$ref": "#/$defs/runtimeServerDiscoveryMarker" }, "minItems": 1, "maxItems": 32, "uniqueItems": true },
"verificationChecks": { "type": "array", "items": { "$ref": "#/$defs/runtimeServerVerificationCheck" }, "minItems": 5, "maxItems": 16, "uniqueItems": true }
"verificationChecks": { "type": "array", "items": { "$ref": "#/$defs/runtimeServerVerificationCheck" }, "minItems": 4, "maxItems": 16, "uniqueItems": true }
}
},
"runtimeServerPrerequisite": {
@@ -651,7 +667,7 @@
"additionalProperties": false,
"properties": {
"key": { "$ref": "#/$defs/logicalKey" },
"kind": { "enum": ["file", "ftp", "rsync", "mysql", "sqlite", "rcon"] },
"kind": { "enum": ["file", "ftp", "rsync", "mysql", "sqlite", "rcon", "program"] },
"targetKey": { "$ref": "#/$defs/logicalKey" },
"capabilities": { "type": "array", "items": { "$ref": "#/$defs/runCapability" }, "uniqueItems": true, "minItems": 1 }
}
@@ -789,7 +805,7 @@
"allOf": [
{
"if": { "required": ["deployment"] },
"then": { "required": ["version", "lifecycle", "health", "compatibility", "updatePolicy"] }
"then": { "required": ["version", "lifecycle", "health", "updatePolicy"] }
}
]
},
+35 -3
View File
@@ -393,7 +393,7 @@ function validateServerDeploymentProfiles(manifest: unknown): string[] {
if (mappingKeys.has(mapping.fieldKey)) errors.push(`${mappingLocation}.fieldKey: duplicate mapping`);
mappingKeys.add(mapping.fieldKey);
}
const requiredChecks = new Set(["executable.present", "version.matches", "port.bound", "config.readable", "process.healthy"]);
const requiredChecks = new Set(["executable.present", "port.bound", "config.readable", "process.healthy"]);
for (const check of profile.verificationChecks ?? []) {
if (check.required) requiredChecks.delete(check.kind);
}
@@ -601,7 +601,8 @@ export function validateGameClientBridgeCatalog(manifest: unknown): string[] {
if (typeof manifest !== "object" || manifest === null) {
return [];
}
type BridgeCommand = { type?: string; approvalLevel?: string; payloadSchemaRef?: string; resultSchemaRef?: string };
type ProtectedRequest = { kind?: string; transportKey?: string; targetKey?: string; textField?: string; maxTextBytes?: number };
type BridgeCommand = { type?: string; approvalLevel?: string; payloadSchemaRef?: string; resultSchemaRef?: string; protectedRequest?: ProtectedRequest };
type BridgeQueryTemplate = {
key?: string;
permission?: string;
@@ -693,13 +694,44 @@ export function validateGameClientBridgeCatalog(manifest: unknown): string[] {
for (const [index, command] of (bridge.commands ?? []).entries()) {
const location = `manifest.gameClientBridge.commands[${index}]`;
const type = command.type ?? "";
const unsafeTypeReason = unsafeGameClientBridgeCommandTypeReason(type);
const unsafeTypeReason = command.protectedRequest ? undefined : unsafeGameClientBridgeCommandTypeReason(type);
if (unsafeTypeReason) {
errors.push(`${location}.type: ${unsafeTypeReason}`);
}
if (!command.approvalLevel) {
errors.push(`${location}.approvalLevel: approval metadata is required`);
}
const protectedRequest = command.protectedRequest;
if (protectedRequest) {
if (!new Set(["sql", "rcon", "program"]).has(protectedRequest.kind ?? "")) {
errors.push(`${location}.protectedRequest.kind: must be sql, rcon, or program`);
}
if (!/^[A-Za-z][A-Za-z0-9._-]{0,79}$/.test(protectedRequest.textField ?? "")) {
errors.push(`${location}.protectedRequest.textField: must be a safe bounded field name`);
}
if (!Number.isInteger(protectedRequest.maxTextBytes) || (protectedRequest.maxTextBytes ?? 0) < 1 || (protectedRequest.maxTextBytes ?? 0) > 16384) {
errors.push(`${location}.protectedRequest.maxTextBytes: must be between 1 and 16384`);
}
const transport = transportProfiles.find((candidate) => candidate.key === protectedRequest.transportKey);
if (!transport) {
errors.push(`${location}.protectedRequest.transportKey: must reference a declared runtime transport profile`);
} else {
if (!protectedRequest.targetKey || protectedRequest.targetKey !== transport.targetKey) {
errors.push(`${location}.protectedRequest.targetKey: must match the declared runtime transport target`);
}
const expectedCapability = { sql: "remote.run.protected.sql", rcon: "remote.run.protected.rcon", program: "remote.run.program.command" }[protectedRequest.kind ?? ""];
if (protectedRequest.kind === "sql" && transport.kind !== "mysql" && transport.kind !== "sqlite") {
errors.push(`${location}.protectedRequest.transportKey: sql requests require mysql or sqlite transport`);
}
if ((protectedRequest.kind === "rcon" && transport.kind !== "rcon") || (protectedRequest.kind === "program" && transport.kind !== "program")) {
errors.push(`${location}.protectedRequest.transportKey: transport kind does not match protected request kind`);
}
if (expectedCapability && !transport.capabilities?.includes(expectedCapability)) {
errors.push(`${location}.protectedRequest.transportKey: is missing required protected transport capability`);
}
}
}
for (const [field, ref] of [["payloadSchemaRef", command.payloadSchemaRef], ["resultSchemaRef", command.resultSchemaRef]] as const) {
if (ref && !isSafeRelativeJsonRef(ref)) {
errors.push(`${location}.${field}: raw host paths and unsafe schema references are not allowed`);
+1 -1
View File
@@ -37,7 +37,7 @@ Run distribution, dependency, log backfill, and client-manager requests use `cre
Client-manager lifecycle requests remain Platform-mediated. A plugin declaration does not grant access by itself: Platform rechecks the installed plugin, server owner/administrator scope, runtime binding, assigned Run endpoint capabilities, current distribution target/revision/key generation, and durable installation state before dispatching a typed job.
Game-client plugin pages receive a host-provided `GameClientBridgePageClient`. The SDK defines status, command, result, snapshot, approval, and manifest declaration types but never creates its own HTTP client. Queue requests carry only a declared command type, logical profile key, bounded typed payload, expiry, priority, and idempotency key. Browser-facing types intentionally have no component session, component key, installation fence, host path, DSN, Run endpoint, socket, or storage credential fields.
Game-client plugin pages receive a host-provided `GameClientBridgePageClient`. The SDK defines status, command, result, snapshot, approval, and manifest declaration types but never creates its own HTTP client. Queue requests carry only a declared command type, logical profile key, bounded typed payload, expiry, priority, and idempotency key. A command may declare a protected `sql`, `rcon`, or management-program request: the plugin supplies only its one bounded text field and logical transport/target keys; Platform authorizes, approves, redacts, queues, and forwards it to Run. A management program is not host OS shell access. Browser-facing types intentionally have no component session, component key, installation fence, host path, DSN, Run endpoint, socket, or storage credential fields.
Production plugin lifecycle requests use `createProductionPluginLifecycleRequest`. Envelopes contain only plugin/server scope, enumerated operation, optional target version, confirmation, and idempotency key. Platform rechecks the manifest `productionLifecycle` declaration, dependency policy, disruptive approval, endpoint capacity, compatibility, and prior idempotency inputs before dispatch.
+14 -3
View File
@@ -211,7 +211,17 @@ export interface GamePluginRemoteAccess {
export type GameClientBridgeApprovalLevel = "none" | "operator" | "platform-admin";
export type GameClientBridgeApprovalState = "not_required" | "pending" | "approved" | "rejected";
export type GameClientBridgeCommandState = "pending" | "claimed" | "succeeded" | "failed" | "cancelled" | "expired";
export type GameClientBridgeCommandState = "pending" | "claimed" | "succeeded" | "failed" | "unknown" | "cancelled" | "expired";
export type GameClientBridgeProtectedRequestKind = "sql" | "rcon" | "program";
export interface GameClientBridgeProtectedRequestDeclaration {
kind: GameClientBridgeProtectedRequestKind;
transportKey: string;
targetKey: string;
textField: string;
maxTextBytes: number;
}
export interface GameClientBridgeCommandDeclaration {
type: string;
@@ -221,7 +231,8 @@ export interface GameClientBridgeCommandDeclaration {
payloadSchemaRef: string;
resultSchemaRef?: string;
timeoutSeconds: number;
maxPayloadBytes: number;
maxPayloadBytes: number;
protectedRequest?: GameClientBridgeProtectedRequestDeclaration;
}
export interface GameClientBridgeSnapshotDeclaration {
@@ -306,7 +317,7 @@ export interface GameClientBridgeStatus {
}
export interface GameClientBridgeCommandResult {
status: "succeeded" | "failed" | "cancelled";
status: "succeeded" | "failed" | "unknown" | "cancelled";
summary?: string;
payload?: Record<string, unknown>;
completedAt: string;
+35 -15
View File
@@ -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",