Skip to content

Commit bfe9bda

Browse files
authored
fix: correct engine.env secrets warning — excluded from sandbox, not leaked (#43302)
1 parent a67a29f commit bfe9bda

4 files changed

Lines changed: 192 additions & 4 deletions

File tree

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
# ADR-43302: Section-Aware Secret Validation Messages in Workflow Compiler
2+
3+
**Date**: 2026-07-04
4+
**Status**: Draft
5+
**Deciders**: Unknown
6+
7+
---
8+
9+
### Context
10+
11+
The workflow compiler's `validateEnvSecretsSection` function validates secrets across multiple
12+
env sections (`env`, `engine.env`) and emits warnings or errors when `${{ secrets.* }}`
13+
references are detected. Historically it used a single generic message — "will be leaked to the
14+
agent container" — regardless of which section contained the secret reference.
15+
16+
However, `engine.env` secrets are handled differently from top-level `env` secrets: the
17+
`ComputeAWFExcludeEnvVarNames` function auto-detects any `engine.env` value that contains a
18+
`${{ secrets.* }}` reference and adds the corresponding variable name to AWF's `--exclude-env`
19+
list. This means `engine.env` secrets never reach the agent sandbox process, making the
20+
"will be leaked" warning factually incorrect for that section and a source of user confusion.
21+
22+
### Decision
23+
24+
We will differentiate the validation messages emitted by `validateEnvSecretsSection` based on
25+
which section is being validated, rather than using a single generic message for all sections.
26+
For `engine.env`, messages will accurately reflect that secrets are auto-excluded from the
27+
agent sandbox via AWF `--exclude-env`; for all other sections (e.g., top-level `env`), the
28+
existing "will be leaked" language is retained because those secrets genuinely reach the
29+
agent container. Strict mode continues to treat `engine.env` secrets as an error (encouraging
30+
engine-specific secret configuration), but with accurate, non-misleading error text.
31+
32+
### Alternatives Considered
33+
34+
#### Alternative 1: Remove the `engine.env` warning entirely
35+
36+
Since `engine.env` secrets are auto-excluded and never reach the agent process, one option is
37+
to suppress the warning entirely for that section — no message, no error. This eliminates the
38+
false positive and simplifies the code path. It was rejected because users should still be
39+
nudged toward engine-specific secret configuration (the more secure, explicit pattern), and
40+
removing all feedback would silently permit a configuration style that the platform discourages.
41+
42+
#### Alternative 2: Extract a separate validation function for `engine.env`
43+
44+
Rather than branching inside `validateEnvSecretsSection`, a dedicated
45+
`validateEngineEnvSecretsSection` function could own all `engine.env` secret logic. This would
46+
keep each function single-purpose and avoid section-name string comparisons. It was rejected
47+
for this fix because it would duplicate the secret-detection regex and collection logic that
48+
lives in `validateEnvSecretsSection`, increasing maintenance surface for a change whose primary
49+
goal is correcting a misleading string — not restructuring validation logic.
50+
51+
### Consequences
52+
53+
#### Positive
54+
- Users see accurate feedback: `engine.env` secret warnings now correctly describe sandbox
55+
exclusion rather than falsely claiming leakage to the agent container.
56+
- Strict mode error messages for `engine.env` remain actionable and no longer contradict the
57+
platform's actual security behavior, reducing support burden and user confusion.
58+
59+
#### Negative
60+
- `validateEnvSecretsSection` now contains section-specific branching (`if sectionName == "engine.env"`),
61+
slightly increasing cyclomatic complexity and making the function less generic.
62+
- Any future env section added to the validation path must be evaluated for whether it also
63+
requires a custom message, creating an implicit maintenance contract.
64+
65+
#### Neutral
66+
- Test assertions for `engine.env` strict-mode cases were updated to match the new message
67+
text; this is a mechanical change with no behavioral impact on test coverage.
68+
- The `awf_helpers_test.go` additions verify the existing `ComputeAWFExcludeEnvVarNames`
69+
behavior that this fix depends on for correctness, improving confidence in the invariant.
70+
71+
---
72+
73+
*ADR created by [adr-writer agent]. Review and finalize before changing status from Draft to Accepted.*

pkg/workflow/awf_helpers_test.go

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1118,6 +1118,104 @@ func TestAWFSupportsExcludeEnv(t *testing.T) {
11181118
}
11191119
}
11201120

1121+
// TestComputeAWFExcludeEnvVarNames verifies that engine.env vars whose values contain
1122+
// ${{ secrets.* }} are automatically included in the --exclude-env list, and that
1123+
// non-secret engine.env vars and plain-value core secrets are handled correctly.
1124+
func TestComputeAWFExcludeEnvVarNames(t *testing.T) {
1125+
tests := []struct {
1126+
name string
1127+
workflowData *WorkflowData
1128+
coreSecretVarNames []string
1129+
want []string
1130+
notWant []string
1131+
}{
1132+
{
1133+
name: "engine.env secret var is auto-excluded",
1134+
workflowData: &WorkflowData{
1135+
EngineConfig: &EngineConfig{
1136+
Env: map[string]string{
1137+
"GOOGLE_API_KEY": "${{ secrets.SOME_KEY }}",
1138+
},
1139+
},
1140+
},
1141+
coreSecretVarNames: []string{},
1142+
want: []string{"GOOGLE_API_KEY"},
1143+
},
1144+
{
1145+
name: "engine.env non-secret var is not excluded",
1146+
workflowData: &WorkflowData{
1147+
EngineConfig: &EngineConfig{
1148+
Env: map[string]string{
1149+
"DEBUG": "true",
1150+
"LOG_LEVEL": "info",
1151+
},
1152+
},
1153+
},
1154+
coreSecretVarNames: []string{},
1155+
want: []string{},
1156+
notWant: []string{"DEBUG", "LOG_LEVEL"},
1157+
},
1158+
{
1159+
name: "engine.env mixes secret and non-secret vars: only secrets excluded",
1160+
workflowData: &WorkflowData{
1161+
EngineConfig: &EngineConfig{
1162+
Env: map[string]string{
1163+
"GOOGLE_API_KEY": "${{ secrets.SOME_KEY }}",
1164+
"LOG_LEVEL": "debug",
1165+
},
1166+
},
1167+
},
1168+
coreSecretVarNames: []string{},
1169+
want: []string{"GOOGLE_API_KEY"},
1170+
notWant: []string{"LOG_LEVEL"},
1171+
},
1172+
{
1173+
name: "engine.env secret combined with core secret vars",
1174+
workflowData: &WorkflowData{
1175+
EngineConfig: &EngineConfig{
1176+
Env: map[string]string{
1177+
"CUSTOM_API_KEY": "${{ secrets.CUSTOM_KEY }}",
1178+
},
1179+
},
1180+
},
1181+
coreSecretVarNames: []string{"GEMINI_API_KEY"},
1182+
want: []string{"GEMINI_API_KEY", "CUSTOM_API_KEY"},
1183+
},
1184+
{
1185+
name: "engine.env secret embedded in a larger string is excluded",
1186+
workflowData: &WorkflowData{
1187+
EngineConfig: &EngineConfig{
1188+
Env: map[string]string{
1189+
"AUTH_HEADER": "Bearer ${{ secrets.TOKEN }}",
1190+
},
1191+
},
1192+
},
1193+
coreSecretVarNames: []string{},
1194+
want: []string{"AUTH_HEADER"},
1195+
},
1196+
{
1197+
name: "nil engine config produces no exclusions beyond core secrets",
1198+
workflowData: &WorkflowData{
1199+
EngineConfig: nil,
1200+
},
1201+
coreSecretVarNames: []string{"COPILOT_GITHUB_TOKEN"},
1202+
want: []string{"COPILOT_GITHUB_TOKEN"},
1203+
},
1204+
}
1205+
1206+
for _, tt := range tests {
1207+
t.Run(tt.name, func(t *testing.T) {
1208+
got := ComputeAWFExcludeEnvVarNames(tt.workflowData, tt.coreSecretVarNames)
1209+
for _, name := range tt.want {
1210+
assert.Contains(t, got, name, "expected %q in exclude list", name)
1211+
}
1212+
for _, name := range tt.notWant {
1213+
assert.NotContains(t, got, name, "expected %q to be absent from exclude list", name)
1214+
}
1215+
})
1216+
}
1217+
}
1218+
11211219
// TestBuildAWFArgsCliProxy tests that BuildAWFArgs correctly injects --difc-proxy-host
11221220
// and --difc-proxy-ca-cert based on the cli-proxy feature flag.
11231221
func TestBuildAWFArgsCliProxy(t *testing.T) {

pkg/workflow/env_secrets_validation_test.go

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@
33
package workflow
44

55
import (
6+
"fmt"
67
"strings"
78
"testing"
89

10+
"github.com/github/gh-aw/pkg/constants"
911
"github.com/stretchr/testify/assert"
1012
"github.com/stretchr/testify/require"
1113
)
@@ -537,7 +539,7 @@ func TestValidateEngineEnvSecrets(t *testing.T) {
537539
},
538540
strictMode: true,
539541
expectError: true,
540-
errorMsg: "strict mode: secrets detected in 'engine.env' section will be leaked to the agent container. Found: ${{ secrets.API_KEY }}",
542+
errorMsg: fmt.Sprintf("are excluded from the agent sandbox via awf --exclude-env (requires AWF %s+) and are not accessible to the agent when that version is in use. Found: ${{ secrets.API_KEY }}", constants.AWFExcludeEnvMinVersion),
541543
},
542544
{
543545
name: "engine.env with multiple secrets in strict mode fails",
@@ -553,7 +555,7 @@ func TestValidateEngineEnvSecrets(t *testing.T) {
553555
},
554556
strictMode: true,
555557
expectError: true,
556-
errorMsg: "strict mode: secrets detected in 'engine.env' section will be leaked to the agent container",
558+
errorMsg: fmt.Sprintf("are excluded from the agent sandbox via awf --exclude-env (requires AWF %s+)", constants.AWFExcludeEnvMinVersion),
557559
},
558560
{
559561
name: "engine.env with secret embedded in string in strict mode fails",
@@ -568,7 +570,7 @@ func TestValidateEngineEnvSecrets(t *testing.T) {
568570
},
569571
strictMode: true,
570572
expectError: true,
571-
errorMsg: "strict mode: secrets detected in 'engine.env' section will be leaked to the agent container. Found: ${{ secrets.TOKEN }}",
573+
errorMsg: fmt.Sprintf("are excluded from the agent sandbox via awf --exclude-env (requires AWF %s+) and are not accessible to the agent when that version is in use. Found: ${{ secrets.TOKEN }}", constants.AWFExcludeEnvMinVersion),
572574
},
573575
{
574576
name: "engine.env with secret in non-strict mode emits warning (no error)",

pkg/workflow/strict_mode_env_validation.go

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"strings"
1212

1313
"github.com/github/gh-aw/pkg/console"
14+
"github.com/github/gh-aw/pkg/constants"
1415
"github.com/github/gh-aw/pkg/setutil"
1516
)
1617

@@ -132,11 +133,25 @@ func (c *Compiler) validateEnvSecretsSection(config map[string]any, sectionName
132133

133134
// In strict mode, this is an error
134135
if c.strictMode {
136+
if sectionName == "engine.env" {
137+
// engine.env secrets are excluded from the agent sandbox via awf --exclude-env
138+
// (requires AWF v0.25.3+), so they are not leaked, but strict mode still requires
139+
// engine-specific configuration.
140+
return fmt.Errorf("strict mode: secrets detected in 'engine.env' section are excluded from the agent sandbox via awf --exclude-env (requires AWF %s+) and are not accessible to the agent when that version is in use. Found: %s. Use engine-specific secret configuration instead. See: https://github.github.com/gh-aw/reference/engines/", constants.AWFExcludeEnvMinVersion, strings.Join(secretRefs, ", "))
141+
}
135142
return fmt.Errorf("strict mode: secrets detected in '%s' section will be leaked to the agent container. Found: %s. Use engine-specific secret configuration instead. See: https://github.github.com/gh-aw/reference/engines/", sectionName, strings.Join(secretRefs, ", "))
136143
}
137144

138145
// In non-strict mode, emit a warning
139-
warningMsg := fmt.Sprintf("Warning: secrets detected in '%s' section will be leaked to the agent container. Found: %s. Consider using engine-specific secret configuration instead.", sectionName, strings.Join(secretRefs, ", "))
146+
var warningMsg string
147+
if sectionName == "engine.env" {
148+
// engine.env secrets are excluded from the agent sandbox via awf --exclude-env
149+
// (requires AWF v0.25.3+). On older AWF versions this protection is not applied and
150+
// the values will reach the agent container.
151+
warningMsg = fmt.Sprintf("Warning: secrets detected in 'engine.env' section will be excluded from the agent sandbox via awf --exclude-env (requires AWF %s+); on older AWF versions the agent process will see these values. Found: %s. Consider using engine-specific secret configuration instead.", constants.AWFExcludeEnvMinVersion, strings.Join(secretRefs, ", "))
152+
} else {
153+
warningMsg = fmt.Sprintf("Warning: secrets detected in '%s' section will be leaked to the agent container. Found: %s. Consider using engine-specific secret configuration instead.", sectionName, strings.Join(secretRefs, ", "))
154+
}
140155
fmt.Fprintln(os.Stderr, console.FormatWarningMessage(warningMsg))
141156
c.IncrementWarningCount()
142157

0 commit comments

Comments
 (0)