From 42eef103d6d2cd4b90989c05525e04ba9dc2edcb Mon Sep 17 00:00:00 2001 From: Luis Pater Date: Wed, 5 Aug 2026 00:27:32 +0800 Subject: [PATCH] feat(antigravity): obfuscate sensitive words in system instructions Closes: #4696 #4723 #4732 --- config.example.yaml | 6 ++ internal/config/clone_test.go | 15 +++++ internal/config/config.go | 3 + internal/config/config_types.go | 6 ++ .../runtime/executor/antigravity_executor.go | 8 +++ .../executor/antigravity_executor_execute.go | 2 + .../antigravity_executor_signature_test.go | 65 +++++++++++++++++++ .../executor/antigravity_executor_stream.go | 1 + .../executor/antigravity_executor_tokens.go | 1 + .../runtime/executor/helps/cloak_obfuscate.go | 38 +++++++++++ internal/watcher/diff/config_diff.go | 3 + internal/watcher/diff/config_diff_test.go | 3 + 12 files changed, 151 insertions(+) diff --git a/config.example.yaml b/config.example.yaml index 5020eaef8..17fc3169b 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -254,6 +254,12 @@ codex: # username: "user" # credential: "secret" +# Antigravity provider behavior. +# antigravity: +# sensitive-words: # optional: words to obfuscate with zero-width characters in system instructions +# - "API" +# - "proxy" + # xAI provider behavior. xai: # When true, inject the native x_search tool when the request does not declare it. diff --git a/internal/config/clone_test.go b/internal/config/clone_test.go index 1ee33035f..7b657d45c 100644 --- a/internal/config/clone_test.go +++ b/internal/config/clone_test.go @@ -15,6 +15,21 @@ func TestCloneForRuntimeNil(t *testing.T) { } } +func TestParseConfigBytes_AntigravitySensitiveWords(t *testing.T) { + cfg, errParse := ParseConfigBytes([]byte(`antigravity: + sensitive-words: + - "API" + - "proxy" +`)) + if errParse != nil { + t.Fatalf("ParseConfigBytes() error = %v", errParse) + } + want := []string{"API", "proxy"} + if !reflect.DeepEqual(cfg.Antigravity.SensitiveWords, want) { + t.Fatalf("Antigravity.SensitiveWords = %#v, want %#v", cfg.Antigravity.SensitiveWords, want) + } +} + func TestCloneForRuntimeDeepCopiesConfig(t *testing.T) { cfg := sampleCloneRuntimeConfig() diff --git a/internal/config/config.go b/internal/config/config.go index 45eae4611..4718f253b 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -100,6 +100,9 @@ type Config struct { AntigravitySignatureBypassStrict *bool `yaml:"antigravity-signature-bypass-strict,omitempty" json:"antigravity-signature-bypass-strict,omitempty"` + // Antigravity configures provider-wide Antigravity request behavior. + Antigravity AntigravityConfig `yaml:"antigravity" json:"antigravity"` + // GeminiKey defines Gemini API key configurations with optional routing overrides. GeminiKey []GeminiKey `yaml:"gemini-api-key" json:"gemini-api-key"` diff --git a/internal/config/config_types.go b/internal/config/config_types.go index 198750a21..ca71086dd 100644 --- a/internal/config/config_types.go +++ b/internal/config/config_types.go @@ -125,6 +125,12 @@ type XAIConfig struct { InjectXSearch bool `yaml:"inject-x-search" json:"inject-x-search"` } +// AntigravityConfig configures provider-wide Antigravity request behavior. +type AntigravityConfig struct { + // SensitiveWords is a list of words to obfuscate with zero-width characters in system instructions. + SensitiveWords []string `yaml:"sensitive-words,omitempty" json:"sensitive-words,omitempty"` +} + // CodexConfig configures provider-wide Codex request behavior. type CodexConfig struct { IdentityConfuse bool `yaml:"identity-confuse" json:"identity-confuse"` diff --git a/internal/runtime/executor/antigravity_executor.go b/internal/runtime/executor/antigravity_executor.go index 460a73077..02e01b957 100644 --- a/internal/runtime/executor/antigravity_executor.go +++ b/internal/runtime/executor/antigravity_executor.go @@ -59,6 +59,14 @@ func NewAntigravityExecutor(cfg *config.Config) *AntigravityExecutor { return &AntigravityExecutor{cfg: cfg} } +func (e *AntigravityExecutor) obfuscateSensitiveWords(payload []byte) []byte { + if e == nil || e.cfg == nil || len(e.cfg.Antigravity.SensitiveWords) == 0 { + return payload + } + matcher := helps.BuildSensitiveWordMatcher(e.cfg.Antigravity.SensitiveWords) + return helps.ObfuscateSensitiveWordsInSystemInstruction(payload, matcher) +} + // antigravityTransport is a singleton HTTP/1.1 transport shared by all Antigravity requests. // It is initialized once via antigravityTransportOnce to avoid leaking a new connection pool // (and the goroutines managing it) on every request. diff --git a/internal/runtime/executor/antigravity_executor_execute.go b/internal/runtime/executor/antigravity_executor_execute.go index f183e9613..fb9c84d69 100644 --- a/internal/runtime/executor/antigravity_executor_execute.go +++ b/internal/runtime/executor/antigravity_executor_execute.go @@ -77,6 +77,7 @@ func (e *AntigravityExecutor) Execute(ctx context.Context, auth *cliproxyauth.Au requestedModel := helps.PayloadRequestedModel(opts, req.Model) requestPath := helps.PayloadRequestPath(opts) translated = helps.ApplyPayloadConfigWithRequest(e.cfg, baseModel, "antigravity", from.String(), "request", translated, originalTranslated, requestedModel, requestPath, opts.Headers) + translated = e.obfuscateSensitiveWords(translated) translated = sanitizeAntigravityGeminiRequestSignatures(baseModel, translated) reporter.SetTranslatedReasoningEffort(translated, to.String()) @@ -300,6 +301,7 @@ func (e *AntigravityExecutor) executeClaudeNonStream(ctx context.Context, auth * requestedModel := helps.PayloadRequestedModel(opts, req.Model) requestPath := helps.PayloadRequestPath(opts) translated = helps.ApplyPayloadConfigWithRequest(e.cfg, baseModel, "antigravity", from.String(), "request", translated, originalTranslated, requestedModel, requestPath, opts.Headers) + translated = e.obfuscateSensitiveWords(translated) translated = sanitizeAntigravityGeminiRequestSignatures(baseModel, translated) reporter.SetTranslatedReasoningEffort(translated, to.String()) diff --git a/internal/runtime/executor/antigravity_executor_signature_test.go b/internal/runtime/executor/antigravity_executor_signature_test.go index 6a56cc2c4..532a2d2a3 100644 --- a/internal/runtime/executor/antigravity_executor_signature_test.go +++ b/internal/runtime/executor/antigravity_executor_signature_test.go @@ -145,6 +145,71 @@ func TestSanitizeAntigravityGeminiRequestSignaturesFinalizesParallelCalls(t *tes } } +func TestAntigravitySensitiveWordsObfuscatesSystemInstructionOnly(t *testing.T) { + executor := NewAntigravityExecutor(&config.Config{ + Antigravity: config.AntigravityConfig{SensitiveWords: []string{"proxy"}}, + }) + payload := []byte(`{"request":{"systemInstruction":{"parts":[{"text":"Use proxy safely"}]},"contents":[{"role":"user","parts":[{"text":"proxy remains unchanged"}]}]}}`) + + got := executor.obfuscateSensitiveWords(payload) + if systemText := gjson.GetBytes(got, "request.systemInstruction.parts.0.text").String(); systemText != "Use p\u200Broxy safely" { + t.Fatalf("system instruction = %q, want zero-width obfuscation", systemText) + } + if contentText := gjson.GetBytes(got, "request.contents.0.parts.0.text").String(); contentText != "proxy remains unchanged" { + t.Fatalf("content text = %q, want unchanged", contentText) + } +} + +func TestAntigravityStreamObfuscatesSensitiveSystemInstruction(t *testing.T) { + captured := make(chan []byte, 1) + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + body, errRead := io.ReadAll(r.Body) + if errRead != nil { + t.Errorf("read request body: %v", errRead) + return + } + captured <- body + w.Header().Set("Content-Type", "text/event-stream") + _, _ = w.Write([]byte("data: {}\n\n")) + })) + defer server.Close() + + executor := NewAntigravityExecutor(&config.Config{ + Antigravity: config.AntigravityConfig{SensitiveWords: []string{"Hermes", "Nous Research"}}, + RequestRetry: 1, + }) + result, errExecute := executor.ExecuteStream(context.Background(), &cliproxyauth.Auth{ + Metadata: map[string]any{ + "access_token": "token-123", + "expired": time.Now().Add(24 * time.Hour).Format(time.RFC3339), + "project_id": "project-1", + }, + Attributes: map[string]string{"base_url": server.URL}, + }, cliproxyexecutor.Request{ + Model: "gemini-3.6-flash-high", + Payload: []byte(`{"model":"gemini-3.6-flash-high","instructions":"You are Hermes Agent, an intelligent AI assistant created by Nous Research.","input":[{"type":"message","role":"user","content":[{"type":"input_text","text":"hello"}]}]}`), + }, cliproxyexecutor.Options{ + SourceFormat: sdktranslator.FormatOpenAIResponse, + ResponseFormat: sdktranslator.FormatOpenAIResponse, + Stream: true, + }) + if errExecute != nil { + t.Fatalf("ExecuteStream() error = %v", errExecute) + } + for chunk := range result.Chunks { + if chunk.Err != nil { + t.Fatalf("stream chunk error = %v", chunk.Err) + } + } + + body := <-captured + got := gjson.GetBytes(body, "request.systemInstruction.parts.0.text").String() + want := "You are H\u200Bermes Agent, an intelligent AI assistant created by N\u200Bous Research." + if got != want { + t.Fatalf("system instruction = %q, want %q; body=%s", got, want, body) + } +} + func TestAntigravityExecutorCountTokensSanitizesGeminiToolHistory(t *testing.T) { inner := protowire.AppendTag(nil, 1, protowire.BytesType) inner = protowire.AppendBytes(inner, []byte{0x01, 0x0c, 0x39, 0xd6, 0xc7, 0x34}) diff --git a/internal/runtime/executor/antigravity_executor_stream.go b/internal/runtime/executor/antigravity_executor_stream.go index 16760af0c..876a757d1 100644 --- a/internal/runtime/executor/antigravity_executor_stream.go +++ b/internal/runtime/executor/antigravity_executor_stream.go @@ -72,6 +72,7 @@ func (e *AntigravityExecutor) ExecuteStream(ctx context.Context, auth *cliproxya requestedModel := helps.PayloadRequestedModel(opts, req.Model) requestPath := helps.PayloadRequestPath(opts) translated = helps.ApplyPayloadConfigWithRequest(e.cfg, baseModel, "antigravity", from.String(), "request", translated, originalTranslated, requestedModel, requestPath, opts.Headers) + translated = e.obfuscateSensitiveWords(translated) translated = sanitizeAntigravityGeminiRequestSignatures(baseModel, translated) translated, _ = sjson.DeleteBytes(translated, "request.stream") reporter.SetTranslatedReasoningEffort(translated, to.String()) diff --git a/internal/runtime/executor/antigravity_executor_tokens.go b/internal/runtime/executor/antigravity_executor_tokens.go index 108847de5..663a3bfe8 100644 --- a/internal/runtime/executor/antigravity_executor_tokens.go +++ b/internal/runtime/executor/antigravity_executor_tokens.go @@ -55,6 +55,7 @@ func (e *AntigravityExecutor) CountTokens(ctx context.Context, auth *cliproxyaut if err != nil { return cliproxyexecutor.Response{}, err } + payload = e.obfuscateSensitiveWords(payload) payload = sanitizeAntigravityGeminiRequestSignatures(baseModel, payload) preparedPayload, _, errReplay := prepareAntigravityGeminiReasoningReplayPayload(ctx, baseModel, req, opts, payload) if errReplay != nil { diff --git a/internal/runtime/executor/helps/cloak_obfuscate.go b/internal/runtime/executor/helps/cloak_obfuscate.go index dce724af8..b35780374 100644 --- a/internal/runtime/executor/helps/cloak_obfuscate.go +++ b/internal/runtime/executor/helps/cloak_obfuscate.go @@ -97,6 +97,44 @@ func ObfuscateSensitiveWords(payload []byte, matcher *SensitiveWordMatcher) []by return payload } +// ObfuscateSensitiveWordsInSystemInstruction obfuscates sensitive words in an Antigravity system instruction. +func ObfuscateSensitiveWordsInSystemInstruction(payload []byte, matcher *SensitiveWordMatcher) []byte { + if matcher == nil || matcher.regex == nil { + return payload + } + + for _, path := range []string{"request.systemInstruction", "request.system_instruction"} { + instruction := gjson.GetBytes(payload, path) + if !instruction.Exists() { + continue + } + if instruction.Type == gjson.String { + text := instruction.String() + if obfuscated := matcher.obfuscateText(text); obfuscated != text { + payload, _ = sjson.SetBytes(payload, path, obfuscated) + } + continue + } + + parts := instruction.Get("parts") + if !parts.IsArray() { + continue + } + parts.ForEach(func(key, part gjson.Result) bool { + if part.Get("text").Type != gjson.String { + return true + } + text := part.Get("text").String() + if obfuscated := matcher.obfuscateText(text); obfuscated != text { + payload, _ = sjson.SetBytes(payload, path+".parts."+key.String()+".text", obfuscated) + } + return true + }) + } + + return payload +} + // obfuscateSystemBlocks obfuscates sensitive words in system blocks. func obfuscateSystemBlocks(payload []byte, matcher *SensitiveWordMatcher) []byte { system := gjson.GetBytes(payload, "system") diff --git a/internal/watcher/diff/config_diff.go b/internal/watcher/diff/config_diff.go index 4c01a0760..708c0cf15 100644 --- a/internal/watcher/diff/config_diff.go +++ b/internal/watcher/diff/config_diff.go @@ -104,6 +104,9 @@ func BuildConfigChangeDetails(oldCfg, newCfg *config.Config) []string { if oldCfg.QuotaExceeded.AntigravityCredits != newCfg.QuotaExceeded.AntigravityCredits { changes = append(changes, fmt.Sprintf("quota-exceeded.antigravity-credits: %t -> %t", oldCfg.QuotaExceeded.AntigravityCredits, newCfg.QuotaExceeded.AntigravityCredits)) } + if !reflect.DeepEqual(oldCfg.Antigravity.SensitiveWords, newCfg.Antigravity.SensitiveWords) { + changes = append(changes, fmt.Sprintf("antigravity.sensitive-words: %d -> %d", len(oldCfg.Antigravity.SensitiveWords), len(newCfg.Antigravity.SensitiveWords))) + } if oldCfg.Codex.IdentityConfuse != newCfg.Codex.IdentityConfuse { changes = append(changes, fmt.Sprintf("codex.identity-confuse: %t -> %t", oldCfg.Codex.IdentityConfuse, newCfg.Codex.IdentityConfuse)) diff --git a/internal/watcher/diff/config_diff_test.go b/internal/watcher/diff/config_diff_test.go index f13a36584..723de3e5c 100644 --- a/internal/watcher/diff/config_diff_test.go +++ b/internal/watcher/diff/config_diff_test.go @@ -337,6 +337,7 @@ func TestBuildConfigChangeDetails_FlagsAndKeys(t *testing.T) { MaxRetryInterval: 1, WebsocketAuth: false, QuotaExceeded: config.QuotaExceeded{SwitchProject: false, SwitchPreviewModel: false, AntigravityCredits: false}, + Antigravity: config.AntigravityConfig{SensitiveWords: []string{"old-word"}}, ClaudeKey: []config.ClaudeKey{{APIKey: "c1"}}, CodexKey: []config.CodexKey{{APIKey: "x1"}}, RemoteManagement: config.RemoteManagement{DisableControlPanel: false, PanelGitHubRepository: "old/repo", SecretKey: "keep"}, @@ -362,6 +363,7 @@ func TestBuildConfigChangeDetails_FlagsAndKeys(t *testing.T) { MaxRetryInterval: 3, WebsocketAuth: true, QuotaExceeded: config.QuotaExceeded{SwitchProject: true, SwitchPreviewModel: true, AntigravityCredits: true}, + Antigravity: config.AntigravityConfig{SensitiveWords: []string{"new-word-1", "new-word-2"}}, XAI: config.XAIConfig{InjectXSearch: true}, ClaudeKey: []config.ClaudeKey{ {APIKey: "c1", BaseURL: "http://new", ProxyURL: "http://p", Headers: map[string]string{"H": "1"}, ExcludedModels: []string{"a"}}, @@ -410,6 +412,7 @@ func TestBuildConfigChangeDetails_FlagsAndKeys(t *testing.T) { expectContains(t, details, "quota-exceeded.switch-project: false -> true") expectContains(t, details, "quota-exceeded.switch-preview-model: false -> true") expectContains(t, details, "quota-exceeded.antigravity-credits: false -> true") + expectContains(t, details, "antigravity.sensitive-words: 1 -> 2") expectContains(t, details, "xai.inject-x-search: false -> true") expectContains(t, details, "api-keys count: 1 -> 2") expectContains(t, details, "claude-api-key count: 1 -> 2")