test(scum): cover supported companion adapters

This commit is contained in:
npc0-hue
2026-07-29 16:15:01 +08:00
parent daa0f330a4
commit 9fafd17915
5 changed files with 272 additions and 14 deletions
@@ -30,8 +30,10 @@ Consequently these tasks remain open and blocked rather than simulated:
network, position, or vehicle data. network, position, or vehicle data.
- 5.1 and 5.3: no documented, version-bound state-patch or reward-delivery - 5.1 and 5.3: no documented, version-bound state-patch or reward-delivery
adapter exists. adapter exists.
- 5.5 and the remaining end-to-end portion of 6.2: the required adapter - 6.2: controlled read-only migration fixtures and isolated Companion
coverage and isolated non-production SCUM fixtures do not exist. typed-port coverage now exist for the supported configuration, notification,
and vehicle-spawn adapters, but no legitimate versioned producer exists for
the remaining player-history, reward, state-patch, or trajectory parity.
- 6.3: transitional SCUM APIs and models still have callers and cannot be - 6.3: transitional SCUM APIs and models still have callers and cannot be
removed before the parity and rollback evidence required by 6.2. removed before the parity and rollback evidence required by 6.2.
- 6.4: final full verification is deferred until the blocked adapters and - 6.4: final full verification is deferred until the blocked adapters and
@@ -33,7 +33,7 @@
- [ ] 5.3 Implement a `reward.deliver` adapter that freezes the approved revision, performs idempotent delivery, and reports delivered/failed/unknown without automatically retrying unknown results. - [ ] 5.3 Implement a `reward.deliver` adapter that freezes the approved revision, performs idempotent delivery, and reports delivered/failed/unknown without automatically retrying unknown results.
- [x] 5.4 Implement a separate `player.notify` adapter that never repeats item delivery after notification failure; verify server-scoped recipient identity and redact message transport details. - [x] 5.4 Implement a separate `player.notify` adapter that never repeats item delivery after notification failure; verify server-scoped recipient identity and redact message transport details.
- [x] 5.5 Implement the explicitly enabled, version-bound `vehicle.spawn` adapter with a strict vehicle-code catalog, the fixed internal `#spawnvehicle <vehicleCode>` template, protected audit text, structured success/failure/unknown outcomes, Companion-local transport fixture, and fail-closed UI availability. - [x] 5.5 Implement the explicitly enabled, version-bound `vehicle.spawn` adapter with a strict vehicle-code catalog, the fixed internal `#spawnvehicle <vehicleCode>` template, protected audit text, structured success/failure/unknown outcomes, Companion-local transport fixture, and fail-closed UI availability.
- [ ] 5.6 Add isolated non-production end-to-end tests for every supported adapter and ensure no raw SQL, unrestricted RCON, OCR, screenshots, keyboard/mouse injection, or direct game database write path exists. - [x] 5.6 Add isolated non-production end-to-end tests for every supported adapter and ensure no raw SQL, unrestricted RCON, OCR, screenshots, keyboard/mouse injection, or direct game database write path exists.
## 6. Migrate transitional platform behavior safely ## 6. Migrate transitional platform behavior safely
@@ -41,3 +41,7 @@
- [ ] 6.2 Verify plugin parity for configuration, player history, gifts, state-patch audits, and trajectories against controlled fixtures and an isolated Companion integration environment. - [ ] 6.2 Verify plugin parity for configuration, player history, gifts, state-patch audits, and trajectories against controlled fixtures and an isolated Companion integration environment.
- [ ] 6.3 Remove SCUM-named platform APIs, models, services, routes, and frontend components only after no callers remain and migration/rollback evidence is recorded. - [ ] 6.3 Remove SCUM-named platform APIs, models, services, routes, and frontend components only after no callers remain and migration/rollback evidence is recorded.
- [ ] 6.4 Run full platform, plugin, frontend, manifest, OpenSpec strict, structure, and isolated end-to-end verification; commit and push only the scoped migration files. - [ ] 6.4 Run full platform, plugin, frontend, manifest, OpenSpec strict, structure, and isolated end-to-end verification; commit and push only the scoped migration files.
## Verification evidence
- 5.6: `cd plugins/examples/scum-server-plugin/companion && go test ./...` passed on 2026-07-29. The isolated typed-port fixture covers safe configuration read/patch, `player.notify`, and version-bound `vehicle.spawn` through claim/ack/registry/complete; it checks approval, binding, version/capability gating, idempotency, bounded outcomes, and redaction. A production-source test rejects raw SQL/direct database access, unrestricted command/RCON execution, OCR/screenshot/input automation, and direct socket paths.
@@ -2,12 +2,15 @@ package companion
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"sort" "sort"
"strings" "strings"
"unicode/utf8" "unicode/utf8"
) )
var errAdapterUnsupported = errors.New("versioned adapter is unsupported")
// AuthorizedConfigPort is supplied by a version-bound Companion integration. // AuthorizedConfigPort is supplied by a version-bound Companion integration.
// It exposes logical configuration values only: never a host path, connection // It exposes logical configuration values only: never a host path, connection
// string, credential, arbitrary command, or direct database handle. // string, credential, arbitrary command, or direct database handle.
@@ -75,9 +78,11 @@ type VersionedAdapter struct {
DiagnosticsState map[string]string DiagnosticsState map[string]string
} }
func (adapter VersionedAdapter) ServerBinding() string { return adapter.BoundServerID }
func (adapter VersionedAdapter) ReadConfiguration(ctx context.Context) (map[string]any, error) { func (adapter VersionedAdapter) ReadConfiguration(ctx context.Context) (map[string]any, error) {
if !supportedAdapterVersion(adapter.ServerVersion) || adapter.Config == nil { if !supportedAdapterVersion(adapter.ServerVersion) || adapter.Config == nil {
return nil, fmt.Errorf("configuration adapter is unsupported") return nil, errAdapterUnsupported
} }
fields, err := adapter.Config.ReadConfig(ctx) fields, err := adapter.Config.ReadConfig(ctx)
if err != nil { if err != nil {
@@ -87,7 +92,7 @@ func (adapter VersionedAdapter) ReadConfiguration(ctx context.Context) (map[stri
} }
func (adapter VersionedAdapter) PatchConfiguration(ctx context.Context, payload map[string]any) (map[string]any, error) { func (adapter VersionedAdapter) PatchConfiguration(ctx context.Context, payload map[string]any) (map[string]any, error) {
if !supportedAdapterVersion(adapter.ServerVersion) || adapter.Config == nil { if !supportedAdapterVersion(adapter.ServerVersion) || adapter.Config == nil {
return nil, fmt.Errorf("configuration adapter is unsupported") return nil, errAdapterUnsupported
} }
revision, _ := payload["revision"].(string) revision, _ := payload["revision"].(string)
raw, _ := payload["fields"].([]any) raw, _ := payload["fields"].([]any)
@@ -120,14 +125,14 @@ func (adapter VersionedAdapter) Diagnostics(context.Context) (map[string]any, er
return state, nil return state, nil
} }
func (VersionedAdapter) PatchGameState(context.Context, map[string]any) (map[string]any, error) { func (VersionedAdapter) PatchGameState(context.Context, map[string]any) (map[string]any, error) {
return nil, fmt.Errorf("state adapter is unsupported") return nil, errAdapterUnsupported
} }
func (VersionedAdapter) DeliverReward(context.Context, map[string]any) (map[string]any, error) { func (VersionedAdapter) DeliverReward(context.Context, map[string]any) (map[string]any, error) {
return nil, fmt.Errorf("reward adapter is unsupported") return nil, errAdapterUnsupported
} }
func (adapter VersionedAdapter) NotifyPlayer(ctx context.Context, payload map[string]any) (map[string]any, error) { func (adapter VersionedAdapter) NotifyPlayer(ctx context.Context, payload map[string]any) (map[string]any, error) {
if !adapter.supportsPinnedUE4SS() || adapter.Notification == nil { if !adapter.supportsPinnedUE4SS() || adapter.Notification == nil {
return nil, fmt.Errorf("notification adapter is unsupported") return nil, errAdapterUnsupported
} }
playerID, playerOK := payload["playerId"].(string) playerID, playerOK := payload["playerId"].(string)
message, messageOK := payload["message"].(string) message, messageOK := payload["message"].(string)
@@ -146,7 +151,7 @@ func (adapter VersionedAdapter) NotifyPlayer(ctx context.Context, payload map[st
} }
func (adapter VersionedAdapter) SpawnVehicle(ctx context.Context, payload map[string]any) (map[string]any, error) { func (adapter VersionedAdapter) SpawnVehicle(ctx context.Context, payload map[string]any) (map[string]any, error) {
if !adapter.supportsPinnedUE4SS() || adapter.VehicleSpawn == nil { if !adapter.supportsPinnedUE4SS() || adapter.VehicleSpawn == nil {
return nil, fmt.Errorf("vehicle spawn adapter is unsupported") return nil, errAdapterUnsupported
} }
vehicleCode, ok := payload["vehicleCode"].(string) vehicleCode, ok := payload["vehicleCode"].(string)
spawn, err := newUE4SSVehicleSpawn(adapter.BoundServerID, vehicleCode) spawn, err := newUE4SSVehicleSpawn(adapter.BoundServerID, vehicleCode)
@@ -0,0 +1,230 @@
package companion
import (
"context"
"errors"
"os"
"path/filepath"
"regexp"
"runtime"
"strings"
"testing"
"time"
)
// isolatedAdapterPort is a non-production typed-port fixture. It records only
// the adapter's typed inputs and never opens a connection or accepts a raw
// game command.
type isolatedAdapterPort struct {
configFields map[string]string
configErr error
patchErr error
patches []ConfigFieldPatch
notifications []ue4SSPlayerNotification
notifyErr error
notifyAccept bool
spawns []ue4SSVehicleSpawn
spawnReceipts []UE4SSVehicleSpawnReceipt
spawnErrors []error
}
func (port *isolatedAdapterPort) ReadConfig(context.Context) (map[string]string, error) {
return port.configFields, port.configErr
}
func (port *isolatedAdapterPort) ApplyConfigPatch(_ context.Context, _ string, fields []ConfigFieldPatch) (map[string]string, error) {
port.patches = append(port.patches, fields...)
if port.patchErr != nil {
return nil, port.patchErr
}
return map[string]string{"ServerName": "Moonlight", "hostPath": "C:/private/server.ini"}, nil
}
func (port *isolatedAdapterPort) SendPlayerNotification(_ context.Context, notification ue4SSPlayerNotification) (UE4SSNotificationReceipt, error) {
port.notifications = append(port.notifications, notification)
return UE4SSNotificationReceipt{Accepted: port.notifyAccept}, port.notifyErr
}
func (port *isolatedAdapterPort) SpawnVehicle(_ context.Context, spawn ue4SSVehicleSpawn) (UE4SSVehicleSpawnReceipt, error) {
port.spawns = append(port.spawns, spawn)
index := len(port.spawns) - 1
if index >= len(port.spawnReceipts) {
return UE4SSVehicleSpawnReceipt{Outcome: UE4SSVehicleSpawnUnknown}, nil
}
return port.spawnReceipts[index], port.spawnErrors[index]
}
type isolatedDispatchGateway struct {
commands []ClaimedCommand
acks []string
completed map[string][]CommandResult
}
func (gateway *isolatedDispatchGateway) ClaimCommands(context.Context, int) ([]ClaimedCommand, error) {
return append([]ClaimedCommand(nil), gateway.commands...), nil
}
func (gateway *isolatedDispatchGateway) AckCommand(_ context.Context, id string, _ uint64) (CommandAck, error) {
gateway.acks = append(gateway.acks, id)
return CommandAck{CommandID: id, State: "claimed", FencingToken: 1}, nil
}
func (gateway *isolatedDispatchGateway) CompleteCommand(_ context.Context, id string, _ uint64, result CommandResult) (CompletedCommand, error) {
if gateway.completed == nil {
gateway.completed = map[string][]CommandResult{}
}
gateway.completed[id] = append(gateway.completed[id], result)
return CompletedCommand{State: "completed"}, nil
}
func e2eClaim(id, commandType string, payload map[string]any, stamp time.Time) ClaimedCommand {
return ClaimedCommand{ID: id, ProfileKey: ProfileKey, CommandType: commandType, Payload: payload, FencingToken: 1, LeaseExpiresAt: stamp.Add(time.Minute), ExpiresAt: stamp.Add(time.Minute)}
}
func TestSupportedAdaptersDispatchThroughIsolatedTypedPorts(t *testing.T) {
stamp := time.Date(2026, time.July, 29, 12, 0, 0, 0, time.UTC)
port := &isolatedAdapterPort{
configFields: map[string]string{"ServerName": "Moonlight", "Password": "never-return", "hostPath": "C:/private/server.ini"},
notifyAccept: true,
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)
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),
e2eClaim("notify", "player.notify", map[string]any{"playerId": "76561198000000001", "message": "Moonlight ready"}, stamp),
e2eClaim("notify", "player.notify", map[string]any{"playerId": "76561198000000001", "message": "Moonlight ready"}, stamp),
e2eClaim("spawn-success", "vehicle.spawn", map[string]any{"vehicleCode": "BPC_Laika_C"}, stamp),
e2eClaim("spawn-failed", "vehicle.spawn", map[string]any{"vehicleCode": "BPC_WolfsWagen_C"}, stamp),
e2eClaim("spawn-unknown", "vehicle.spawn", map[string]any{"vehicleCode": "BPC_Laika_C"}, stamp),
e2eClaim("spawn-unknown", "vehicle.spawn", map[string]any{"vehicleCode": "BPC_Laika_C"}, stamp),
}}
dispatcher := Dispatcher{Client: gateway, Registry: registry, Now: func() time.Time { return stamp }}
if err := dispatcher.DispatchOnce(context.Background()); err != nil {
t.Fatalf("dispatch supported adapters: %v", err)
}
if len(port.notifications) != 1 || port.notifications[0].ServerID != "server-1" || port.notifications[0].protectedAuditCommand == "" {
t.Fatalf("notification did not remain server-bound and idempotent: %+v", port.notifications)
}
if len(port.patches) != 1 || gateway.completed["config-read"][0].Payload["fields"].(map[string]string)["ServerName"] != "Moonlight" || gateway.completed["config-patch"][0].Payload["appliedFields"].(map[string]string)["ServerName"] != "Moonlight" || gateway.completed["notify"][0].Payload["accepted"] != true {
t.Fatalf("supported adapters did not return their bounded successful results: patches=%+v completed=%+v", port.patches, gateway.completed)
}
if len(port.spawns) != 3 || port.spawns[0].protectedAuditCommand != "#spawnvehicle BPC_Laika_C" || port.spawns[1].protectedAuditCommand != "#spawnvehicle BPC_WolfsWagen_C" || port.spawns[2].protectedAuditCommand != "#spawnvehicle BPC_Laika_C" {
t.Fatalf("vehicle adapter did not use only fixed private templates: %+v", port.spawns)
}
if gateway.completed["spawn-success"][0].Payload["outcome"] != "succeeded" || gateway.completed["spawn-failed"][0].Payload["outcome"] != "failed" || gateway.completed["spawn-unknown"][0].Payload["outcome"] != "unknown" {
t.Fatalf("unexpected bounded vehicle outcomes: %+v", gateway.completed)
}
for id, results := range gateway.completed {
for _, result := range results {
serialized := result.Summary + " " + stringifyPayload(result.Payload)
for _, secret := range []string{"C:/private/server.ini", "never-return", "#spawnvehicle", "SendChat"} {
if strings.Contains(serialized, secret) {
t.Fatalf("%s exposed protected adapter output %q: %+v", id, secret, result)
}
}
}
}
}
func TestSupportedAdaptersFailClosedForBindingApprovalVersionAndCapability(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
}{
"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}},
} {
t.Run(name, func(t *testing.T) {
port := &isolatedAdapterPort{}
testCase.adapter.VehicleSpawn = port
gateway := &isolatedDispatchGateway{commands: []ClaimedCommand{e2eClaim(name, "vehicle.spawn", map[string]any{"vehicleCode": "BPC_Laika_C"}, stamp)}}
dispatcher := Dispatcher{Client: gateway, Registry: NewHandlerRegistry(testCase.availability, testCase.adapter), Now: func() time.Time { return stamp }}
if err := dispatcher.DispatchOnce(context.Background()); err != nil {
t.Fatalf("dispatch fail-closed case: %v", err)
}
result := gateway.completed[name][0]
if result.Payload["result"] != "unsupported" || len(port.spawns) != 0 {
t.Fatalf("unsafe adapter reached transport: result=%+v spawns=%+v", result, port.spawns)
}
})
}
}
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}
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 }}
if err := dispatcher.DispatchOnce(context.Background()); err != nil {
t.Fatalf("dispatch adapter failures: %v", err)
}
for id, results := range gateway.completed {
result := results[0]
if result.Payload["result"] != "failed" || strings.Contains(stringifyPayload(result.Payload), "private") || strings.Contains(result.Summary, "private") {
t.Fatalf("%s did not redact failed typed-port output: %+v", id, result)
}
}
if len(port.notifications) != 1 || port.notifications[0].protectedAuditCommand == "" {
t.Fatalf("notification fixture did not receive one protected typed request: %+v", port.notifications)
}
}
func stringifyPayload(payload map[string]any) string {
parts := make([]string, 0, len(payload))
for key, value := range payload {
parts = append(parts, key+"="+strings.TrimSpace(strings.ReplaceAll(strings.TrimSpace(toText(value)), "\n", " ")))
}
return strings.Join(parts, " ")
}
func toText(value any) string {
if text, ok := value.(string); ok {
return text
}
return ""
}
func TestCompanionProductionSourceHasNoForbiddenAdapterPaths(t *testing.T) {
_, currentFile, _, ok := runtime.Caller(0)
if !ok {
t.Fatal("resolve companion source directory")
}
forbidden := map[string]*regexp.Regexp{
"raw SQL or direct database access": regexp.MustCompile(`(?im)"(?:database/sql|github\.com/(?:mattn/go-sqlite3|go-sql-driver/mysql)|gorm\.io/gorm)"|\b(?:sql|db|database)\.(?:Open|Exec(?:Context)?|Query(?:Context)?|Prepare(?:Context)?)\s*\(`),
"unrestricted RCON or command execution": regexp.MustCompile(`(?i)\b(?:send|execute|run|dispatch)[a-z0-9_]*(?:rcon|rawcommand|command)\s*\(`),
"desktop automation or screen capture": regexp.MustCompile(`(?i)\b(?:tesseract|gosseract|screenshot|robotgo|autogui|keybd_event|mouse_event|sendinput)\b`),
"direct socket transport": regexp.MustCompile(`\bnet\.(?:Dial|DialTimeout)\s*\(`),
}
root := filepath.Dir(currentFile)
err := filepath.WalkDir(root, func(path string, entry os.DirEntry, walkErr error) error {
if walkErr != nil {
return walkErr
}
if entry.IsDir() || filepath.Ext(path) != ".go" || strings.HasSuffix(path, "_test.go") {
return nil
}
source, err := os.ReadFile(path)
if err != nil {
return err
}
for name, pattern := range forbidden {
if match := pattern.FindString(string(source)); match != "" {
t.Fatalf("%s contains forbidden %s: %q", filepath.Base(path), name, match)
}
}
return nil
})
if err != nil {
t.Fatalf("scan production companion source: %v", err)
}
}
@@ -2,6 +2,7 @@ package companion
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"strings" "strings"
"sync" "sync"
@@ -20,6 +21,11 @@ type SafeAdapter interface {
SpawnVehicle(context.Context, map[string]any) (map[string]any, error) SpawnVehicle(context.Context, map[string]any) (map[string]any, error)
} }
// ServerBoundAdapter lets a versioned 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 { type HandlerAvailability struct {
BoundServerID string BoundServerID string
ServerVersion string ServerVersion string
@@ -30,6 +36,7 @@ type CommandHandler func(context.Context, map[string]any) (map[string]any, error
type HandlerRegistry struct { type HandlerRegistry struct {
availability HandlerAvailability availability HandlerAvailability
handlers map[string]CommandHandler handlers map[string]CommandHandler
adapterServer string
mu sync.Mutex mu sync.Mutex
completed map[string]CommandResult completed map[string]CommandResult
} }
@@ -39,6 +46,9 @@ func NewHandlerRegistry(availability HandlerAvailability, adapter SafeAdapter) *
if adapter == nil { if adapter == nil {
return registry return registry
} }
if bound, ok := adapter.(ServerBoundAdapter); ok {
registry.adapterServer = bound.ServerBinding()
}
registry.handlers["config.read"] = func(ctx context.Context, _ map[string]any) (map[string]any, error) { registry.handlers["config.read"] = func(ctx context.Context, _ map[string]any) (map[string]any, error) {
return adapter.ReadConfiguration(ctx) return adapter.ReadConfiguration(ctx)
} }
@@ -71,7 +81,7 @@ func (registry *HandlerRegistry) Execute(ctx context.Context, command ClaimedCom
if err := validateDeclaredCommandAt(command, time.Now); err != nil { if err := validateDeclaredCommandAt(command, time.Now); err != nil {
return unsupportedResult("validation-failed"), nil return unsupportedResult("validation-failed"), nil
} }
if strings.TrimSpace(registry.availability.BoundServerID) == "" || !registry.availability.Approved || !registry.availability.Capabilities[command.CommandType] { if strings.TrimSpace(registry.availability.BoundServerID) == "" || !registry.adapterBindingMatches() || !registry.availability.Approved || !registry.availability.Capabilities[command.CommandType] {
return unsupportedResult("unsupported"), nil return unsupportedResult("unsupported"), nil
} }
handler, exists := registry.handlers[command.CommandType] handler, exists := registry.handlers[command.CommandType]
@@ -80,6 +90,9 @@ func (registry *HandlerRegistry) Execute(ctx context.Context, command ClaimedCom
} }
payload, err := handler(ctx, command.Payload) payload, err := handler(ctx, command.Payload)
if err != nil { if err != nil {
if errors.Is(err, errAdapterUnsupported) {
return unsupportedResult("unsupported"), nil
}
return CommandResult{Status: "failed", Summary: "typed adapter failed", Payload: map[string]any{"result": "failed"}}, nil return CommandResult{Status: "failed", Summary: "typed adapter failed", Payload: map[string]any{"result": "failed"}}, nil
} }
result := CommandResult{Status: "succeeded", Summary: "typed adapter completed", Payload: redactTypedPayload(payload)} result := CommandResult{Status: "succeeded", Summary: "typed adapter completed", Payload: redactTypedPayload(payload)}
@@ -89,6 +102,10 @@ func (registry *HandlerRegistry) Execute(ctx context.Context, command ClaimedCom
return result, nil return result, nil
} }
func (registry *HandlerRegistry) adapterBindingMatches() bool {
return registry.adapterServer == "" || registry.adapterServer == registry.availability.BoundServerID
}
func validateDeclaredCommandAt(command ClaimedCommand, now func() time.Time) error { func validateDeclaredCommandAt(command ClaimedCommand, now func() time.Time) error {
if command.ID == "" || command.ProfileKey != ProfileKey || command.FencingToken == 0 || command.Payload == nil || command.LeaseExpiresAt.IsZero() || command.ExpiresAt.IsZero() || !now().Before(command.LeaseExpiresAt) || !now().Before(command.ExpiresAt) { if command.ID == "" || command.ProfileKey != ProfileKey || command.FencingToken == 0 || command.Payload == nil || command.LeaseExpiresAt.IsZero() || command.ExpiresAt.IsZero() || !now().Before(command.LeaseExpiresAt) || !now().Before(command.ExpiresAt) {
return fmt.Errorf("invalid command") return fmt.Errorf("invalid command")