feat(antigravity): obfuscate sensitive words in system instructions

Closes: #4696 #4723 #4732
This commit is contained in:
Luis Pater
2026-08-05 00:27:32 +08:00
parent 32f1069a18
commit 42eef103d6
12 changed files with 151 additions and 0 deletions

View File

@@ -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.

View File

@@ -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()

View File

@@ -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"`

View File

@@ -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"`

View File

@@ -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.

View File

@@ -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())

View File

@@ -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})

View File

@@ -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())

View File

@@ -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 {

View File

@@ -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")

View File

@@ -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))

View File

@@ -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")