first commit

This commit is contained in:
npc0-hue
2026-07-11 14:56:10 +08:00
commit 7e05d0a4e7
660 changed files with 78119 additions and 0 deletions
+103
View File
@@ -0,0 +1,103 @@
package validator
import (
"fmt"
"strings"
"browser.local/platform/domain"
)
const (
maxAIInvocationPromptSize = 8000
maxAIInvocationConfigSize = 64 * 1024
maxAIInvocationContextRefs = 12
)
func ValidateAIInvocationRequest(request domain.AIInvocationRequest) error {
request = domain.CopyAIInvocationRequest(request)
var violations []string
violations = appendRequired(violations, "requestId", request.RequestID)
violations = appendRequired(violations, "purpose", request.Purpose)
violations = appendRequired(violations, "prompt", request.Prompt)
if request.Purpose != "" && !validAIPurpose(request.Purpose) {
violations = append(violations, "purpose is not allowed")
}
if len([]byte(request.Prompt)) > maxAIInvocationPromptSize {
violations = append(violations, "prompt is too large")
}
if len([]byte(request.CurrentConfig)) > maxAIInvocationConfigSize {
violations = append(violations, "currentConfig is too large")
}
if request.ProviderID != "" && !safeIdentifier(request.ProviderID) {
violations = append(violations, "providerId is invalid")
}
if request.Model != "" && unsafeAIString(request.Model) {
violations = append(violations, "model is unsafe")
}
if len(request.ContextRefs) > maxAIInvocationContextRefs {
violations = append(violations, "contextRefs has too many keys")
}
for key, value := range request.ContextRefs {
if strings.TrimSpace(key) == "" || key != strings.TrimSpace(key) {
violations = append(violations, "contextRefs key is invalid")
}
if !validAIContextRef(value) {
violations = append(violations, fmt.Sprintf("contextRefs[%s] is invalid", key))
}
}
for _, value := range []fieldString{
{field: "requestId", value: request.RequestID},
{field: "prompt", value: request.Prompt},
{field: "currentConfig", value: request.CurrentConfig},
{field: "providerId", value: request.ProviderID},
{field: "model", value: request.Model},
} {
if unsafeAIString(value.value) {
violations = append(violations, value.field+" contains unsafe content")
}
}
return finish(violations)
}
func ValidateAIInvocationResponse(response domain.AIInvocationResponse) error {
var violations []string
violations = appendRequired(violations, "requestId", response.RequestID)
violations = appendRequired(violations, "purpose", response.Purpose)
violations = appendRequired(violations, "status", response.Status)
if unsafeAIString(response.Recommendation) {
violations = append(violations, "recommendation contains unsafe content")
}
if response.ConfigRecommendation != nil && unsafeAIString(response.ConfigRecommendation.SuggestedConfig) {
violations = append(violations, "configRecommendation contains unsafe content")
}
if response.Error != nil && unsafeAIString(response.Error.Message) {
violations = append(violations, "error message contains unsafe content")
}
return finish(violations)
}
func validAIContextRef(value string) bool {
trimmed := strings.TrimSpace(value)
if trimmed == "" || trimmed != value || unsafeAIString(value) {
return false
}
return strings.HasPrefix(value, "server://") || strings.HasPrefix(value, "log://") || strings.HasPrefix(value, "artifact://") || strings.HasPrefix(value, "input://")
}
func safeIdentifier(value string) bool {
trimmed := strings.TrimSpace(value)
if trimmed == "" || trimmed != value || len([]rune(value)) > 160 {
return false
}
for _, char := range value {
if (char >= 'a' && char <= 'z') || (char >= 'A' && char <= 'Z') || (char >= '0' && char <= '9') || char == '_' || char == '-' || char == '.' {
continue
}
return false
}
return true
}
func unsafeAIString(value string) bool {
return containsUnsafeRuntimeSecret(value) || looksLikeRawHostPath(value) || strings.Contains(strings.ToLower(value), "provider base url secret")
}
+140
View File
@@ -0,0 +1,140 @@
package validator
import (
"fmt"
"strings"
"browser.local/platform/domain"
)
const MaxArtifactDownloadBytes = MaxArtifactChunkBytes
func ValidateArtifactDownloadReferenceRequest(request domain.ArtifactDownloadReferenceRequest) error {
var violations []string
violations = appendRequired(violations, "artifactId", request.ArtifactID)
violations = appendArtifactIDViolations(violations, request.ArtifactID)
return finish(violations)
}
func ValidateArtifactDownloadReference(reference domain.ArtifactDownloadReference) error {
var violations []string
violations = appendRequired(violations, "artifactId", reference.ArtifactID)
violations = appendRequired(violations, "ownerId", reference.OwnerID)
violations = appendRequired(violations, "filename", reference.Filename)
violations = appendRequired(violations, "contentType", reference.ContentType)
violations = appendRequired(violations, "checksum", reference.Checksum)
violations = appendRequired(violations, "downloadUrl", reference.DownloadURL)
violations = appendRequired(violations, "storageBehavior", reference.StorageBehavior)
violations = appendArtifactIDViolations(violations, reference.ArtifactID)
if !validArtifactOwnerKind(reference.OwnerKind) {
violations = append(violations, "ownerKind is invalid")
}
if reference.State != domain.ArtifactStateAvailable {
violations = append(violations, "state must be available")
}
if reference.SizeBytes <= 0 {
violations = append(violations, "sizeBytes must be positive")
}
if reference.Checksum != "" && !validSHA256Checksum(reference.Checksum) {
violations = append(violations, "checksum must be sha256:<hex>")
}
if reference.ChunkSizeBytes <= 0 || reference.ChunkSizeBytes > MaxArtifactDownloadBytes {
violations = append(violations, fmt.Sprintf("chunkSizeBytes must be between 1 and %d", MaxArtifactDownloadBytes))
}
if reference.ExpiresAt.IsZero() {
violations = append(violations, "expiresAt is required")
}
for _, value := range []fieldString{
{field: "filename", value: reference.Filename},
{field: "contentType", value: reference.ContentType},
{field: "downloadUrl", value: reference.DownloadURL},
{field: "storageBehavior", value: reference.StorageBehavior},
} {
if unsafeArtifactString(value.value) {
violations = append(violations, value.field+" contains unsafe content")
}
}
if !strings.HasPrefix(reference.DownloadURL, "/api/v1/artifacts/") || !strings.HasSuffix(reference.DownloadURL, "/content") {
violations = append(violations, "downloadUrl must be a platform artifact content route")
}
return finish(violations)
}
func ValidateArtifactContentRequest(request domain.ArtifactContentRequest) error {
var violations []string
violations = appendRequired(violations, "artifactId", request.ArtifactID)
violations = appendArtifactIDViolations(violations, request.ArtifactID)
if request.Offset < 0 {
violations = append(violations, "offset must not be negative")
}
if request.Limit < 0 {
violations = append(violations, "limit must not be negative")
}
if request.Limit > MaxArtifactDownloadBytes {
violations = append(violations, fmt.Sprintf("limit must not exceed %d", MaxArtifactDownloadBytes))
}
return finish(violations)
}
func ValidateArtifactContent(content domain.ArtifactContent) error {
content = domain.CopyArtifactContent(content)
var violations []string
violations = appendRequired(violations, "artifactId", content.ArtifactID)
violations = appendRequired(violations, "filename", content.Filename)
violations = appendRequired(violations, "contentType", content.ContentType)
violations = appendRequired(violations, "checksum", content.Checksum)
violations = appendRequired(violations, "contentChecksum", content.ContentChecksum)
violations = appendArtifactIDViolations(violations, content.ArtifactID)
if content.Offset < 0 {
violations = append(violations, "offset must not be negative")
}
if content.SizeBytes < 0 {
violations = append(violations, "sizeBytes must not be negative")
}
if content.TotalSizeBytes <= 0 {
violations = append(violations, "totalSizeBytes must be positive")
}
if content.SizeBytes > MaxArtifactDownloadBytes {
violations = append(violations, fmt.Sprintf("sizeBytes must not exceed %d", MaxArtifactDownloadBytes))
}
if int64(len(content.Payload)) != content.SizeBytes {
violations = append(violations, "payload size must match sizeBytes")
}
if content.Offset+content.SizeBytes > content.TotalSizeBytes {
violations = append(violations, "range exceeds artifact size")
}
if content.Checksum != "" && !validSHA256Checksum(content.Checksum) {
violations = append(violations, "checksum must be sha256:<hex>")
}
if content.ContentChecksum != "" && content.ContentChecksum != BytesChecksum(content.Payload) {
violations = append(violations, "contentChecksum does not match payload")
}
for _, value := range []fieldString{
{field: "filename", value: content.Filename},
{field: "contentType", value: content.ContentType},
{field: "storageBehavior", value: content.StorageBehavior},
} {
if unsafeArtifactString(value.value) {
violations = append(violations, value.field+" contains unsafe content")
}
}
return finish(violations)
}
func appendArtifactIDViolations(violations []string, artifactID string) []string {
trimmed := strings.TrimSpace(artifactID)
if trimmed == "" {
return violations
}
if trimmed != artifactID || len([]rune(trimmed)) > 120 || strings.Contains(trimmed, "/") || strings.Contains(trimmed, `\`) || strings.Contains(trimmed, "://") || strings.Contains(trimmed, "..") {
violations = append(violations, "artifactId is invalid")
}
if unsafeArtifactString(trimmed) {
violations = append(violations, "artifactId contains unsafe content")
}
return violations
}
func unsafeArtifactString(value string) bool {
return containsUnsafeRuntimeSecret(value) || looksLikeRawHostPath(value) || strings.Contains(strings.ToLower(value), "file://")
}
+121
View File
@@ -0,0 +1,121 @@
package validator
import (
"crypto/sha256"
"encoding/hex"
"fmt"
"strings"
"browser.local/platform/domain"
)
const MaxArtifactChunkBytes = 1024 * 1024
func ValidateArtifactTransferOpen(open domain.ArtifactTransferOpen) error {
var violations []string
violations = appendRequired(violations, "runEndpointId", open.RunEndpointID)
violations = appendRequired(violations, "sessionToken", open.SessionToken)
violations = appendRequired(violations, "artifactId", open.ArtifactID)
violations = appendRequired(violations, "ownerId", open.OwnerID)
violations = appendRequired(violations, "checksum", open.Checksum)
violations = appendRequired(violations, "idempotencyKey", open.IdempotencyKey)
if open.Direction != domain.ArtifactTransferDirectionUpload {
violations = append(violations, "direction must be upload")
}
if !validArtifactOwnerKind(open.OwnerKind) {
violations = append(violations, "ownerKind is invalid")
}
if open.OwnerKind != domain.ArtifactOwnerKindJob && open.OwnerKind != domain.ArtifactOwnerKindServerInstance {
violations = append(violations, "ownerKind must be job or server-instance for run uploads")
}
if open.SizeBytes <= 0 {
violations = append(violations, "sizeBytes must be positive")
}
if open.ChunkSizeBytes <= 0 {
violations = append(violations, "chunkSizeBytes must be positive")
}
if open.ChunkSizeBytes > MaxArtifactChunkBytes {
violations = append(violations, fmt.Sprintf("chunkSizeBytes must not exceed %d", MaxArtifactChunkBytes))
}
if open.Checksum != "" && !validSHA256Checksum(open.Checksum) {
violations = append(violations, "checksum must be sha256:<hex>")
}
return finish(violations)
}
func ValidateArtifactChunkUpload(chunk domain.ArtifactChunkUpload) error {
var violations []string
violations = appendRequired(violations, "runEndpointId", chunk.RunEndpointID)
violations = appendRequired(violations, "sessionToken", chunk.SessionToken)
violations = appendRequired(violations, "transferId", chunk.TransferID)
violations = appendRequired(violations, "artifactId", chunk.ArtifactID)
violations = appendRequired(violations, "checksum", chunk.Checksum)
if chunk.ChunkIndex < 0 {
violations = append(violations, "chunkIndex must not be negative")
}
if chunk.Offset < 0 {
violations = append(violations, "offset must not be negative")
}
if chunk.SizeBytes <= 0 {
violations = append(violations, "sizeBytes must be positive")
}
if chunk.SizeBytes > MaxArtifactChunkBytes {
violations = append(violations, fmt.Sprintf("sizeBytes must not exceed %d", MaxArtifactChunkBytes))
}
if len(chunk.Payload) == 0 {
violations = append(violations, "payload must not be empty")
}
if chunk.SizeBytes > 0 && len(chunk.Payload) != chunk.SizeBytes {
violations = append(violations, "sizeBytes must match payload size")
}
if chunk.Checksum != "" {
if !validSHA256Checksum(chunk.Checksum) {
violations = append(violations, "checksum must be sha256:<hex>")
} else if chunk.Checksum != BytesChecksum(chunk.Payload) {
violations = append(violations, "checksum does not match payload")
}
}
return finish(violations)
}
func ValidateArtifactTransferStatusQuery(query domain.ArtifactTransferStatusQuery) error {
var violations []string
violations = appendRequired(violations, "runEndpointId", query.RunEndpointID)
violations = appendRequired(violations, "sessionToken", query.SessionToken)
violations = appendRequired(violations, "transferId", query.TransferID)
violations = appendRequired(violations, "artifactId", query.ArtifactID)
return finish(violations)
}
func ValidateArtifactTransferComplete(complete domain.ArtifactTransferComplete) error {
var violations []string
violations = appendRequired(violations, "runEndpointId", complete.RunEndpointID)
violations = appendRequired(violations, "sessionToken", complete.SessionToken)
violations = appendRequired(violations, "transferId", complete.TransferID)
violations = appendRequired(violations, "artifactId", complete.ArtifactID)
violations = appendRequired(violations, "checksum", complete.Checksum)
if complete.SizeBytes <= 0 {
violations = append(violations, "sizeBytes must be positive")
}
if complete.Checksum != "" && !validSHA256Checksum(complete.Checksum) {
violations = append(violations, "checksum must be sha256:<hex>")
}
return finish(violations)
}
func BytesChecksum(payload []byte) string {
sum := sha256.Sum256(payload)
return "sha256:" + hex.EncodeToString(sum[:])
}
func validSHA256Checksum(value string) bool {
if !strings.HasPrefix(value, "sha256:") {
return false
}
hexValue := strings.TrimPrefix(value, "sha256:")
if len(hexValue) != sha256.Size*2 {
return false
}
_, err := hex.DecodeString(hexValue)
return err == nil
}
+67
View File
@@ -0,0 +1,67 @@
package validator
import (
"fmt"
"strings"
"browser.local/platform/domain"
)
func ValidateRunControlHello(hello domain.RunControlHello) error {
var violations []string
violations = appendRequired(violations, "registrationToken", hello.RegistrationToken)
violations = appendRequired(violations, "runEndpointId", hello.RunEndpointID)
violations = appendRequired(violations, "displayName", hello.DisplayName)
violations = appendRequired(violations, "version", hello.Version)
violations = appendRequired(violations, "capabilityReport.fingerprint", hello.CapabilityReport.Fingerprint)
if !validRunControlStatus(hello.Status) {
violations = append(violations, "status is invalid")
}
violations = appendCapacityViolations(violations, hello.Capacity)
violations = appendCapabilitiesViolations(violations, "capabilityReport.capabilities", hello.CapabilityReport.Capabilities)
return finish(violations)
}
func ValidateRunControlHeartbeat(heartbeat domain.RunControlHeartbeat) error {
var violations []string
violations = appendRequired(violations, "runEndpointId", heartbeat.RunEndpointID)
violations = appendRequired(violations, "sessionToken", heartbeat.SessionToken)
violations = appendRequired(violations, "version", heartbeat.Version)
violations = appendRequired(violations, "capabilityFingerprint", heartbeat.CapabilityFingerprint)
if !validRunControlStatus(heartbeat.Status) {
violations = append(violations, "status is invalid")
}
violations = appendCapacityViolations(violations, heartbeat.Capacity)
return finish(violations)
}
func appendCapacityViolations(violations []string, capacity domain.RunCapacity) []string {
if capacity.MaxJobs < 0 || capacity.RunningJobs < 0 || capacity.QueuedJobs < 0 {
violations = append(violations, "capacity counts must not be negative")
}
if capacity.MaxJobs > 0 && capacity.RunningJobs > capacity.MaxJobs {
violations = append(violations, "runningJobs must not exceed maxJobs")
}
return violations
}
func appendCapabilitiesViolations(violations []string, field string, capabilities []string) []string {
if len(capabilities) == 0 {
violations = append(violations, field+" must not be empty")
}
for i, capability := range capabilities {
if strings.TrimSpace(capability) == "" {
violations = append(violations, fmt.Sprintf("%s[%d] is required", field, i))
}
}
return violations
}
func validRunControlStatus(status domain.RunEndpointStatus) bool {
switch status {
case domain.RunEndpointStatusOnline, domain.RunEndpointStatusDegraded, domain.RunEndpointStatusOffline:
return true
default:
return false
}
}
+121
View File
@@ -0,0 +1,121 @@
package validator
import (
"fmt"
"strings"
"browser.local/platform/domain"
)
const maxJobChannelMessageLength = 256
func ValidateRunJobClaim(claim domain.RunJobClaim) error {
var violations []string
violations = appendRequired(violations, "runEndpointId", claim.RunEndpointID)
violations = appendRequired(violations, "sessionToken", claim.SessionToken)
violations = appendCapacityViolations(violations, claim.Capacity)
for i, capability := range claim.Capabilities {
if strings.TrimSpace(capability) == "" {
violations = append(violations, fmt.Sprintf("capabilities[%d] is required", i))
}
}
return finish(violations)
}
func ValidateRunJobAck(ack domain.RunJobAck) error {
var violations []string
violations = appendLeaseFields(violations, ack.RunEndpointID, ack.SessionToken, ack.JobID, ack.LeaseToken, ack.Attempt)
violations = appendMessageLength(violations, "message", ack.Message)
return finish(violations)
}
func ValidateRunJobProgress(progress domain.RunJobProgress) error {
var violations []string
violations = appendLeaseFields(violations, progress.RunEndpointID, progress.SessionToken, progress.JobID, progress.LeaseToken, progress.Attempt)
violations = appendProgressViolations(violations, progress.Progress)
return finish(violations)
}
func ValidateRunJobResult(result domain.RunJobResult) error {
var violations []string
violations = appendLeaseFields(violations, result.RunEndpointID, result.SessionToken, result.JobID, result.LeaseToken, result.Attempt)
if !validTerminalJobState(result.State) {
violations = append(violations, "state must be succeeded, failed, or cancelled")
}
violations = appendProgressViolations(violations, result.Progress)
violations = appendMessageLength(violations, "message", result.Message)
violations = appendMessageLength(violations, "errorCode", result.ErrorCode)
return finish(violations)
}
func ValidateRunJobCancelRequest(request domain.RunJobCancelRequest) error {
var violations []string
violations = appendRequired(violations, "jobId", request.JobID)
violations = appendRequired(violations, "reason", request.Reason)
violations = appendMessageLength(violations, "reason", request.Reason)
return finish(violations)
}
func ValidateRunJobCancelPoll(poll domain.RunJobCancelPoll) error {
var violations []string
violations = appendRequired(violations, "runEndpointId", poll.RunEndpointID)
violations = appendRequired(violations, "sessionToken", poll.SessionToken)
if poll.LeaseToken != "" && strings.TrimSpace(poll.JobID) == "" {
violations = append(violations, "jobId is required when leaseToken is provided")
}
return finish(violations)
}
func ValidateRunJobReconcile(reconcile domain.RunJobReconcile) error {
var violations []string
violations = appendRequired(violations, "runEndpointId", reconcile.RunEndpointID)
violations = appendRequired(violations, "sessionToken", reconcile.SessionToken)
seen := map[string]struct{}{}
for i, jobID := range reconcile.ActiveJobIDs {
jobID = strings.TrimSpace(jobID)
if jobID == "" {
violations = append(violations, fmt.Sprintf("activeJobIds[%d] is required", i))
continue
}
if _, exists := seen[jobID]; exists {
violations = append(violations, fmt.Sprintf("activeJobIds[%d] duplicates %q", i, jobID))
}
seen[jobID] = struct{}{}
}
return finish(violations)
}
func appendLeaseFields(violations []string, runEndpointID string, sessionToken string, jobID string, leaseToken string, attempt int) []string {
violations = appendRequired(violations, "runEndpointId", runEndpointID)
violations = appendRequired(violations, "sessionToken", sessionToken)
violations = appendRequired(violations, "jobId", jobID)
violations = appendRequired(violations, "leaseToken", leaseToken)
if attempt <= 0 {
violations = append(violations, "attempt must be positive")
}
return violations
}
func appendProgressViolations(violations []string, progress domain.RunJobProgressReport) []string {
if progress.Percent < 0 || progress.Percent > 100 {
violations = append(violations, "progress.percent must be between 0 and 100")
}
violations = appendMessageLength(violations, "progress.message", progress.Message)
return violations
}
func appendMessageLength(violations []string, field string, message string) []string {
if len(message) > maxJobChannelMessageLength {
violations = append(violations, field+" is too long")
}
return violations
}
func validTerminalJobState(state domain.JobState) bool {
switch state {
case domain.JobStateSucceeded, domain.JobStateFailed, domain.JobStateCancelled:
return true
default:
return false
}
}
+117
View File
@@ -0,0 +1,117 @@
package validator
import (
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"strings"
"browser.local/platform/domain"
)
const (
MaxLogBatchEntries = 512
MaxLogLineLength = 8192
MaxLogQueryLimit = 500
)
func ValidateLogBatchIngest(batch domain.LogBatchIngest) error {
var violations []string
violations = appendRequired(violations, "runEndpointId", batch.RunEndpointID)
violations = appendRequired(violations, "sessionToken", batch.SessionToken)
violations = appendRequired(violations, "logStreamId", batch.LogStreamID)
violations = appendRequired(violations, "serverInstanceId", batch.ServerInstanceID)
violations = appendRequired(violations, "streamKey", batch.StreamKey)
violations = appendRequired(violations, "checksum", batch.Checksum)
if !validLogStreamSource(batch.Source) {
violations = append(violations, "source is invalid")
}
if batch.FirstSeq == 0 || batch.LastSeq == 0 {
violations = append(violations, "sequence range must be positive")
}
if batch.FirstSeq > batch.LastSeq {
violations = append(violations, "firstSeq must not exceed lastSeq")
}
if batch.Compression != "" && batch.Compression != "none" {
violations = append(violations, "compression is invalid")
}
if len(batch.Entries) == 0 {
violations = append(violations, "entries must not be empty")
}
if len(batch.Entries) > MaxLogBatchEntries {
violations = append(violations, fmt.Sprintf("entries must not exceed %d", MaxLogBatchEntries))
}
if len(batch.Entries) > 0 {
expectedCount := int(batch.LastSeq - batch.FirstSeq + 1)
if expectedCount != len(batch.Entries) {
violations = append(violations, "sequence range must match entry count")
}
}
for i, entry := range batch.Entries {
if entry.Seq != batch.FirstSeq+uint64(i) {
violations = append(violations, fmt.Sprintf("entries[%d].seq must be contiguous", i))
}
if strings.TrimSpace(entry.Line) == "" {
violations = append(violations, fmt.Sprintf("entries[%d].line is required", i))
}
if len(entry.Line) > MaxLogLineLength {
violations = append(violations, fmt.Sprintf("entries[%d].line is too long", i))
}
for key := range entry.Fields {
if strings.TrimSpace(key) == "" {
violations = append(violations, fmt.Sprintf("entries[%d].fields key is required", i))
}
}
}
if batch.Checksum != "" {
computed, err := LogEntriesChecksum(batch.Entries)
if err != nil {
violations = append(violations, "checksum cannot be computed")
} else if batch.Checksum != computed {
violations = append(violations, "checksum does not match entries")
}
}
return finish(violations)
}
func ValidateLogStreamCursorQuery(query domain.LogStreamCursorQuery) error {
var violations []string
violations = appendRequired(violations, "logStreamId", query.LogStreamID)
if query.Limit < 0 {
violations = append(violations, "limit must not be negative")
}
if query.Limit > MaxLogQueryLimit {
violations = append(violations, fmt.Sprintf("limit must not exceed %d", MaxLogQueryLimit))
}
return finish(violations)
}
func LogEntriesChecksum(entries []domain.LogEntry) (string, error) {
stable := make([]logEntryChecksumBody, len(entries))
for i, entry := range entries {
stable[i] = logEntryChecksumBody{
Seq: entry.Seq,
Timestamp: entry.Timestamp.UTC().Format("2006-01-02T15:04:05.000000000Z07:00"),
Level: entry.Level,
Line: entry.Line,
Fields: entry.Fields,
Redacted: entry.Redacted,
}
}
encoded, err := json.Marshal(stable)
if err != nil {
return "", err
}
sum := sha256.Sum256(encoded)
return "sha256:" + hex.EncodeToString(sum[:]), nil
}
type logEntryChecksumBody struct {
Seq uint64 `json:"seq"`
Timestamp string `json:"timestamp"`
Level string `json:"level,omitempty"`
Line string `json:"line"`
Fields map[string]string `json:"fields,omitempty"`
Redacted bool `json:"redacted"`
}
File diff suppressed because it is too large Load Diff
+221
View File
@@ -0,0 +1,221 @@
package validator
import (
"strings"
"testing"
"browser.local/platform/domain"
)
func TestValidateAIProviderRejectsRawSecret(t *testing.T) {
provider := validAIProvider()
provider.APIKeyRef = "sk-test-secret"
err := ValidateAIProvider(provider)
if err == nil || !strings.Contains(err.Error(), "apiKeyRef must reference secret storage") {
t.Fatalf("expected raw secret rejection, got %v", err)
}
}
func TestValidateAIProviderRequiresDefaultModelInModels(t *testing.T) {
provider := validAIProvider()
provider.DefaultModel = "missing-model"
err := ValidateAIProvider(provider)
if err == nil || !strings.Contains(err.Error(), "defaultModel must be included") {
t.Fatalf("expected default model validation, got %v", err)
}
}
func TestValidateGamePluginManifestRegistration(t *testing.T) {
registration := validGamePluginManifestRegistration()
if err := ValidateGamePluginManifestRegistration(registration); err != nil {
t.Fatalf("expected manifest registration to validate, got %v", err)
}
}
func TestValidateGamePluginManifestRegistrationRejectsUnsafeRequests(t *testing.T) {
registration := validGamePluginManifestRegistration()
registration.Manifest.Description = "requires direct run socket and raw AI key material"
registration.ManifestRef = "file:///etc/plugin.json"
err := ValidateGamePluginManifestRegistration(registration)
if err == nil {
t.Fatal("expected unsafe manifest registration rejection")
}
message := err.Error()
for _, want := range []string{"manifestRef is unsafe", "direct run access", "raw credential or AI/provider key"} {
if !strings.Contains(message, want) {
t.Fatalf("expected validation error to contain %q, got %v", want, err)
}
}
}
func TestValidateGamePluginManifestRegistrationRejectsUnsafeCapabilitiesAndPermissions(t *testing.T) {
registration := validGamePluginManifestRegistration()
registration.Manifest.Capabilities = append(registration.Manifest.Capabilities, "run.socket")
registration.Manifest.Permissions = append(registration.Manifest.Permissions, "provider.key.read")
err := ValidateGamePluginManifestRegistration(registration)
if err == nil || !strings.Contains(err.Error(), "manifest.capabilities") || !strings.Contains(err.Error(), "permissions") {
t.Fatalf("expected unsafe capability and permission rejection, got %v", err)
}
}
func TestValidateServerInstanceDependencies(t *testing.T) {
instance := domain.ServerInstance{
ID: "server-1",
PluginID: "server.scum",
PluginVersion: "1.0.0",
RunEndpointID: "run-local",
Name: "SCUM #1",
State: domain.ServerInstanceStateDraft,
ConfigVersion: 1,
}
plugin := domain.GamePlugin{
ID: "server.scum",
Version: "1.0.0",
RequiredRunCapabilities: []string{"process.start", "logs.read"},
Status: domain.GamePluginStatusInstalled,
}
endpoint := domain.RunEndpoint{
ID: "run-local",
Status: domain.RunEndpointStatusOnline,
Capabilities: []string{"process.start", "logs.read", "files.read"},
}
if err := ValidateServerInstanceDependencies(instance, plugin, endpoint); err != nil {
t.Fatalf("expected valid dependencies, got %v", err)
}
endpoint.Capabilities = []string{"process.start"}
err := ValidateServerInstanceDependencies(instance, plugin, endpoint)
if err == nil || !strings.Contains(err.Error(), "logs.read") {
t.Fatalf("expected missing capability error, got %v", err)
}
}
func TestValidateServerInstanceRejectsDeletedCreateState(t *testing.T) {
instance := domain.ServerInstance{
ID: "server-1",
PluginID: "server.scum",
PluginVersion: "1.0.0",
RunEndpointID: "run-local",
Name: "SCUM #1",
State: domain.ServerInstanceStateDeleted,
}
err := ValidateServerInstance(instance)
if err == nil || !strings.Contains(err.Error(), "state must not be deleted") {
t.Fatalf("expected deleted state rejection, got %v", err)
}
}
func TestValidateJobBoundsProgress(t *testing.T) {
job := domain.Job{
ID: "job-1",
RunEndpointID: "run-local",
Capability: "process.start",
IdempotencyKey: "idem-1",
State: domain.JobStateQueued,
Progress: domain.JobProgress{Percent: 101},
}
err := ValidateJob(job)
if err == nil || !strings.Contains(err.Error(), "progress.percent") {
t.Fatalf("expected progress bounds error, got %v", err)
}
}
func TestValidateArtifactLogAndAudit(t *testing.T) {
artifact := domain.Artifact{
ID: "artifact-1",
OwnerKind: domain.ArtifactOwnerKindJob,
OwnerID: "job-1",
SizeBytes: 10,
Checksum: "sha256:abc",
State: domain.ArtifactStateAvailable,
}
if err := ValidateArtifact(artifact); err != nil {
t.Fatalf("expected artifact to validate, got %v", err)
}
stream := domain.LogStream{
ID: "log-1",
ServerInstanceID: "server-1",
Source: domain.LogStreamSourceFile,
StreamKey: "server.log",
StorageBackend: domain.LogStorageBackendLocalSegments,
RetentionPolicy: "default",
}
if err := ValidateLogStream(stream); err != nil {
t.Fatalf("expected log stream to validate, got %v", err)
}
audit := domain.AuditEvent{
ID: "audit-1",
ActorID: "user-1",
Action: "server.create",
ResourceKind: "server-instance",
ResourceID: "server-1",
Result: domain.AuditResultSuccess,
Summary: "created server instance",
}
if err := ValidateAuditEvent(audit); err != nil {
t.Fatalf("expected audit event to validate, got %v", err)
}
audit.Summary = "bearer raw-secret"
if err := ValidateAuditEvent(audit); err == nil || !strings.Contains(err.Error(), "summary must be redacted") {
t.Fatalf("expected audit redaction error, got %v", err)
}
}
func validAIProvider() domain.AIProvider {
return domain.AIProvider{
ID: "ai.openai",
Name: "OpenAI",
Kind: domain.AIProviderKindOpenAI,
BaseURL: "https://api.openai.com/v1",
APIKeyRef: "secret://providers/openai",
Models: []string{"gpt-4.1", "gpt-4.1-mini"},
DefaultModel: "gpt-4.1",
RelayMode: domain.AIRelayModeDirect,
TimeoutMS: 30000,
Status: domain.AIProviderStatusActive,
RedactionPolicy: "default",
}
}
func validGamePluginManifestRegistration() domain.GamePluginManifestRegistration {
return domain.GamePluginManifestRegistration{
ManifestRef: "artifact://manifests/game.example/0.1.0",
Manifest: domain.GamePluginManifest{
ID: "game.example",
Name: "Example Server",
Description: "Development plugin",
Version: "0.1.0",
Kind: "game-plugin",
Tags: []string{"example", "development"},
Server: domain.GamePluginManifestServer{
Type: "example",
DisplayName: "Example Server",
SupportedOS: []string{"linux", "darwin"},
CreateFormSchema: "schemas/create-form.schema.json",
},
Capabilities: []string{"process.install", "process.start", "process.stop", "logs.read", "files.read", "artifacts.read", "ai.invoke"},
Permissions: []string{"server.read", "server.lifecycle", "server.logs.read", "server.files.read", "server.artifacts.read", "ai.invoke"},
Actions: domain.PluginLifecycleActions{
Install: "actions/install.json",
Start: "actions/start.json",
Stop: "actions/stop.json",
Restart: "actions/restart.json",
},
Pages: []domain.GamePluginPage{
{Key: "logs", Title: "Logs", Path: "/logs", Permissions: []string{"server.logs.read"}},
},
AI: domain.GamePluginManifestAI{Purposes: []string{"logs.diagnose"}},
},
}
}
+10
View File
@@ -0,0 +1,10 @@
# Platform Validation Rules
- API handlers must use named DTOs from `platform/dto`.
- Platform services must not accept raw plugin-provided host paths.
- AI provider secrets must be stored by reference and redacted from logs, audit, and plugin bridge responses.
- Game management plugin installation must validate manifest identity, server type, required run capabilities, pages, permissions, and schema references.
- Server instance creation must validate plugin installation state and run endpoint capability compatibility.
- `platform/validator/resources.go` validates required IDs, enum values, AI key-reference shape, bounded progress/audit summaries, artifact metadata, log stream cursors, and run capability compatibility.
- `platform/service.Core` must call validators before repository writes and must reject server creation when the plugin is not installed, the run endpoint is disabled/offline, or required run capabilities are missing.
- Job creation must require an idempotency key and return the existing job for duplicate `(runEndpointId, idempotencyKey)` pairs.
+50
View File
@@ -0,0 +1,50 @@
package validator
import (
"fmt"
"strings"
"browser.local/platform/domain"
)
const maxLifecycleIdempotencyKeyLength = 160
func ValidateServerLifecycleCreate(create domain.ServerLifecycleCreate) error {
var violations []string
violations = appendRequired(violations, "id", create.ID)
violations = appendRequired(violations, "pluginId", create.PluginID)
violations = appendRequired(violations, "runEndpointId", create.RunEndpointID)
violations = appendRequired(violations, "name", create.Name)
violations = appendLifecycleIdempotencyViolations(violations, create.IdempotencyKey)
return finish(violations)
}
func ValidateServerLifecycleCommand(command domain.ServerLifecycleCommand) error {
var violations []string
violations = appendRequired(violations, "serverInstanceId", command.ServerInstanceID)
if command.ExpectedConfigVersion <= 0 {
violations = append(violations, "expectedConfigVersion must be positive")
}
violations = appendLifecycleIdempotencyViolations(violations, command.IdempotencyKey)
return finish(violations)
}
func ValidateServerLifecycleAction(action domain.ServerLifecycleAction) error {
switch action {
case domain.ServerLifecycleActionCreate, domain.ServerLifecycleActionStart, domain.ServerLifecycleActionStop:
return nil
default:
return ValidationError{Violations: []string{fmt.Sprintf("action %q is invalid", action)}}
}
}
func appendLifecycleIdempotencyViolations(violations []string, key string) []string {
violations = appendRequired(violations, "idempotencyKey", key)
if len(key) > maxLifecycleIdempotencyKeyLength {
violations = append(violations, "idempotencyKey is too long")
}
if strings.TrimSpace(key) != key {
violations = append(violations, "idempotencyKey must not have surrounding whitespace")
}
return violations
}