feat(platform): forward protected requests to run

This commit is contained in:
npc0-hue
2026-07-30 09:58:41 +08:00
parent 99be8f0f3a
commit 1e004dc9ec
16 changed files with 484 additions and 7 deletions
@@ -1,6 +1,7 @@
package service
import (
"encoding/json"
"strings"
"testing"
"time"
@@ -115,6 +116,79 @@ func TestProtectedGameClientBridgeRequestIsScopedAndRedacted(t *testing.T) {
}
}
func TestProtectedGameClientBridgeRequestDispatchesOneTimeRunInput(t *testing.T) {
svc, clock := newGameClientBridgeService(t)
plugin, err := svc.store.GamePlugins().Get("game.scum")
if err != nil {
t.Fatal(err)
}
plugin.RequiredRunCapabilities = []string{domain.JobCapabilityRemoteRunProtectedSQL}
plugin.RuntimeProfiles.TransportProfiles = []domain.RuntimeTransportProfile{{Key: "scum-database", Kind: "sqlite", TargetKey: "scum-database", Capabilities: []string{domain.JobCapabilityRemoteRunProtectedSQL}}}
plugin.GameClientBridge.Commands = append(plugin.GameClientBridge.Commands, domain.GameClientBridgeCommandDeclaration{Type: "database.request", ApprovalLevel: domain.GameClientBridgeApprovalLevelPlatformAdmin, TimeoutSeconds: 120, MaxPayloadBytes: 4096, ProtectedRequest: &domain.GameClientBridgeProtectedRequestDeclaration{Kind: "sql", TransportKey: "scum-database", TargetKey: "scum-database", TextField: "requestText", MaxTextBytes: 1024}})
if err := svc.store.GamePlugins().Update(plugin); err != nil {
t.Fatal(err)
}
if err := svc.store.Users().Create(domain.User{ID: "platform-admin", Email: "admin@example.test", Roles: []string{"platform-admin"}}); err != nil {
t.Fatal(err)
}
if err := svc.store.ServerInstances().Create(domain.ServerInstance{ID: "server-1", PluginID: plugin.ID, RunEndpointID: "run-local", Name: "Protected Bridge", State: domain.ServerInstanceStateRunning}); err != nil {
t.Fatal(err)
}
hello := validRunControlHello()
hello.CapabilityReport.Capabilities = []string{domain.JobCapabilityRemoteRunProtectedSQL}
hello.CapabilityReport.Fingerprint = "protected-request-capabilities"
run, err := svc.RegisterRunHello(hello)
if err != nil {
t.Fatalf("register Run: %v", err)
}
text := "SELECT player_id, position FROM players WHERE player_id = 7"
command, err := svc.queueGameClientBridgeCommand("platform-admin", domain.GameClientBridgeQueueRequest{ServerInstanceID: "server-1", PluginID: plugin.ID, ProfileKey: "scum-client", CommandType: "database.request", Payload: map[string]any{"requestText": text}, IdempotencyKey: "protected-run-1", ExpiresAt: clock.Add(time.Minute)})
if err != nil {
t.Fatalf("queue protected request: %v", err)
}
if command.RunJobID == "" || command.Payload["requestText"] != "redacted" || command.ApprovalState != domain.GameClientBridgeApprovalApproved {
t.Fatalf("protected command was not redacted and dispatched: %#v", command)
}
commandJSON, _ := json.Marshal(command)
if strings.Contains(string(commandJSON), text) {
t.Fatalf("protected bridge command persisted request text: %s", commandJSON)
}
if claimed, err := svc.claimGameClientBridgeCommands(bridgeComponent(), 10); err != nil || len(claimed) != 0 {
t.Fatalf("protected request must not be exposed to the Companion: commands=%#v err=%v", claimed, err)
}
claim, err := svc.ClaimRunJob(domain.RunJobClaim{RunEndpointID: "run-local", SessionToken: run.SessionToken, Capabilities: []string{domain.JobCapabilityRemoteRunProtectedSQL}, Capacity: domain.RunCapacity{MaxJobs: 1}})
if err != nil || !claim.HasJob || claim.Job == nil || claim.Job.JobID != command.RunJobID || claim.Job.FencingToken == 0 {
t.Fatalf("claim protected Run job: claim=%#v err=%v", claim, err)
}
assignmentJSON, _ := json.Marshal(claim.Job)
if strings.Contains(string(assignmentJSON), text) {
t.Fatalf("Run assignment exposed protected request text: %s", assignmentJSON)
}
ack, err := svc.AckRunJob(domain.RunJobAck{RunEndpointID: "run-local", SessionToken: run.SessionToken, JobID: claim.Job.JobID, LeaseToken: claim.Job.LeaseToken, Attempt: claim.Job.Attempt, Message: "accepted"})
if err != nil || !ack.Accepted {
t.Fatalf("ack protected Run job: ack=%#v err=%v", ack, err)
}
if _, err := svc.GetProtectedRequestExecutionInput(domain.ProtectedRequestExecutionInputRequest{RunEndpointID: "run-local", SessionToken: run.SessionToken, JobID: claim.Job.JobID, LeaseToken: claim.Job.LeaseToken, Attempt: claim.Job.Attempt, FencingToken: claim.Job.FencingToken + 1}); err == nil {
t.Fatal("expected fencing mismatch rejection")
}
input, err := svc.GetProtectedRequestExecutionInput(domain.ProtectedRequestExecutionInputRequest{RunEndpointID: "run-local", SessionToken: run.SessionToken, JobID: claim.Job.JobID, LeaseToken: claim.Job.LeaseToken, Attempt: claim.Job.Attempt, FencingToken: claim.Job.FencingToken})
if err != nil || input.RequestText != text || input.Kind != "sql" || input.TransportKey != "scum-database" || !input.Authorized {
t.Fatalf("read protected Run input: input=%#v err=%v", input, err)
}
if _, err := svc.GetProtectedRequestExecutionInput(domain.ProtectedRequestExecutionInputRequest{RunEndpointID: "run-local", SessionToken: run.SessionToken, JobID: claim.Job.JobID, LeaseToken: claim.Job.LeaseToken, Attempt: claim.Job.Attempt, FencingToken: claim.Job.FencingToken}); err == nil {
t.Fatal("expected one-time protected input rejection")
}
if _, err := svc.CompleteRunJob(domain.RunJobResult{RunEndpointID: "run-local", SessionToken: run.SessionToken, JobID: claim.Job.JobID, LeaseToken: claim.Job.LeaseToken, Attempt: claim.Job.Attempt, State: domain.JobStateFailed, Progress: domain.RunJobProgressReport{Percent: 100, Message: "unknown request"}, ErrorCode: "protected_request_unknown", ExecutionResult: domain.JobExecutionResult{Kind: "protected.sql.unknown", AuditSummary: "protected request outcome is unknown"}}); err != nil {
t.Fatalf("complete protected Run job: %v", err)
}
completed, err := svc.store.GameClientBridgeCommands().Get(command.ID)
if err != nil || completed.State != domain.GameClientBridgeCommandUnknown || completed.Result.Status != domain.GameClientBridgeResultUnknown || strings.Contains(completed.Result.Summary, text) {
t.Fatalf("project protected Run result: command=%#v err=%v", completed, err)
}
}
func TestGameClientBridgeIdempotencyScopeIsAppliedByService(t *testing.T) {
svc, clock := newGameClientBridgeService(t)
request := bridgeQueueRequest(*clock, "scope-key")