From 4abb0e66026ce905025465bf0e5d32ea266abb0d Mon Sep 17 00:00:00 2001 From: sususu Date: Fri, 7 Aug 2026 16:39:07 +0800 Subject: [PATCH] feat(signature): add Grok as a target-only provider family xAI had no SignatureProvider value, so a Grok target could not be named in a compatibility decision and fell through to the unknown-target branch, which reports no compatible replacement rather than the correct policy. SignatureProviderGrok is deliberately target-only and DetectSignatureProvider never returns it. xAI ciphertext carries no envelope, no version byte and no fixed length, and is statistically indistinguishable from uniform random bytes, so any positive detection rule would also claim every other opaque high-entropy payload. Callers establish an xAI target from the model or route, then use InspectGrokEncryptedContent as a replay-safety shape check. A test replays the native corpus to pin that detection keeps returning unknown. A Grok target drops the whole block when the signature does not match, in contrast to Kimi: xAI decrypts encrypted_content on replay and rejects foreign or mutated blobs with 400, so the block cannot survive by shedding just its signature. Also correct the recorded floor for MinGrokEncryptedContentDecodedLen. The comment claimed the shortest observed native payload was exactly 50 bytes, which read like a protocol minimum; a later capture from grok-4.5 and grok-composer-2.5-fast reached 43 and 48 bytes. The observed minimum keeps sliding, which is the argument for keeping the constant well below it and letting the entropy check filter. The value is unchanged. --- internal/signature/grok_validation.go | 15 +++--- internal/signature/grok_validation_test.go | 57 ++++++++++++++++++++ internal/signature/provider_compatibility.go | 18 +++++++ 3 files changed, 84 insertions(+), 6 deletions(-) diff --git a/internal/signature/grok_validation.go b/internal/signature/grok_validation.go index ba09ad94..8b424ac3 100644 --- a/internal/signature/grok_validation.go +++ b/internal/signature/grok_validation.go @@ -11,12 +11,15 @@ import ( const ( // MaxGrokEncryptedContentLen is a transport safety cap for opaque replay blobs. MaxGrokEncryptedContentLen = 8 * 1024 * 1024 - // MinGrokEncryptedContentDecodedLen is a deliberately loose floor. The - // shortest observed native payload is exactly 50 bytes (grok-composer-2.5-fast), - // and those samples share no structure, so 50 is a sampling artifact rather - // than a protocol minimum. Sitting on the observed floor would silently reject - // a future shorter payload and surface as lost reasoning context, so keep - // headroom here and let the entropy check do the real filtering. + // MinGrokEncryptedContentDecodedLen is a deliberately loose floor, and the + // headroom has already proven necessary. An earlier corpus of 207 samples put + // the shortest native payload at exactly 50 bytes, with several samples piled + // on that value, which read like a protocol floor; a later 215-sample capture + // from grok-4.5 and grok-composer-2.5-fast reached 43 and 48 bytes and moved + // it. Both corpora agree there is no structure to anchor on, so the observed + // minimum is a sampling artifact that keeps sliding, and sitting on it would + // silently reject a future shorter payload as lost reasoning context. Keep the + // floor low and let the entropy check do the real filtering. MinGrokEncryptedContentDecodedLen = 32 // MinGrokEncryptedContentEntropyRatio rejects obvious non-ciphertext payloads. // Native samples are >= 0.892 against the sample-size entropy ceiling. diff --git a/internal/signature/grok_validation_test.go b/internal/signature/grok_validation_test.go index ca3383d8..1c9a2d72 100644 --- a/internal/signature/grok_validation_test.go +++ b/internal/signature/grok_validation_test.go @@ -333,3 +333,60 @@ func grokEncryptedContentSamplesPath() (string, bool) { } return path, true } + +func TestSignatureProviderFromModelName_Grok(t *testing.T) { + for _, model := range []string{"grok-4.5", "grok-4.5-build", "grok-composer-2.5-fast", "grok-code-fast-1"} { + t.Run(model, func(t *testing.T) { + if got := SignatureProviderFromModelName(model); got != SignatureProviderGrok { + t.Errorf("SignatureProviderFromModelName(%q) = %q, want %q", model, got, SignatureProviderGrok) + } + }) + } +} + +// TestDetectSignatureProvider_NeverClassifiesGrok pins the contract that xAI is +// a target-only family. Its ciphertext carries no envelope, no version byte and +// no fixed length, so a positive detection rule would necessarily also claim +// unrelated opaque payloads. Callers establish an xAI target from provenance and +// then use InspectGrokEncryptedContent as a replay-safety check. +func TestDetectSignatureProvider_NeverClassifiesGrok(t *testing.T) { + path, ok := grokEncryptedContentSamplesPath() + if !ok { + t.Skip("grok encrypted_content corpus missing; run docs/native-prompt-capture/scripts/harvest-grok-encrypted-content.sh") + } + raw, err := os.ReadFile(path) + if err != nil { + t.Fatalf("read grok corpus: %v", err) + } + var samples []string + if err := json.Unmarshal(raw, &samples); err != nil { + var wrapped struct { + Samples []string `json:"samples"` + } + if err := json.Unmarshal(raw, &wrapped); err != nil { + t.Fatalf("parse grok corpus: %v", err) + } + samples = wrapped.Samples + } + if len(samples) == 0 { + t.Skip("grok encrypted_content corpus is empty") + } + for _, sig := range samples { + if got := DetectSignatureProvider(sig); got != SignatureProviderUnknown { + t.Fatalf("DetectSignatureProvider = %q, want %q for native encrypted_content", got, SignatureProviderUnknown) + } + } +} + +// TestDecideSignatureCompatibility_GrokDropsBlock contrasts with the Kimi +// policy: xAI decrypts the blob and answers 400 for foreign or mutated input, so +// an incompatible block cannot survive by shedding just its signature. +func TestDecideSignatureCompatibility_GrokDropsBlock(t *testing.T) { + decision := DecideSignatureCompatibility(SignatureProviderGrok, observedFable5Sample, SignatureBlockKindUnknown) + if decision.Compatible { + t.Fatalf("Claude signature reported compatible with a Grok target") + } + if decision.Action != SignatureActionDropBlock { + t.Errorf("Action = %q, want %q", decision.Action, SignatureActionDropBlock) + } +} diff --git a/internal/signature/provider_compatibility.go b/internal/signature/provider_compatibility.go index 79d7530a..2eba2cc1 100644 --- a/internal/signature/provider_compatibility.go +++ b/internal/signature/provider_compatibility.go @@ -13,6 +13,13 @@ const ( // SignatureProviderKimi is identified by fixed signature size rather than by // an envelope. See kimi_validation.go for the empirical basis and its limits. SignatureProviderKimi SignatureProvider = "kimi" + // SignatureProviderGrok is a target-only family. DetectSignatureProvider never + // returns it: xAI emits no envelope, no version byte and no fixed length, and + // its ciphertext is statistically indistinguishable from uniform random bytes, + // so any positive claim would also capture every other opaque blob. Grok + // handling is provenance-first - establish the target from the model or route, + // then use InspectGrokEncryptedContent as a replay-safety shape check. + SignatureProviderGrok SignatureProvider = "grok" ) type SignatureBlockKind string @@ -67,6 +74,8 @@ func SignatureProviderFromModelName(modelName string) SignatureProvider { strings.HasPrefix(lower, "k2"), strings.HasPrefix(lower, "k3"): return SignatureProviderKimi + case strings.Contains(lower, "grok"): + return SignatureProviderGrok default: return SignatureProviderUnknown } @@ -283,6 +292,12 @@ func DecideSignatureCompatibilityForModel(targetProvider SignatureProvider, targ // thinking text for no upstream benefit, so drop only the signature. decision.Action = SignatureActionDropSignature decision.Reason = "Kimi does not validate replayed thinking signatures, so the block survives without one" + case SignatureProviderGrok: + // xAI decrypts encrypted_content and rejects the request with 400 + // "Could not decrypt" when the blob is foreign or mutated, so a + // non-matching value has to leave with the block. + decision.Action = SignatureActionDropBlock + decision.Reason = "xAI verifies encrypted_content on replay and rejects foreign or mutated blobs" default: decision.Action = SignatureActionNoCompatibleReplacement decision.Reason = "unknown target provider" @@ -404,6 +419,9 @@ func signatureProviderMatchesTarget(target, detected SignatureProvider) bool { case SignatureProviderKimi: return detected == SignatureProviderKimi default: + // SignatureProviderGrok is deliberately absent. Detection never yields it, + // so a Grok target must decide replay safety from provenance plus + // InspectGrokEncryptedContent rather than from a detected-provider match. return false } } -- 2.51.2