diff --git a/openspec/changes/move-scum-feature-ownership-to-plugin/implementation-blockers.md b/openspec/changes/move-scum-feature-ownership-to-plugin/implementation-blockers.md index 232c2bc..12c4b24 100644 --- a/openspec/changes/move-scum-feature-ownership-to-plugin/implementation-blockers.md +++ b/openspec/changes/move-scum-feature-ownership-to-plugin/implementation-blockers.md @@ -30,8 +30,10 @@ Consequently these tasks remain open and blocked rather than simulated: network, position, or vehicle data. - 5.1 and 5.3: no documented, version-bound state-patch or reward-delivery adapter exists. -- 5.5 and the remaining end-to-end portion of 6.2: the required adapter - coverage and isolated non-production SCUM fixtures do not exist. +- 6.2: controlled read-only migration fixtures and isolated Companion + 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 removed before the parity and rollback evidence required by 6.2. - 6.4: final full verification is deferred until the blocked adapters and diff --git a/openspec/changes/move-scum-feature-ownership-to-plugin/tasks.md b/openspec/changes/move-scum-feature-ownership-to-plugin/tasks.md index fa3eea7..b7b0c98 100644 --- a/openspec/changes/move-scum-feature-ownership-to-plugin/tasks.md +++ b/openspec/changes/move-scum-feature-ownership-to-plugin/tasks.md @@ -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. - [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 ` 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 @@ -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.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. + +## 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. diff --git a/plugins/examples/scum-server-plugin/companion/adapters.go b/plugins/examples/scum-server-plugin/companion/adapters.go index 56d9870..6718098 100644 --- a/plugins/examples/scum-server-plugin/companion/adapters.go +++ b/plugins/examples/scum-server-plugin/companion/adapters.go @@ -2,12 +2,15 @@ package companion import ( "context" + "errors" "fmt" "sort" "strings" "unicode/utf8" ) +var errAdapterUnsupported = errors.New("versioned adapter is unsupported") + // 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. @@ -75,9 +78,11 @@ type VersionedAdapter struct { DiagnosticsState map[string]string } +func (adapter VersionedAdapter) ServerBinding() string { return adapter.BoundServerID } + func (adapter VersionedAdapter) ReadConfiguration(ctx context.Context) (map[string]any, error) { if !supportedAdapterVersion(adapter.ServerVersion) || adapter.Config == nil { - return nil, fmt.Errorf("configuration adapter is unsupported") + return nil, errAdapterUnsupported } fields, err := adapter.Config.ReadConfig(ctx) 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) { if !supportedAdapterVersion(adapter.ServerVersion) || adapter.Config == nil { - return nil, fmt.Errorf("configuration adapter is unsupported") + return nil, errAdapterUnsupported } revision, _ := payload["revision"].(string) raw, _ := payload["fields"].([]any) @@ -120,14 +125,14 @@ 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, fmt.Errorf("state adapter is unsupported") + return nil, errAdapterUnsupported } 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) { if !adapter.supportsPinnedUE4SS() || adapter.Notification == nil { - return nil, fmt.Errorf("notification adapter is unsupported") + return nil, errAdapterUnsupported } playerID, playerOK := payload["playerId"].(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) { if !adapter.supportsPinnedUE4SS() || adapter.VehicleSpawn == nil { - return nil, fmt.Errorf("vehicle spawn adapter is unsupported") + return nil, errAdapterUnsupported } vehicleCode, ok := payload["vehicleCode"].(string) spawn, err := newUE4SSVehicleSpawn(adapter.BoundServerID, vehicleCode) diff --git a/plugins/examples/scum-server-plugin/companion/adapters_e2e_test.go b/plugins/examples/scum-server-plugin/companion/adapters_e2e_test.go new file mode 100644 index 0000000..4e19150 --- /dev/null +++ b/plugins/examples/scum-server-plugin/companion/adapters_e2e_test.go @@ -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) + } +} diff --git a/plugins/examples/scum-server-plugin/companion/dispatcher.go b/plugins/examples/scum-server-plugin/companion/dispatcher.go index 6f3334c..41062d8 100644 --- a/plugins/examples/scum-server-plugin/companion/dispatcher.go +++ b/plugins/examples/scum-server-plugin/companion/dispatcher.go @@ -2,6 +2,7 @@ package companion import ( "context" + "errors" "fmt" "strings" "sync" @@ -20,6 +21,11 @@ type SafeAdapter interface { 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 { BoundServerID string ServerVersion string @@ -28,10 +34,11 @@ type HandlerAvailability struct { } type CommandHandler func(context.Context, map[string]any) (map[string]any, error) type HandlerRegistry struct { - availability HandlerAvailability - handlers map[string]CommandHandler - mu sync.Mutex - completed map[string]CommandResult + availability HandlerAvailability + handlers map[string]CommandHandler + adapterServer string + mu sync.Mutex + completed map[string]CommandResult } func NewHandlerRegistry(availability HandlerAvailability, adapter SafeAdapter) *HandlerRegistry { @@ -39,6 +46,9 @@ func NewHandlerRegistry(availability HandlerAvailability, adapter SafeAdapter) * if adapter == nil { 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) { 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 { 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 } 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) 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 } 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 } +func (registry *HandlerRegistry) adapterBindingMatches() bool { + return registry.adapterServer == "" || registry.adapterServer == registry.availability.BoundServerID +} + 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) { return fmt.Errorf("invalid command")