diff --git a/agent-schema.json b/agent-schema.json index e70a17de6..6b3a67e12 100644 --- a/agent-schema.json +++ b/agent-schema.json @@ -564,7 +564,8 @@ }, "redact_secrets": { "type": "boolean", - "description": "When true, the runtime auto-installs the redact_secrets builtin on all three of pre_tool_use (scrubs detected secrets from tool arguments), before_llm_call (scrubs the messages sent to the LLM), and tool_response_transform (scrubs tool output before it reaches event consumers, the persisted session, the post_tool_use hook input, or the next LLM call). The same hook entries can be authored directly in YAML for finer-grained control \u2014 see the hooks.tool_response_transform / hooks.before_llm_call / hooks.pre_tool_use sections. Detection uses the portcullis ruleset (GitHub PATs, AWS keys, Stripe / Slack / GitLab tokens, JWTs, private keys, etc.). Each detected span is replaced with the literal '[REDACTED]'." + "default": true, + "description": "Enabled by default. When true (the default), the runtime auto-installs the redact_secrets builtin on all three of pre_tool_use (scrubs detected secrets from tool arguments), before_llm_call (scrubs the messages sent to the LLM), and tool_response_transform (scrubs tool output before it reaches event consumers, the persisted session, the post_tool_use hook input, or the next LLM call). Set to false to opt out. The same hook entries can be authored directly in YAML for finer-grained control \u2014 see the hooks.tool_response_transform / hooks.before_llm_call / hooks.pre_tool_use sections. Detection uses the portcullis ruleset (GitHub PATs, AWS keys, Stripe / Slack / GitLab tokens, JWTs, private keys, etc.). Each detected span is replaced with the literal '[REDACTED]'." }, "max_iterations": { "type": "integer", diff --git a/examples/redact_secrets.yaml b/examples/redact_secrets.yaml index d44c6c65c..4d7e3020c 100644 --- a/examples/redact_secrets.yaml +++ b/examples/redact_secrets.yaml @@ -1,8 +1,12 @@ # Demonstrates the redact_secrets feature. # -# `redact_secrets: true` is a single agent-level switch that wires up -# all THREE defenses against accidentally leaking credentials, tokens, -# or private keys: +# `redact_secrets` is enabled by default for every agent. This +# example sets it explicitly for documentation purposes; you would +# normally omit the field entirely. To opt out, set +# `redact_secrets: false`. +# +# The single agent-level switch wires up all THREE defenses against +# accidentally leaking credentials, tokens, or private keys: # # 1. A pre_tool_use builtin hook that scrubs detected secrets from # the arguments of every tool call, before the tool sees them. @@ -37,6 +41,7 @@ agents: instruction: | You are a helpful assistant. If the user accidentally pastes a token, do your best work without echoing the secret back. + # On by default; shown explicitly here. Set to `false` to opt out. redact_secrets: true toolsets: - type: shell diff --git a/examples/redact_secrets_hooks.yaml b/examples/redact_secrets_hooks.yaml index 3a3b2f0cf..abf1f0b01 100644 --- a/examples/redact_secrets_hooks.yaml +++ b/examples/redact_secrets_hooks.yaml @@ -1,11 +1,10 @@ # Demonstrates wiring the redact_secrets builtin from YAML hooks -# directly, instead of through the agent-level `redact_secrets: true` -# shorthand. +# directly, instead of through the agent-level `redact_secrets` flag +# (which is enabled by default and auto-injects the same entries). # -# `redact_secrets: true` is the one-liner equivalent of the entries -# below — it auto-injects the same three hook entries when the runtime -# constructs the agent. Spelling them out by hand is useful when you -# want to: +# Auto-injection is idempotent against manually-written entries that +# name the same builtin, so spelling them out by hand is safe and is +# useful when you want to: # # * scope the rewrite to a subset of tools (set `matcher:` to a # regex instead of `*`), diff --git a/pkg/config/latest/redact_secrets_test.go b/pkg/config/latest/redact_secrets_test.go new file mode 100644 index 000000000..fd5f1c38b --- /dev/null +++ b/pkg/config/latest/redact_secrets_test.go @@ -0,0 +1,66 @@ +package latest + +import ( + "testing" + + "github.com/goccy/go-yaml" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestRedactSecretsEnabledDefault(t *testing.T) { + t.Parallel() + + tru, fls := true, false + + tests := []struct { + name string + cfg *AgentConfig + want bool + }{ + {name: "nil receiver defaults to on", cfg: nil, want: true}, + {name: "field omitted defaults to on", cfg: &AgentConfig{}, want: true}, + {name: "explicit true is on", cfg: &AgentConfig{RedactSecrets: &tru}, want: true}, + {name: "explicit false opts out", cfg: &AgentConfig{RedactSecrets: &fls}, want: false}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + assert.Equal(t, tt.want, tt.cfg.RedactSecretsEnabled()) + }) + } +} + +func TestRedactSecretsYAMLRoundTrip(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + yaml string + wantSet bool // whether the pointer should be non-nil + wantVal bool // value to expect when set + want bool // expected effective value via RedactSecretsEnabled + }{ + {name: "omitted", yaml: "model: openai/gpt-5", wantSet: false, want: true}, + {name: "explicit true", yaml: "model: openai/gpt-5\nredact_secrets: true", wantSet: true, wantVal: true, want: true}, + {name: "explicit false", yaml: "model: openai/gpt-5\nredact_secrets: false", wantSet: true, wantVal: false, want: false}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + var cfg AgentConfig + require.NoError(t, yaml.Unmarshal([]byte(tt.yaml), &cfg)) + + if tt.wantSet { + require.NotNil(t, cfg.RedactSecrets, "field should be set") + assert.Equal(t, tt.wantVal, *cfg.RedactSecrets) + } else { + assert.Nil(t, cfg.RedactSecrets, "field should be nil when omitted") + } + assert.Equal(t, tt.want, cfg.RedactSecretsEnabled()) + }) + } +} diff --git a/pkg/config/latest/types.go b/pkg/config/latest/types.go index 104f0caa4..da4d7b0dc 100644 --- a/pkg/config/latest/types.go +++ b/pkg/config/latest/types.go @@ -409,7 +409,11 @@ type AgentConfig struct { // hook entries by hand — the runtime auto-injects them when this // flag is true. See pkg/hooks/builtins/redact_secrets.go for the // hook-side implementation. - RedactSecrets bool `json:"redact_secrets,omitempty"` + // + // Pointer (tri-state) so we can distinguish "unset" (nil → default + // on) from "explicitly disabled" (false). Use + // [AgentConfig.RedactSecretsEnabled] to read the effective value. + RedactSecrets *bool `json:"redact_secrets,omitempty"` CodeModeTools bool `json:"code_mode_tools,omitempty"` AddDescriptionParameter bool `json:"add_description_parameter,omitempty"` MaxIterations int `json:"max_iterations,omitempty"` @@ -600,6 +604,17 @@ func (a *AgentConfig) GetFallbackModels() []string { return nil } +// RedactSecretsEnabled reports the effective value of the agent's +// redact_secrets flag. The feature is on by default: a nil pointer +// (the field omitted from YAML) means enabled, an explicit +// `redact_secrets: false` is the only way to disable it. +func (a *AgentConfig) RedactSecretsEnabled() bool { + if a == nil || a.RedactSecrets == nil { + return true + } + return *a.RedactSecrets +} + // GetFallbackRetries returns the fallback retries from the config. func (a *AgentConfig) GetFallbackRetries() int { if a.Fallback != nil { diff --git a/pkg/teamloader/teamloader.go b/pkg/teamloader/teamloader.go index a7fd35a03..64ad21623 100644 --- a/pkg/teamloader/teamloader.go +++ b/pkg/teamloader/teamloader.go @@ -164,7 +164,7 @@ func LoadWithConfig(ctx context.Context, agentSource config.Source, runConfig *c agent.WithAddDate(agentConfig.AddDate), agent.WithAddEnvironmentInfo(agentConfig.AddEnvironmentInfo), agent.WithAddDescriptionParameter(agentConfig.AddDescriptionParameter), - agent.WithRedactSecrets(agentConfig.RedactSecrets), + agent.WithRedactSecrets(agentConfig.RedactSecretsEnabled()), agent.WithAddPromptFiles(promptFiles), agent.WithMaxIterations(agentConfig.MaxIterations), agent.WithMaxConsecutiveToolCalls(agentConfig.MaxConsecutiveToolCalls),