Remove SCUM server-management client path
This commit is contained in:
+1
-1
@@ -29,7 +29,7 @@ Platform may read plugin manifests, validate lifecycle/action declarations, pack
|
||||
|
||||
Platform may persist desired lifecycle state, last-known run reports, and UI projections. It must not treat those persisted fields as the authoritative source for the current machine/process state; current runtime status must come from the registered run endpoint's reports, heartbeats, supervised process facts, and job/log channels.
|
||||
|
||||
Log bodies from supervised stdout/stderr and plugin-declared file tails are opaque verbatim payloads. Platform may validate the transport envelope, sequence, checksum, and source binding, then persist, relay, and display the body unchanged. It must not inspect, parse, filter, redact, normalize, correlate, or derive player, login, user, or other plugin records from a log body. A plugin companion owns any game-specific parsing and its typed records arrive through the component boundary, never as a Platform log projection.
|
||||
Log bodies from supervised stdout/stderr and plugin-declared file tails are opaque verbatim payloads. Platform may validate the transport envelope, sequence, checksum, and source binding, then persist, relay, and display the body unchanged. It must not inspect, parse, filter, redact, normalize, correlate, or derive player, login, user, or other plugin records from a log body. A plugin-owned component owns any game-specific parsing and its typed records arrive through the component boundary, never as a Platform log projection.
|
||||
|
||||
Do not add platform service code that hardcodes a game's executable path, Steam app ID, SteamCMD command line, process name, default launch flags, or update policy. For SCUM specifically, `SCUMServer.exe`, app `3792580`, `+app_update 3792580 validate`, `-port`, `-MaxPlayers`, and `-log` must come from the SCUM plugin action assets or plugin-declared startup fields.
|
||||
|
||||
|
||||
@@ -760,7 +760,7 @@ func TestServerInstanceManagementAPI(t *testing.T) {
|
||||
postJSON[dto.RunEndpointResponse](t, router, "/api/v1/run/endpoints", validRunEndpointRequest())
|
||||
|
||||
ready := postJSONWithAuth[dto.ServerInstanceResponse](t, router, "/api/v1/server-instances", dto.ServerInstanceCreateRequest{
|
||||
ID: "server-management",
|
||||
ID: "server-ops",
|
||||
PluginID: "server.scum",
|
||||
RunEndpointID: "run-local",
|
||||
Name: "SCUM Ops",
|
||||
@@ -768,7 +768,7 @@ func TestServerInstanceManagementAPI(t *testing.T) {
|
||||
}, adminSession)
|
||||
|
||||
newName := "SCUM Ops Renamed"
|
||||
updated := putJSONWithAuth[dto.ServerInstanceResponse](t, router, "/api/v1/server-instances/server-management", dto.ServerInstanceUpdateRequest{Name: &newName}, adminSession)
|
||||
updated := putJSONWithAuth[dto.ServerInstanceResponse](t, router, "/api/v1/server-instances/server-ops", dto.ServerInstanceUpdateRequest{Name: &newName}, adminSession)
|
||||
if updated.Name != newName || updated.PluginID != ready.PluginID || updated.RunEndpointID != ready.RunEndpointID {
|
||||
t.Fatalf("unexpected server update: %+v", updated)
|
||||
}
|
||||
@@ -783,16 +783,16 @@ func TestServerInstanceManagementAPI(t *testing.T) {
|
||||
unsafeDelete := requestWithAuth(t, router, http.MethodDelete, "/api/v1/server-instances/"+running.ID, mustJSON(t, dto.ServerDeletionRequest{Password: "operator-local"}), adminSession)
|
||||
assertErrorResponse(t, unsafeDelete, http.StatusBadRequest, errorCodeValidation)
|
||||
|
||||
deleted := requestWithAuth(t, router, http.MethodDelete, "/api/v1/server-instances/server-management", mustJSON(t, dto.ServerDeletionRequest{Password: "operator-local"}), adminSession)
|
||||
deleted := requestWithAuth(t, router, http.MethodDelete, "/api/v1/server-instances/server-ops", mustJSON(t, dto.ServerDeletionRequest{Password: "operator-local"}), adminSession)
|
||||
assertStatus(t, deleted, http.StatusNoContent)
|
||||
activeList := getJSONWithAuth[dto.ServerInstanceListResponse](t, router, "/api/v1/server-instances", adminSession)
|
||||
for _, item := range activeList.Items {
|
||||
if item.ID == "server-management" {
|
||||
if item.ID == "server-ops" {
|
||||
t.Fatalf("deleted server should be hidden from normal list: %+v", activeList)
|
||||
}
|
||||
}
|
||||
deletedList := getJSONWithAuth[dto.ServerInstanceListResponse](t, router, "/api/v1/server-instances?state=deleted", adminSession)
|
||||
if deletedList.Count != 1 || deletedList.Items[0].ID != "server-management" || deletedList.Items[0].State != domain.ServerInstanceStateDeleted {
|
||||
if deletedList.Count != 1 || deletedList.Items[0].ID != "server-ops" || deletedList.Items[0].State != domain.ServerInstanceStateDeleted {
|
||||
t.Fatalf("expected explicit deleted filter to return deleted server, got %+v", deletedList)
|
||||
}
|
||||
forcedDelete := requestWithAuth(t, router, http.MethodDelete, "/api/v1/server-instances/"+running.ID, mustJSON(t, dto.ServerDeletionRequest{Password: "operator-local", Force: true, Confirmation: service.ServerDeletionForceConfirmation}), adminSession)
|
||||
|
||||
@@ -1215,6 +1215,9 @@ func (svc *CoreService) executeBridgeRemoteAccessRequest(sessionID string, base
|
||||
inputs[strings.TrimPrefix(key, "input.")] = value
|
||||
}
|
||||
}
|
||||
if capability == domain.JobCapabilityRemoteRunRCONCommand && strings.TrimSpace(inputs["command"]) != "" {
|
||||
return svc.executeBridgeSourceRCONCommand(sessionID, base, payload, inputs)
|
||||
}
|
||||
if capability == domain.JobCapabilityRemoteRunDBSQLiteQuery {
|
||||
templateKey := strings.TrimSpace(inputs["templateKey"])
|
||||
if templateKey == "" {
|
||||
@@ -1274,6 +1277,32 @@ func (svc *CoreService) executeBridgeRemoteAccessRequest(sessionID string, base
|
||||
return base
|
||||
}
|
||||
|
||||
func (svc *CoreService) executeBridgeSourceRCONCommand(sessionID string, base domain.PluginBridgeExecuteResponse, payload map[string]string, inputs map[string]string) domain.PluginBridgeExecuteResponse {
|
||||
dispatch, err := svc.DispatchSourceRCONCommandForSession(sessionID, domain.SourceRCONCommandRequest{
|
||||
ServerInstanceID: base.ServerInstanceID,
|
||||
Kind: domain.SourceRCONCommandKindCommand,
|
||||
Command: inputs["command"],
|
||||
IdempotencyKey: defaultBridgeValue(payload["idempotencyKey"], base.RequestID),
|
||||
})
|
||||
if err != nil {
|
||||
return bridgeExecutionError(base, err)
|
||||
}
|
||||
targetKey := strings.TrimSpace(payload["targetKey"])
|
||||
if job, getErr := svc.store.Jobs().Get(dispatch.JobID); getErr == nil && strings.TrimSpace(job.TargetKey) != "" {
|
||||
targetKey = job.TargetKey
|
||||
}
|
||||
base.Status = "queued"
|
||||
base.Result = map[string]string{
|
||||
"jobId": dispatch.JobID,
|
||||
"state": dispatch.Status,
|
||||
"capability": domain.JobCapabilityRemoteRunRCONCommand,
|
||||
"targetKey": targetKey,
|
||||
"serverInstanceId": dispatch.ServerInstanceID,
|
||||
"adapterKind": string(domain.RemoteAdapterRCON),
|
||||
}
|
||||
return base
|
||||
}
|
||||
|
||||
func findBridgeQueryTemplate(plugin domain.GamePlugin, routeKey string, templateKey string) (domain.GameClientBridgeQueryTemplateDeclaration, string) {
|
||||
pageFound := false
|
||||
pageAllowsTemplate := false
|
||||
|
||||
@@ -11,7 +11,7 @@ import (
|
||||
"browser.local/platform/repo"
|
||||
)
|
||||
|
||||
func TestSourceRCONDispatchUsesOneTimeRedactedInput(t *testing.T) {
|
||||
func TestSourceRCONDispatchUsesOneTimeOpaqueInput(t *testing.T) {
|
||||
svc, session, runSession, instance := newSourceRCONFixture(t)
|
||||
request := domain.SourceRCONCommandRequest{ServerInstanceID: instance.ID, Kind: domain.SourceRCONCommandKindChat, ChatType: 4, Message: `Bounty "claimed"`, TargetSteamID: "76561198000000001", IdempotencyKey: "rcon-chat-1"}
|
||||
dispatch, err := svc.DispatchSourceRCONCommandForSession(session, request)
|
||||
@@ -150,14 +150,70 @@ func TestSourceRCONDispatchSelectsDeclaredProfileWithoutManualBinding(t *testing
|
||||
}
|
||||
}
|
||||
|
||||
func TestSourceRCONDispatchFallsBackFromCustomClientBinding(t *testing.T) {
|
||||
func TestBridgeRemoteAccessDispatchesDeclaredSourceRCONCommand(t *testing.T) {
|
||||
svc, session, runSession, instance := newSourceRCONFixture(t)
|
||||
plugin, err := svc.store.GamePlugins().Get(instance.PluginID)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
plugin.BridgeActions = []string{string(domain.PluginBridgeActionRemoteAccessRequest)}
|
||||
plugin.Pages = []domain.GamePluginPage{{Key: "remote", Title: "Remote", Path: "/remote", Permissions: []string{"server.remote.access"}, BridgeActions: []string{string(domain.PluginBridgeActionRemoteAccessRequest)}}}
|
||||
if err := svc.store.GamePlugins().Update(plugin); err != nil {
|
||||
t.Fatalf("update bridge-capable RCON plugin: %v", err)
|
||||
}
|
||||
|
||||
response, err := svc.ExecutePluginBridgeAction(session, domain.PluginBridgeExecuteRequest{
|
||||
RequestID: "bridge-rcon-1",
|
||||
PluginID: plugin.ID,
|
||||
RouteKey: "remote",
|
||||
ServerInstanceID: instance.ID,
|
||||
Action: domain.PluginBridgeActionRemoteAccessRequest,
|
||||
Payload: map[string]string{
|
||||
"capability": domain.JobCapabilityRemoteRunRCONCommand,
|
||||
"declarationKey": "rcon",
|
||||
"targetKey": "rcon",
|
||||
"idempotencyKey": "bridge-rcon-1",
|
||||
"input.command": "#ListPlayers",
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("execute bridge RCON command: %v", err)
|
||||
}
|
||||
if response.Status != "queued" || response.Result["adapterKind"] != string(domain.RemoteAdapterRCON) || response.Result["targetKey"] != "rcon" || response.Result["capability"] != domain.JobCapabilityRemoteRunRCONCommand {
|
||||
t.Fatalf("expected queued source RCON bridge response, got %+v", response)
|
||||
}
|
||||
job, err := svc.store.Jobs().Get(response.Result["jobId"])
|
||||
if err != nil {
|
||||
t.Fatalf("get bridge RCON job: %v", err)
|
||||
}
|
||||
if job.ExecutionInput.SourceRCON == nil || !strings.HasPrefix(job.InputRef, "input://source-rcon/") || len(job.ExecutionInput.Inputs) != 0 {
|
||||
t.Fatalf("expected source RCON one-time input job, got %+v", job)
|
||||
}
|
||||
claim, err := svc.ClaimRunJob(domain.RunJobClaim{RunEndpointID: "run-local", SessionToken: runSession, Capabilities: []string{domain.JobCapabilityRemoteRunRCONCommand}, Capacity: domain.RunCapacity{MaxJobs: 1}})
|
||||
if err != nil || !claim.HasJob || claim.Job == nil || claim.Job.JobID != job.ID {
|
||||
t.Fatalf("claim bridge RCON job: claim=%+v err=%v", claim, err)
|
||||
}
|
||||
ack, err := svc.AckRunJob(domain.RunJobAck{RunEndpointID: "run-local", SessionToken: runSession, JobID: claim.Job.JobID, LeaseToken: claim.Job.LeaseToken, Attempt: claim.Job.Attempt, Message: "accepted"})
|
||||
if err != nil || !ack.Accepted {
|
||||
t.Fatalf("ack bridge RCON job: ack=%+v err=%v", ack, err)
|
||||
}
|
||||
input, err := svc.GetSourceRCONExecutionInput(domain.SourceRCONExecutionInputRequest{RunEndpointID: "run-local", SessionToken: runSession, JobID: job.ID, LeaseToken: ack.Job.LeaseToken, Attempt: ack.Job.Attempt})
|
||||
if err != nil {
|
||||
t.Fatalf("consume bridge RCON input: %v", err)
|
||||
}
|
||||
if input.Command != "#ListPlayers" {
|
||||
t.Fatalf("expected bridge RCON command to remain verbatim, got %q", input.Command)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSourceRCONDispatchFallsBackFromIncompatibleLegacyBinding(t *testing.T) {
|
||||
svc, session, _, instance := newSourceRCONFixture(t)
|
||||
plugin, err := svc.store.GamePlugins().Get(instance.PluginID)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
plugin.RuntimeProfiles.LifecycleProfiles[0].Key = "run-local"
|
||||
plugin.RuntimeProfiles.LifecycleProfiles = append(plugin.RuntimeProfiles.LifecycleProfiles, domain.RuntimeLifecycleProfile{Key: "scum-client", Mode: "custom-client", Capabilities: []string{"client-manager.deploy", "client-manager.control", "logs.read"}, ClientManagerRef: "scum-client-manager", Platforms: []string{"windows"}})
|
||||
plugin.RuntimeProfiles.LifecycleProfiles = append(plugin.RuntimeProfiles.LifecycleProfiles, domain.RuntimeLifecycleProfile{Key: "legacy-local", Mode: "local-process", Capabilities: []string{"logs.read"}, Platforms: []string{"windows"}})
|
||||
if err := svc.store.GamePlugins().Update(plugin); err != nil {
|
||||
t.Fatalf("update plugin profiles: %v", err)
|
||||
}
|
||||
@@ -165,23 +221,23 @@ func TestSourceRCONDispatchFallsBackFromCustomClientBinding(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("get runtime binding: %v", err)
|
||||
}
|
||||
binding.ProfileKey = "scum-client"
|
||||
binding.Mode = "custom-client"
|
||||
binding.Bindings = map[string]string{"scum-client-manager": "runtime-client-manager"}
|
||||
binding.ProfileKey = "legacy-local"
|
||||
binding.Mode = "local-process"
|
||||
binding.Bindings = map[string]string{"logs": "runtime-logs"}
|
||||
if err := svc.store.RuntimeBindings().Update(binding); err != nil {
|
||||
t.Fatalf("point binding at custom client profile: %v", err)
|
||||
t.Fatalf("point binding at incompatible profile: %v", err)
|
||||
}
|
||||
|
||||
dispatch, err := svc.DispatchSourceRCONCommandForSession(session, domain.SourceRCONCommandRequest{ServerInstanceID: instance.ID, Kind: domain.SourceRCONCommandKindCommand, Command: "#ListPlayers", IdempotencyKey: "rcon-custom-client-binding"})
|
||||
dispatch, err := svc.DispatchSourceRCONCommandForSession(session, domain.SourceRCONCommandRequest{ServerInstanceID: instance.ID, Kind: domain.SourceRCONCommandKindCommand, Command: "#ListPlayers", IdempotencyKey: "rcon-incompatible-binding"})
|
||||
if err != nil {
|
||||
t.Fatalf("dispatch RCON with custom-client binding fallback: %v", err)
|
||||
t.Fatalf("dispatch RCON with incompatible binding fallback: %v", err)
|
||||
}
|
||||
job, err := svc.store.Jobs().Get(dispatch.JobID)
|
||||
if err != nil {
|
||||
t.Fatalf("get RCON job: %v", err)
|
||||
}
|
||||
if job.ExecutionInput.WorkspaceScope != "run-local" || job.TargetKey != "rcon" || job.ExecutionInput.RemoteAdapterKey != "rcon" {
|
||||
t.Fatalf("expected source RCON to use run-local despite custom-client binding, got %+v", job)
|
||||
t.Fatalf("expected source RCON to use run-local despite incompatible binding, got %+v", job)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user