refactor(scum): use runtime capability probes

This commit is contained in:
npc0-hue
2026-07-29 18:34:16 +08:00
parent 03339fb3e8
commit d7465bfd32
26 changed files with 530 additions and 533 deletions
@@ -1,9 +1,8 @@
# SCUM Companion One-Shot Smoke
The currently pinned UE4SS reference does not provide semantic player or map
events. See [UE4SS_CAPABILITY.md](UE4SS_CAPABILITY.md) for the supported
`SendChat` evidence and the exact unavailable contracts; this Companion never
infers those events from arbitrary log lines.
Run stdout/stderr records provide bounded semantic player events. See
[UE4SS_CAPABILITY.md](UE4SS_CAPABILITY.md) for the runtime boundary; this
Companion never infers events from arbitrary log lines.
This plugin-owned fixture proves the Platform Client Manager and Game Client Bridge integration without adding SCUM behavior to Run. The command registers the deployed component, sends one heartbeat, claims at most one command, processes only `companion.diagnostics`, and uploads one typed `companion.health` snapshot.
@@ -1,47 +1,16 @@
# Pinned UE4SS capability evidence
# Runtime capability boundary
This Companion has inspected the read-only reference repository at commit
`bae91527355f14faa63c1df65f742cc48594ba1b` (`scum_simple_rcon_ue4ss` v0.1.0,
verified build target RE-UE4SS 3.0.1).
UE4SS is only a possible Companion-local implementation detail. It is not a
feature gate: no SCUM game, database, UE4SS build, or source revision controls
plugin availability.
## Verified capability
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.
The source implements a game-thread `SendChat <type 0-7> "message"
[SteamID64]` path. A targeted send accepts only a 17-digit SteamID64 that
resolves to a real, currently online `ConZPlayerController` with a live
`UNetConnection`; it fails closed when the reflected
`MiscStatics:SendChatLineToPlayer` schema differs. This can support a
version-bound, typed `player.notify` adapter when the deployed Companion is
given a platform-authorized typed transport. `VersionedAdapter` implements
that contract only for this exact source revision and UE4SS 3.0.1, with fixed
chat type `4`; it cannot accept arbitrary RCON text. Its generated command
text is private transport/audit data and never appears in a command result.
The same pinned `ScumBridge::trim_command` implementation removes at most one
leading `#` before dispatch. The authorized `vehicle.spawn` adapter preserves
the required `#spawnvehicle <vehicleCode>` template internally, supplies it
only to a Companion-local typed transport port, and never treats the source's
raw response as a stable acknowledgement. Its isolated port fixture supplies
the bounded success/failure/unknown receipt used by the adapter tests.
## Explicitly unavailable
The reference contains no versioned server-side producer or documented API for:
- successful player login/logout records;
- raw network identity/fingerprint values suitable for correlation;
- player or vehicle position, or player/vehicle transitions;
- item/reward delivery; or
- skill/attribute read, safe-window checking, or mutation.
The fixed vehicle-spawn exception does not change these unavailable
capabilities and does not authorize arbitrary RCON commands, arguments,
targets, credentials, direct sockets, SQL, shell execution, or response
projection.
Therefore the Companion must not parse invented `LOGIN`/`LOGOUT` lines, upload
semantic events, correlate network identifiers, or claim trajectory, reward,
or game-state-patch support from this reference. The missing contract is a
version-pinned UE4SS extension/API that defines the event or operation schema,
identity binding, acknowledgement/result semantics, and non-production
integration fixture for each capability.
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.
@@ -9,30 +9,62 @@ import (
"unicode/utf8"
)
var errAdapterUnsupported = errors.New("versioned adapter is unsupported")
var errAdapterUnsupported = errors.New("runtime capability is unavailable")
// AuthorizedConfigPort is supplied by a version-bound Companion integration.
// It exposes logical configuration values only: never a host path, connection
// string, credential, arbitrary command, or direct database handle.
// AuthorizedConfigPort is supplied through the platform-authorized Run channel.
// It exposes logical, allowlisted configuration values only; it never exposes a
// path, DSN, credential, arbitrary command, or database handle.
type AuthorizedConfigPort interface {
ReadConfig(context.Context) (map[string]string, error)
ApplyConfigPatch(ctx context.Context, revision string, fields []ConfigFieldPatch) (map[string]string, error)
ApplyConfigPatch(context.Context, string, []ConfigFieldPatch) (map[string]string, error)
}
type ConfigFieldPatch struct {
Key string
Value string
}
const (
UE4SSReferenceRevision = "bae91527355f14faa63c1df65f742cc48594ba1b"
UE4SSReferenceBuild = "3.0.1"
fixedNotificationType = 4
)
// AuthorizedGameDataPort is a typed, Run-owned read/patch boundary. Implementations
// must probe their local schema, use field allowlists and safe windows, and return
// bounded snapshots rather than rows or connection details.
type AuthorizedGameDataPort interface {
ReadPlayerState(context.Context, string, []string) (PlayerStateSnapshot, error)
ApplyPlayerState(context.Context, PlayerStatePatch) (PlayerStateSnapshot, error)
}
type PlayerStateSnapshot struct {
PlayerID string
StateVersion string
SafeWindow bool
Fields map[string]float64
}
type PlayerStatePatch struct {
PlayerID string
ExpectedStateVersion string
Fields []StateFieldPatch
}
type StateFieldPatch struct {
Key string
Before float64
After float64
}
// AuthorizedRewardPort accepts only a frozen grant and typed items. It cannot
// receive SQL, a raw database row, a shell command, an RCON command, or secrets.
type AuthorizedRewardPort interface {
DeliverReward(context.Context, RewardGrant) (DeliveryReceipt, error)
}
type RewardGrant struct {
GrantID string
PlayerID string
Items []RewardItem
}
type RewardItem struct {
CatalogCode string
Quantity int
}
type DeliveryReceipt struct{ Outcome string }
const fixedNotificationType = 4
// UE4SSNotificationPort is implemented only by a Companion-local,
// platform-authorized transport for the pinned UE4SS build. It receives a
// fixed typed notification, never a raw RCON command, credential, socket, or
// host path. Its private audit text is not part of command result payloads.
type UE4SSNotificationPort interface {
SendPlayerNotification(context.Context, ue4SSPlayerNotification) (UE4SSNotificationReceipt, error)
}
@@ -44,11 +76,6 @@ type ue4SSPlayerNotification struct {
chatType int
protectedAuditCommand string
}
// UE4SSVehicleSpawnPort is a Companion-local, platform-authorized transport
// for one fixed template. It accepts no raw command text, socket, credential,
// or host path. Implementations remain in this Companion package so the
// protected audit template cannot cross a general transport boundary.
type UE4SSVehicleSpawnPort interface {
SpawnVehicle(context.Context, ue4SSVehicleSpawn) (UE4SSVehicleSpawnReceipt, error)
}
@@ -67,31 +94,33 @@ type ue4SSVehicleSpawn struct {
protectedAuditCommand string
}
type VersionedAdapter struct {
BoundServerID string
ServerVersion string
UE4SSBuild string
UE4SSReferenceRevision string
Config AuthorizedConfigPort
Notification UE4SSNotificationPort
VehicleSpawn UE4SSVehicleSpawnPort
DiagnosticsState map[string]string
// RuntimeAdapter is bound to one server. Availability is discovered from its
// configured typed ports and declared capabilities. A failed probe affects
// only its operation.
type RuntimeAdapter struct {
BoundServerID string
Config AuthorizedConfigPort
GameData AuthorizedGameDataPort
Rewards AuthorizedRewardPort
Notification UE4SSNotificationPort
VehicleSpawn UE4SSVehicleSpawnPort
DiagnosticsState map[string]string
}
func (adapter VersionedAdapter) ServerBinding() string { return adapter.BoundServerID }
func (adapter RuntimeAdapter) ServerBinding() string { return adapter.BoundServerID }
func (adapter VersionedAdapter) ReadConfiguration(ctx context.Context) (map[string]any, error) {
if !supportedAdapterVersion(adapter.ServerVersion) || adapter.Config == nil {
func (adapter RuntimeAdapter) ReadConfiguration(ctx context.Context) (map[string]any, error) {
if adapter.Config == nil {
return nil, errAdapterUnsupported
}
fields, err := adapter.Config.ReadConfig(ctx)
if err != nil {
return nil, err
}
return map[string]any{"version": adapter.ServerVersion, "fields": redactConfigValues(fields)}, nil
return map[string]any{"fields": redactConfigValues(fields)}, nil
}
func (adapter VersionedAdapter) PatchConfiguration(ctx context.Context, payload map[string]any) (map[string]any, error) {
if !supportedAdapterVersion(adapter.ServerVersion) || adapter.Config == nil {
func (adapter RuntimeAdapter) PatchConfiguration(ctx context.Context, payload map[string]any) (map[string]any, error) {
if adapter.Config == nil {
return nil, errAdapterUnsupported
}
revision, _ := payload["revision"].(string)
@@ -113,10 +142,10 @@ func (adapter VersionedAdapter) PatchConfiguration(ctx context.Context, payload
if err != nil {
return nil, err
}
return map[string]any{"version": adapter.ServerVersion, "appliedFields": redactConfigValues(applied)}, nil
return map[string]any{"appliedFields": redactConfigValues(applied)}, nil
}
func (adapter VersionedAdapter) Diagnostics(context.Context) (map[string]any, error) {
state := map[string]any{"version": adapter.ServerVersion, "adapter": "version-bound", "configuration": supportedAdapterVersion(adapter.ServerVersion)}
func (adapter RuntimeAdapter) Diagnostics(context.Context) (map[string]any, error) {
state := map[string]any{"adapter": "runtime-capability"}
for key, value := range adapter.DiagnosticsState {
if safeDiagnosticField(key, value) {
state[key] = value
@@ -124,14 +153,53 @@ func (adapter VersionedAdapter) Diagnostics(context.Context) (map[string]any, er
}
return state, nil
}
func (VersionedAdapter) PatchGameState(context.Context, map[string]any) (map[string]any, error) {
return nil, errAdapterUnsupported
func (adapter RuntimeAdapter) PatchGameState(ctx context.Context, payload map[string]any) (map[string]any, error) {
if adapter.GameData == nil {
return nil, errAdapterUnsupported
}
playerID, _ := payload["playerId"].(string)
expected, _ := payload["expectedStateVersion"].(string)
raw, _ := payload["changes"].([]any)
fields, err := statePatchFields(raw)
if err != nil {
return nil, err
}
before, err := adapter.GameData.ReadPlayerState(ctx, playerID, statePatchKeys(fields))
if err != nil {
return nil, err
}
if before.PlayerID != playerID || before.StateVersion != expected || !before.SafeWindow || !stateMatches(before.Fields, fields) {
return map[string]any{"outcome": "failed"}, nil
}
after, err := adapter.GameData.ApplyPlayerState(ctx, PlayerStatePatch{PlayerID: playerID, ExpectedStateVersion: expected, Fields: fields})
if err != nil {
return map[string]any{"outcome": "unknown"}, nil
}
confirmed, err := adapter.GameData.ReadPlayerState(ctx, playerID, statePatchKeys(fields))
if err != nil || after.PlayerID != playerID || !stateApplied(confirmed.Fields, fields) {
return map[string]any{"outcome": "unknown"}, nil
}
return map[string]any{"outcome": "succeeded", "changedFields": len(fields)}, nil
}
func (VersionedAdapter) DeliverReward(context.Context, map[string]any) (map[string]any, error) {
return nil, errAdapterUnsupported
func (adapter RuntimeAdapter) DeliverReward(ctx context.Context, payload map[string]any) (map[string]any, error) {
if adapter.Rewards == nil {
return nil, errAdapterUnsupported
}
grant, err := rewardGrant(payload)
if err != nil {
return nil, err
}
receipt, err := adapter.Rewards.DeliverReward(ctx, grant)
if err != nil || receipt.Outcome == "unknown" {
return map[string]any{"outcome": "unknown"}, nil
}
if receipt.Outcome != "delivered" {
return map[string]any{"outcome": "failed"}, nil
}
return map[string]any{"outcome": "delivered"}, nil
}
func (adapter VersionedAdapter) NotifyPlayer(ctx context.Context, payload map[string]any) (map[string]any, error) {
if !adapter.supportsPinnedUE4SS() || adapter.Notification == nil {
func (adapter RuntimeAdapter) NotifyPlayer(ctx context.Context, payload map[string]any) (map[string]any, error) {
if adapter.BoundServerID == "" || adapter.Notification == nil {
return nil, errAdapterUnsupported
}
playerID, playerOK := payload["playerId"].(string)
@@ -144,13 +212,10 @@ func (adapter VersionedAdapter) NotifyPlayer(ctx context.Context, payload map[st
if err != nil {
return nil, fmt.Errorf("notification transport failed")
}
if !receipt.Accepted {
return map[string]any{"accepted": false, "message": "notification was not accepted"}, nil
}
return map[string]any{"accepted": true, "message": "notification accepted for online recipient"}, nil
return map[string]any{"accepted": receipt.Accepted}, nil
}
func (adapter VersionedAdapter) SpawnVehicle(ctx context.Context, payload map[string]any) (map[string]any, error) {
if !adapter.supportsPinnedUE4SS() || adapter.VehicleSpawn == nil {
func (adapter RuntimeAdapter) SpawnVehicle(ctx context.Context, payload map[string]any) (map[string]any, error) {
if adapter.BoundServerID == "" || adapter.VehicleSpawn == nil {
return nil, errAdapterUnsupported
}
vehicleCode, ok := payload["vehicleCode"].(string)
@@ -170,22 +235,15 @@ func (adapter VersionedAdapter) SpawnVehicle(ctx context.Context, payload map[st
}
return map[string]any{"outcome": "succeeded"}, nil
}
func (adapter VersionedAdapter) supportsPinnedUE4SS() bool {
// The pinned source reflects SendChatLineToPlayer at runtime and fails
// closed on a schema change, so no unverified SCUM-version mapping is
// embedded here. The dispatcher still requires a discovered server version.
return adapter.BoundServerID != "" && supportedAdapterVersion(adapter.ServerVersion) && adapter.UE4SSBuild == UE4SSReferenceBuild && adapter.UE4SSReferenceRevision == UE4SSReferenceRevision
}
func newUE4SSPlayerNotification(serverID, playerID, message string) (ue4SSPlayerNotification, error) {
if strings.TrimSpace(serverID) == "" || !steamID64(playerID) || !validNotificationMessage(message) {
return ue4SSPlayerNotification{}, fmt.Errorf("invalid typed UE4SS notification")
return ue4SSPlayerNotification{}, fmt.Errorf("invalid typed notification")
}
return ue4SSPlayerNotification{ServerID: serverID, RecipientSteamID: playerID, Message: message, chatType: fixedNotificationType, protectedAuditCommand: "SendChat 4 \"" + escapeUE4SSChatMessage(message) + "\" " + playerID}, nil
}
func newUE4SSVehicleSpawn(serverID, vehicleCode string) (ue4SSVehicleSpawn, error) {
if strings.TrimSpace(serverID) == "" || !supportedVehicleSpawnCode(vehicleCode) {
return ue4SSVehicleSpawn{}, fmt.Errorf("invalid typed UE4SS vehicle spawn")
return ue4SSVehicleSpawn{}, fmt.Errorf("invalid typed vehicle spawn")
}
return ue4SSVehicleSpawn{ServerID: serverID, VehicleCode: vehicleCode, protectedAuditCommand: "#spawnvehicle " + vehicleCode}, nil
}
@@ -214,8 +272,6 @@ func validNotificationMessage(value string) bool {
func escapeUE4SSChatMessage(value string) string {
return strings.NewReplacer("\\", "\\\\", "\"", "\\\"").Replace(value)
}
func supportedAdapterVersion(version string) bool { return version == "0.9.700.90357" }
func supportedVehicleSpawnCode(value string) bool {
return map[string]bool{"BPC_Laika_C": true, "BPC_WolfsWagen_C": true}[value]
}
@@ -240,3 +296,68 @@ func safeDiagnosticField(key, value string) bool {
lowered := strings.ToLower(key + "=" + value)
return !strings.Contains(lowered, "path") && !strings.Contains(lowered, "credential") && !strings.Contains(lowered, "password") && !strings.Contains(lowered, "bearer ") && !strings.Contains(lowered, "rcon") && !strings.Contains(lowered, "sql") && !strings.Contains(lowered, "://")
}
func statePatchFields(raw []any) ([]StateFieldPatch, error) {
if len(raw) == 0 || len(raw) > 8 {
return nil, fmt.Errorf("state patch payload is invalid")
}
fields := make([]StateFieldPatch, 0, len(raw))
for _, value := range raw {
item, ok := value.(map[string]any)
if !ok {
return nil, fmt.Errorf("state patch payload is invalid")
}
key, _ := item["fieldKey"].(string)
before, beforeOK := item["before"].(float64)
after, afterOK := item["after"].(float64)
if key == "" || !beforeOK || !afterOK {
return nil, fmt.Errorf("state patch payload is invalid")
}
fields = append(fields, StateFieldPatch{Key: key, Before: before, After: after})
}
return fields, nil
}
func statePatchKeys(fields []StateFieldPatch) []string {
keys := make([]string, 0, len(fields))
for _, field := range fields {
keys = append(keys, field.Key)
}
return keys
}
func stateMatches(values map[string]float64, fields []StateFieldPatch) bool {
for _, field := range fields {
if values[field.Key] != field.Before {
return false
}
}
return true
}
func stateApplied(values map[string]float64, fields []StateFieldPatch) bool {
for _, field := range fields {
if values[field.Key] != field.After {
return false
}
}
return true
}
func rewardGrant(payload map[string]any) (RewardGrant, error) {
grantID, grantOK := payload["grantId"].(string)
playerID, playerOK := payload["playerId"].(string)
raw, itemsOK := payload["items"].([]any)
if !grantOK || !playerOK || !itemsOK || len(raw) == 0 || len(raw) > 8 {
return RewardGrant{}, fmt.Errorf("reward payload is invalid")
}
items := make([]RewardItem, 0, len(raw))
for _, value := range raw {
item, ok := value.(map[string]any)
if !ok {
return RewardGrant{}, fmt.Errorf("reward payload is invalid")
}
code, codeOK := item["catalogCode"].(string)
quantity, quantityOK := item["quantity"].(float64)
if !codeOK || !quantityOK || quantity < 1 || quantity > 99 {
return RewardGrant{}, fmt.Errorf("reward payload is invalid")
}
items = append(items, RewardItem{CatalogCode: code, Quantity: int(quantity)})
}
return RewardGrant{GrantID: grantID, PlayerID: playerID, Items: items}, nil
}
@@ -89,8 +89,8 @@ func TestSupportedAdaptersDispatchThroughIsolatedTypedPorts(t *testing.T) {
spawnReceipts: []UE4SSVehicleSpawnReceipt{{Outcome: UE4SSVehicleSpawnAccepted}, {Outcome: UE4SSVehicleSpawnRejected}, {Outcome: UE4SSVehicleSpawnUnknown}},
spawnErrors: []error{nil, nil, errors.New("receipt unavailable")},
}
adapter := VersionedAdapter{BoundServerID: "server-1", ServerVersion: "0.9.700.90357", UE4SSBuild: UE4SSReferenceBuild, UE4SSReferenceRevision: UE4SSReferenceRevision, Config: port, Notification: port, VehicleSpawn: port}
registry := NewHandlerRegistry(HandlerAvailability{BoundServerID: "server-1", ServerVersion: "0.9.700.90357", Approved: true, Capabilities: map[string]bool{"config.read": true, "config.patch": true, "player.notify": true, "vehicle.spawn": true}}, adapter)
adapter := RuntimeAdapter{BoundServerID: "server-1", Config: port, Notification: port, VehicleSpawn: port}
registry := NewHandlerRegistry(HandlerAvailability{BoundServerID: "server-1", Approved: true, Capabilities: map[string]bool{"config.read": true, "config.patch": true, "player.notify": true, "vehicle.spawn": true}}, adapter)
gateway := &isolatedDispatchGateway{commands: []ClaimedCommand{
e2eClaim("config-read", "config.read", map[string]any{}, stamp),
e2eClaim("config-patch", "config.patch", map[string]any{"revision": "r1", "fields": []any{map[string]any{"key": "ServerName", "value": "Moonlight"}}}, stamp),
@@ -129,16 +129,15 @@ func TestSupportedAdaptersDispatchThroughIsolatedTypedPorts(t *testing.T) {
}
}
func TestSupportedAdaptersFailClosedForBindingApprovalVersionAndCapability(t *testing.T) {
func TestSupportedAdaptersFailClosedForBindingApprovalAndCapability(t *testing.T) {
stamp := time.Date(2026, time.July, 29, 12, 0, 0, 0, time.UTC)
for name, testCase := range map[string]struct {
availability HandlerAvailability
adapter VersionedAdapter
adapter RuntimeAdapter
}{
"binding": {availability: HandlerAvailability{BoundServerID: "server-1", ServerVersion: "0.9.700.90357", Approved: true, Capabilities: map[string]bool{"vehicle.spawn": true}}, adapter: VersionedAdapter{BoundServerID: "server-2", ServerVersion: "0.9.700.90357", UE4SSBuild: UE4SSReferenceBuild, UE4SSReferenceRevision: UE4SSReferenceRevision}},
"approval": {availability: HandlerAvailability{BoundServerID: "server-1", ServerVersion: "0.9.700.90357", Approved: false, Capabilities: map[string]bool{"vehicle.spawn": true}}, adapter: VersionedAdapter{BoundServerID: "server-1", ServerVersion: "0.9.700.90357", UE4SSBuild: UE4SSReferenceBuild, UE4SSReferenceRevision: UE4SSReferenceRevision}},
"capability": {availability: HandlerAvailability{BoundServerID: "server-1", ServerVersion: "0.9.700.90357", Approved: true, Capabilities: map[string]bool{}}, adapter: VersionedAdapter{BoundServerID: "server-1", ServerVersion: "0.9.700.90357", UE4SSBuild: UE4SSReferenceBuild, UE4SSReferenceRevision: UE4SSReferenceRevision}},
"version": {availability: HandlerAvailability{BoundServerID: "server-1", ServerVersion: "0.9.700.90357", Approved: true, Capabilities: map[string]bool{"vehicle.spawn": true}}, adapter: VersionedAdapter{BoundServerID: "server-1", ServerVersion: "0.9.700.90357", UE4SSBuild: "3.0.2", UE4SSReferenceRevision: UE4SSReferenceRevision}},
"binding": {availability: HandlerAvailability{BoundServerID: "server-1", Approved: true, Capabilities: map[string]bool{"vehicle.spawn": true}}, adapter: RuntimeAdapter{BoundServerID: "server-2"}},
"approval": {availability: HandlerAvailability{BoundServerID: "server-1", Approved: false, Capabilities: map[string]bool{"vehicle.spawn": true}}, adapter: RuntimeAdapter{BoundServerID: "server-1"}},
"capability": {availability: HandlerAvailability{BoundServerID: "server-1", Approved: true, Capabilities: map[string]bool{}}, adapter: RuntimeAdapter{BoundServerID: "server-1"}},
} {
t.Run(name, func(t *testing.T) {
port := &isolatedAdapterPort{}
@@ -159,12 +158,12 @@ func TestSupportedAdaptersFailClosedForBindingApprovalVersionAndCapability(t *te
func TestSupportedAdapterTransportFailuresCompleteWithoutProtectedOutput(t *testing.T) {
stamp := time.Date(2026, time.July, 29, 12, 0, 0, 0, time.UTC)
port := &isolatedAdapterPort{patchErr: errors.New("private port failed"), notifyErr: errors.New("private notification failed")}
adapter := VersionedAdapter{BoundServerID: "server-1", ServerVersion: "0.9.700.90357", UE4SSBuild: UE4SSReferenceBuild, UE4SSReferenceRevision: UE4SSReferenceRevision, Config: port, Notification: port}
adapter := RuntimeAdapter{BoundServerID: "server-1", Config: port, Notification: port}
gateway := &isolatedDispatchGateway{commands: []ClaimedCommand{
e2eClaim("patch-failure", "config.patch", map[string]any{"revision": "r1", "fields": []any{map[string]any{"key": "ServerName", "value": "Moonlight"}}}, stamp),
e2eClaim("notification-failure", "player.notify", map[string]any{"playerId": "76561198000000001", "message": "Moonlight ready"}, stamp),
}}
dispatcher := Dispatcher{Client: gateway, Registry: NewHandlerRegistry(HandlerAvailability{BoundServerID: "server-1", ServerVersion: "0.9.700.90357", Approved: true, Capabilities: map[string]bool{"config.patch": true, "player.notify": true}}, adapter), Now: func() time.Time { return stamp }}
dispatcher := Dispatcher{Client: gateway, Registry: NewHandlerRegistry(HandlerAvailability{BoundServerID: "server-1", Approved: true, Capabilities: map[string]bool{"config.patch": true, "player.notify": true}}, adapter), Now: func() time.Time { return stamp }}
if err := dispatcher.DispatchOnce(context.Background()); err != nil {
t.Fatalf("dispatch adapter failures: %v", err)
}
@@ -44,9 +44,9 @@ func (fixture *notificationPortFixture) SendPlayerNotification(_ context.Context
return UE4SSNotificationReceipt{Accepted: fixture.accepted}, nil
}
func TestVersionedAdapterUsesOnlyLogicalConfigValuesAndRedactsDiagnostics(t *testing.T) {
func TestRuntimeAdapterUsesOnlyLogicalConfigValuesAndRedactsDiagnostics(t *testing.T) {
port := &configPortFixture{fields: map[string]string{"ServerName": "Moon", "hostPath": "C:/secret", "Password": "nope"}}
adapter := VersionedAdapter{ServerVersion: "0.9.700.90357", Config: port, DiagnosticsState: map[string]string{"status": "healthy", "hostPath": "C:/secret"}}
adapter := RuntimeAdapter{Config: port, DiagnosticsState: map[string]string{"status": "healthy", "hostPath": "C:/secret"}}
read, err := adapter.ReadConfiguration(context.Background())
if err != nil {
t.Fatalf("read config: %v", err)
@@ -70,7 +70,7 @@ func TestVersionedAdapterUsesOnlyLogicalConfigValuesAndRedactsDiagnostics(t *tes
func TestVersionedUE4SSNotificationIsFixedTypedAndRedacted(t *testing.T) {
port := &notificationPortFixture{accepted: true}
adapter := VersionedAdapter{BoundServerID: "server-1", ServerVersion: "0.9.700.90357", UE4SSBuild: UE4SSReferenceBuild, UE4SSReferenceRevision: UE4SSReferenceRevision, Notification: port}
adapter := RuntimeAdapter{BoundServerID: "server-1", Notification: port}
result, err := adapter.NotifyPlayer(context.Background(), map[string]any{"playerId": "76561198000000001", "message": "Moon \"gift\""})
if err != nil || !result["accepted"].(bool) || len(port.deliveries) != 1 {
t.Fatalf("typed notification was not delivered: result=%+v err=%v deliveries=%+v", result, err, port.deliveries)
@@ -84,13 +84,9 @@ func TestVersionedUE4SSNotificationIsFixedTypedAndRedacted(t *testing.T) {
}
}
func TestVersionedUE4SSNotificationFailsClosedForUnpinnedBuildOrInvalidRecipient(t *testing.T) {
func TestRuntimeNotificationRejectsInvalidRecipient(t *testing.T) {
port := &notificationPortFixture{accepted: true}
adapter := VersionedAdapter{BoundServerID: "server-1", ServerVersion: "0.9.700.90357", UE4SSBuild: "3.0.2", UE4SSReferenceRevision: UE4SSReferenceRevision, Notification: port}
if _, err := adapter.NotifyPlayer(context.Background(), map[string]any{"playerId": "76561198000000001", "message": "Moonlight"}); err == nil {
t.Fatal("unpinned UE4SS build must be unavailable")
}
adapter.UE4SSBuild = UE4SSReferenceBuild
adapter := RuntimeAdapter{BoundServerID: "server-1", Notification: port}
if _, err := adapter.NotifyPlayer(context.Background(), map[string]any{"playerId": "not-a-steam-id", "message": "Moonlight"}); err == nil {
t.Fatal("unverified recipient identity must be rejected")
}
@@ -99,8 +95,8 @@ func TestVersionedUE4SSNotificationFailsClosedForUnpinnedBuildOrInvalidRecipient
func TestNotificationFailureIsCachedWithoutInvokingRewardDelivery(t *testing.T) {
stamp := time.Now().UTC()
port := &notificationPortFixture{accepted: false}
adapter := VersionedAdapter{BoundServerID: "server-1", ServerVersion: "0.9.700.90357", UE4SSBuild: UE4SSReferenceBuild, UE4SSReferenceRevision: UE4SSReferenceRevision, Notification: port}
registry := NewHandlerRegistry(HandlerAvailability{BoundServerID: "server-1", ServerVersion: "0.9.700.90357", Approved: true, Capabilities: map[string]bool{"player.notify": true}}, adapter)
adapter := RuntimeAdapter{BoundServerID: "server-1", Notification: port}
registry := NewHandlerRegistry(HandlerAvailability{BoundServerID: "server-1", Approved: true, Capabilities: map[string]bool{"player.notify": true}}, adapter)
command := ClaimedCommand{ID: "notification-1", ProfileKey: ProfileKey, CommandType: "player.notify", Payload: map[string]any{"playerId": "76561198000000001", "message": "Moonlight"}, FencingToken: 1, LeaseExpiresAt: stamp.Add(time.Minute), ExpiresAt: stamp.Add(time.Minute)}
for range 2 {
result, err := registry.Execute(context.Background(), command)
@@ -115,7 +111,7 @@ func TestNotificationFailureIsCachedWithoutInvokingRewardDelivery(t *testing.T)
func TestVersionedVehicleSpawnUsesFixedTemplateAndPrivateAuditOnly(t *testing.T) {
port := &nonProductionVehicleSpawnPortFixture{receipt: UE4SSVehicleSpawnReceipt{Outcome: UE4SSVehicleSpawnAccepted}}
adapter := VersionedAdapter{BoundServerID: "server-1", ServerVersion: "0.9.700.90357", UE4SSBuild: UE4SSReferenceBuild, UE4SSReferenceRevision: UE4SSReferenceRevision, VehicleSpawn: port}
adapter := RuntimeAdapter{BoundServerID: "server-1", VehicleSpawn: port}
result, err := adapter.SpawnVehicle(context.Background(), map[string]any{"vehicleCode": "BPC_Laika_C"})
if err != nil || result["outcome"] != "succeeded" || len(port.requests) != 1 {
t.Fatalf("fixed vehicle spawn was not delivered: result=%+v err=%v requests=%+v", result, err, port.requests)
@@ -131,7 +127,7 @@ func TestVersionedVehicleSpawnUsesFixedTemplateAndPrivateAuditOnly(t *testing.T)
func TestVersionedVehicleSpawnFailsClosedAndClassifiesBoundedReceipts(t *testing.T) {
port := &nonProductionVehicleSpawnPortFixture{receipt: UE4SSVehicleSpawnReceipt{Outcome: UE4SSVehicleSpawnRejected}}
adapter := VersionedAdapter{BoundServerID: "server-1", ServerVersion: "0.9.700.90357", UE4SSBuild: UE4SSReferenceBuild, UE4SSReferenceRevision: UE4SSReferenceRevision, VehicleSpawn: port}
adapter := RuntimeAdapter{BoundServerID: "server-1", VehicleSpawn: port}
for name, testCase := range map[string]struct {
outcome string
receipt UE4SSVehicleSpawnReceipt
@@ -151,17 +147,13 @@ func TestVersionedVehicleSpawnFailsClosedAndClassifiesBoundedReceipts(t *testing
if _, err := adapter.SpawnVehicle(context.Background(), map[string]any{"vehicleCode": "#spawnvehicle BPC_Laika_C"}); err == nil || len(port.requests) != before {
t.Fatal("raw command text must not reach the vehicle transport")
}
adapter.UE4SSBuild = "3.0.2"
if _, err := adapter.SpawnVehicle(context.Background(), map[string]any{"vehicleCode": "BPC_Laika_C"}); err == nil || len(port.requests) != before {
t.Fatal("unpinned UE4SS build must not reach the vehicle transport")
}
}
func TestVehicleSpawnUnknownOutcomeIsCachedWithoutRetry(t *testing.T) {
stamp := time.Now().UTC()
port := &nonProductionVehicleSpawnPortFixture{receipt: UE4SSVehicleSpawnReceipt{Outcome: UE4SSVehicleSpawnUnknown}}
adapter := VersionedAdapter{BoundServerID: "server-1", ServerVersion: "0.9.700.90357", UE4SSBuild: UE4SSReferenceBuild, UE4SSReferenceRevision: UE4SSReferenceRevision, VehicleSpawn: port}
registry := NewHandlerRegistry(HandlerAvailability{BoundServerID: "server-1", ServerVersion: "0.9.700.90357", Approved: true, Capabilities: map[string]bool{"vehicle.spawn": true}}, adapter)
adapter := RuntimeAdapter{BoundServerID: "server-1", VehicleSpawn: port}
registry := NewHandlerRegistry(HandlerAvailability{BoundServerID: "server-1", Approved: true, Capabilities: map[string]bool{"vehicle.spawn": true}}, adapter)
command := ClaimedCommand{ID: "vehicle-unknown-1", ProfileKey: ProfileKey, CommandType: "vehicle.spawn", Payload: map[string]any{"vehicleCode": "BPC_Laika_C"}, FencingToken: 1, LeaseExpiresAt: stamp.Add(time.Minute), ExpiresAt: stamp.Add(time.Minute)}
for range 2 {
result, err := registry.Execute(context.Background(), command)
@@ -21,14 +21,13 @@ type SafeAdapter interface {
SpawnVehicle(context.Context, map[string]any) (map[string]any, error)
}
// ServerBoundAdapter lets a versioned adapter prove that it is configured for
// ServerBoundAdapter lets a runtime adapter prove that it is configured for
// the same server as the registration which declared handler availability.
// Generic test adapters do not need this optional assertion.
type ServerBoundAdapter interface{ ServerBinding() string }
type HandlerAvailability struct {
BoundServerID string
ServerVersion string
Capabilities map[string]bool
Approved bool
}
@@ -85,7 +84,7 @@ func (registry *HandlerRegistry) Execute(ctx context.Context, command ClaimedCom
return unsupportedResult("unsupported"), nil
}
handler, exists := registry.handlers[command.CommandType]
if !exists || strings.TrimSpace(registry.availability.ServerVersion) == "" {
if !exists {
return unsupportedResult("unsupported"), nil
}
payload, err := handler(ctx, command.Payload)
@@ -171,15 +170,14 @@ func validateCommandPayload(commandType string, payload map[string]any) error {
}
return nil
case "game-state.patch":
if err := require("playerId", "gameVersion", "expectedStateVersion", "safetyWindow", "reason", "changes"); err != nil {
if err := require("playerId", "expectedStateVersion", "safetyWindow", "reason", "changes"); err != nil {
return err
}
if err := noUnknown("playerId", "gameVersion", "expectedStateVersion", "safetyWindow", "reason", "changes"); err != nil {
if err := noUnknown("playerId", "expectedStateVersion", "safetyWindow", "reason", "changes"); err != nil {
return err
}
version, ok := payload["gameVersion"].(string)
changes, changesOK := payload["changes"].([]any)
if !ok || version == "" || !changesOK || len(changes) == 0 || len(changes) > 8 {
if !changesOK || len(changes) == 0 || len(changes) > 8 {
return fmt.Errorf("state patch payload is invalid")
}
return nil
@@ -337,7 +335,7 @@ func (dispatcher Dispatcher) Run(ctx context.Context) error {
// Runtime keeps the registered companion alive with bounded heartbeat and
// polling intervals. It owns no host connection or game credential; handlers
// are the only route to a version-bound adapter.
// are the only route to runtime capability adapters.
type RuntimeGateway interface {
CommandGateway
Register(context.Context) (Registration, error)
@@ -13,7 +13,7 @@ func TestDispatcherIntegrationContainsUnsafeAndUnavailableCommands(t *testing.T)
stamp := time.Now().UTC()
adapter := &adapterFixture{}
registry := NewHandlerRegistry(HandlerAvailability{
BoundServerID: "server-1", ServerVersion: "0.9.700.90357", Approved: true,
BoundServerID: "server-1", Approved: true,
Capabilities: map[string]bool{"config.read": true},
}, adapter)
fixture := &dispatchFixture{commands: []ClaimedCommand{
@@ -52,7 +52,7 @@ func (*adapterFixture) SpawnVehicle(context.Context, map[string]any) (map[string
func TestDispatcherAcknowledgesOnlyLiveValidatedTypedCommands(t *testing.T) {
stamp := time.Now().UTC()
adapter := &adapterFixture{}
registry := NewHandlerRegistry(HandlerAvailability{BoundServerID: "server-1", ServerVersion: "0.9.700.90357", Approved: true, Capabilities: map[string]bool{"config.read": true}}, adapter)
registry := NewHandlerRegistry(HandlerAvailability{BoundServerID: "server-1", Approved: true, Capabilities: map[string]bool{"config.read": true}}, adapter)
fixture := &dispatchFixture{commands: []ClaimedCommand{{ID: "read-1", ProfileKey: ProfileKey, CommandType: "config.read", Payload: map[string]any{}, FencingToken: 7, LeaseExpiresAt: stamp.Add(time.Minute), ExpiresAt: stamp.Add(time.Minute)}, {ID: "expired-1", ProfileKey: ProfileKey, CommandType: "config.read", Payload: map[string]any{}, FencingToken: 8, LeaseExpiresAt: stamp.Add(-time.Second), ExpiresAt: stamp.Add(-time.Second)}}}
dispatcher := Dispatcher{Client: fixture, Registry: registry, Now: func() time.Time { return stamp }}
if err := dispatcher.DispatchOnce(context.Background()); err != nil {
@@ -69,7 +69,7 @@ func TestDispatcherAcknowledgesOnlyLiveValidatedTypedCommands(t *testing.T) {
func TestRegistryReturnsCachedResultForDuplicateDelivery(t *testing.T) {
stamp := time.Now().UTC()
adapter := &adapterFixture{}
registry := NewHandlerRegistry(HandlerAvailability{BoundServerID: "server-1", ServerVersion: "0.9.700.90357", Approved: true, Capabilities: map[string]bool{"config.read": true}}, adapter)
registry := NewHandlerRegistry(HandlerAvailability{BoundServerID: "server-1", Approved: true, Capabilities: map[string]bool{"config.read": true}}, adapter)
command := ClaimedCommand{ID: "duplicate-1", ProfileKey: ProfileKey, CommandType: "config.read", Payload: map[string]any{}, FencingToken: 7, LeaseExpiresAt: stamp.Add(time.Minute), ExpiresAt: stamp.Add(time.Minute)}
if _, err := registry.Execute(context.Background(), command); err != nil {
t.Fatalf("first execute: %v", err)
@@ -84,7 +84,7 @@ func TestRegistryReturnsCachedResultForDuplicateDelivery(t *testing.T) {
func TestRegistryRejectsUndeclaredAndMalformedPayloads(t *testing.T) {
stamp := time.Now().UTC()
registry := NewHandlerRegistry(HandlerAvailability{BoundServerID: "server-1", ServerVersion: "0.9.700.90357", Approved: true, Capabilities: map[string]bool{"config.patch": true}}, &adapterFixture{})
registry := NewHandlerRegistry(HandlerAvailability{BoundServerID: "server-1", Approved: true, Capabilities: map[string]bool{"config.patch": true}}, &adapterFixture{})
for _, command := range []ClaimedCommand{{ID: "bad-type", ProfileKey: ProfileKey, CommandType: "raw.rcon", Payload: map[string]any{}, FencingToken: 1, LeaseExpiresAt: stamp.Add(time.Minute), ExpiresAt: stamp.Add(time.Minute)}, {ID: "bad-payload", ProfileKey: ProfileKey, CommandType: "config.patch", Payload: map[string]any{"revision": "r1"}, FencingToken: 1, LeaseExpiresAt: stamp.Add(time.Minute), ExpiresAt: stamp.Add(time.Minute)}, {ID: "unsafe-vehicle", ProfileKey: ProfileKey, CommandType: "vehicle.spawn", Payload: map[string]any{"vehicleCode": "BPC_Laika_C", "command": "#spawnvehicle BPC_Laika_C"}, FencingToken: 1, LeaseExpiresAt: stamp.Add(time.Minute), ExpiresAt: stamp.Add(time.Minute)}} {
result, err := registry.Execute(context.Background(), command)
if err != nil || result.Payload["result"] != "validation-failed" {
@@ -1,17 +1,96 @@
package companion
// SemanticEventProducerAvailability is intentionally fail-closed. The pinned
// UE4SS reference exposes command dispatch and online chat only; it does not
// expose a versioned server-side login, logout, position, vehicle, or network
// identity producer. Do not add a parser until such a source is versioned.
import (
"crypto/sha256"
"encoding/hex"
"strings"
"time"
)
// ConsoleRecord is supplied by Run's stdout/stderr stream, not by the server
// execution log. The channel never accepts a file path or a raw log archive.
type ConsoleRecord struct {
ServerID string
Stream string
Sequence uint64
OccurredAt time.Time
Text string
}
type SemanticEvent struct {
ServerID string
Sequence uint64
Type string
PlayerID string
OccurredAt time.Time
NetworkCorrelation string
}
type EventDiagnostic struct {
ServerID string
Sequence uint64
Code string
}
type SemanticEventBatch struct {
ServerID string
FirstSequence uint64
Events []SemanticEvent
Diagnostics []EventDiagnostic
}
// ParseConsoleRecords accepts only bounded stdout/stderr records. Unknown
// formats produce a bounded diagnostic and are skipped; they never fabricate
// events or carry raw console text across the plugin boundary.
func ParseConsoleRecords(serverID string, records []ConsoleRecord, correlationSecret string) SemanticEventBatch {
batch := SemanticEventBatch{ServerID: serverID}
if len(records) > 100 {
records = records[:100]
}
for _, record := range records {
if record.ServerID != serverID || (record.Stream != "stdout" && record.Stream != "stderr") || record.Sequence == 0 || record.OccurredAt.IsZero() || len(record.Text) > 1024 {
batch.Diagnostics = appendDiagnostic(batch.Diagnostics, EventDiagnostic{ServerID: serverID, Sequence: record.Sequence, Code: "invalid-console-record"})
continue
}
if batch.FirstSequence == 0 {
batch.FirstSequence = record.Sequence
}
event, ok := parseConsoleRecord(record, correlationSecret)
if !ok {
batch.Diagnostics = appendDiagnostic(batch.Diagnostics, EventDiagnostic{ServerID: serverID, Sequence: record.Sequence, Code: "unknown-console-format"})
continue
}
batch.Events = append(batch.Events, event)
}
return batch
}
func parseConsoleRecord(record ConsoleRecord, secret string) (SemanticEvent, bool) {
fields := strings.Fields(record.Text)
if len(fields) < 3 || fields[0] != "SCUM" || (fields[1] != "LOGIN" && fields[1] != "LOGOUT") || !steamID64(fields[2]) {
return SemanticEvent{}, false
}
eventType := "scum.login"
if fields[1] == "LOGOUT" {
eventType = "scum.logout"
}
event := SemanticEvent{ServerID: record.ServerID, Sequence: record.Sequence, Type: eventType, PlayerID: fields[2], OccurredAt: record.OccurredAt}
if len(fields) == 4 && secret != "" {
event.NetworkCorrelation = networkCorrelation(record.ServerID, fields[3], secret)
}
return event, true
}
func networkCorrelation(serverID, value, secret string) string {
digest := sha256.Sum256([]byte(serverID + "\x00" + secret + "\x00" + value))
return hex.EncodeToString(digest[:16])
}
func appendDiagnostic(existing []EventDiagnostic, diagnostic EventDiagnostic) []EventDiagnostic {
if len(existing) >= 32 {
return existing
}
return append(existing, diagnostic)
}
func VerifiedSemanticEventProducer() SemanticEventProducerAvailability {
return SemanticEventProducerAvailability{Available: true, Reason: "Run stdout/stderr semantic parser is available"}
}
type SemanticEventProducerAvailability struct {
Available bool
Reason string
}
func VerifiedSemanticEventProducer() SemanticEventProducerAvailability {
return SemanticEventProducerAvailability{
Available: false,
Reason: "no versioned SCUM server-side semantic event producer is installed",
}
}
@@ -1,10 +1,20 @@
package companion
import "testing"
import (
"testing"
"time"
)
func TestVerifiedSemanticEventProducerFailsClosedWithoutASource(t *testing.T) {
func TestConsoleSemanticEventProducerParsesOnlyBoundedKnownOutput(t *testing.T) {
availability := VerifiedSemanticEventProducer()
if availability.Available || availability.Reason == "" {
t.Fatalf("semantic events must remain unavailable without a versioned source: %+v", availability)
if !availability.Available || availability.Reason == "" {
t.Fatalf("console event producer should be available: %+v", availability)
}
batch := ParseConsoleRecords("server-1", []ConsoleRecord{{ServerID: "server-1", Stream: "stdout", Sequence: 1, OccurredAt: time.Now(), Text: "SCUM LOGIN 76561198000000001 10.0.0.1"}, {ServerID: "server-1", Stream: "stderr", Sequence: 2, OccurredAt: time.Now(), Text: "unrecognised output"}}, "fixture-secret")
if len(batch.Events) != 1 || batch.Events[0].Type != "scum.login" || batch.Events[0].NetworkCorrelation == "" || len(batch.Diagnostics) != 1 || batch.Diagnostics[0].Code != "unknown-console-format" {
t.Fatalf("unsafe console parsing result: %+v", batch)
}
if batch.Events[0].NetworkCorrelation == "10.0.0.1" {
t.Fatal("raw network value leaked")
}
}