Skip to content

Commit 2d177db

Browse files
authored
Enforce sandbox-disable justification strings and surface new AWF import/safe-output constraints (#38228)
1 parent 106f4a7 commit 2d177db

17 files changed

Lines changed: 396 additions & 26 deletions

.github/aw/create-agentic-workflow.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,7 @@ Rules:
148148
- always restrict `create-pull-request.allowed-files`
149149
- prefer the dedicated safe output instead of shelling out to `gh` for the same mutation
150150
- include `noop` guidance in the prompt so successful no-op runs are explicit
151+
- when using `create-issue`, instruct the agent to provide a meaningful body (20-65000 characters; avoid placeholder-only text)
151152

152153
### 7. Decide who can trigger the workflow
153154

.github/aw/experiments.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -292,5 +292,5 @@ experiments:
292292
- ❌ **Interpreting early results** (<~20 runs/variant) — chance variation dominates.
293293
- ❌ **Experiments as feature flags** — use `features:` for deterministic switches.
294294
- ❌ **Engine experiments in one file** — `engine:` cannot switch mid-run; use two parallel files.
295-
- ❌ **Nesting `{{#if experiments.<name> }}` inside `{{#runtime-import? }}`** — evaluation order not guaranteed across import boundaries.
295+
- ❌ **Nesting `{{#if experiments.<name> }}` inside `{{#runtime-import? }}`** — evaluation order is brittle across import boundaries. Prefer explicit branching in the main workflow prompt or separate workflow files per variant.
296296
- ❌ **Writing the internal env-var form** `__GH_AW_EXPERIMENTS__*` — implementation detail, may change.

.github/aw/safe-outputs-content.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@ description: Safe-output reference for issue, discussion, comment, and pull requ
2626
allowed-repos: [owner/other] # Optional: additional repos agent can target (agent uses `repo` field in output)
2727
```
2828
29+
`create_issue` output validation requires:
30+
- `body` minimum length: **20** characters
31+
- `body` maximum length: **65000** characters
32+
2933
**Auto-Expiration**: The `expires` field auto-closes issues after a time period. Supports integers (days) or relative formats (2h, 7d, 2w, 1m, 1y). Generates `agentics-maintenance.yml` workflow that runs at minimum required frequency based on shortest expiration time: 1 day or less → every 2 hours, 2 days → every 6 hours, 3-4 days → every 12 hours, 5+ days → daily.
3034
**Deduplication for Scheduled Workflows**: When a `schedule:` trigger is combined with `create-issue`, use `skip-if-match:` in the `on:` block to prevent opening a duplicate issue on every run. Pair with `expires:` so stale issues are cleaned up automatically:
3135

.github/aw/syntax-agentic.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ description: Agentic workflow specific frontmatter fields for GitHub Agentic Wor
6262
- `cli-proxy: true` - Enable AWF CLI proxy sidecar for secure read-only `gh` CLI access without exposing `GITHUB_TOKEN` (requires AWF v0.26.0+). Prerequisite for `integrity-reactions`; the compiler enables it automatically when `integrity-reactions: true` is set.
6363
- `integrity-reactions: true` - Enable reaction-based integrity promotion/demotion. Maintainers can use 👍/❤️ reactions to promote content to `approved` and 👎/😕 to demote it to `none`. Compiler automatically enables `cli-proxy`. Requires `tools.github.min-integrity` to be set and MCPG >= v0.2.18. Defaults: endorsement reactions THUMBS_UP/HEART, disapproval reactions THUMBS_DOWN/CONFUSED, endorser-min-integrity: approved, disapproval-integrity: none.
6464
- `mcp-cli: true` - Deprecated. This flag has been removed; MCP CLI mounting is now always enabled when `tools.cli-proxy: true` is set.
65+
- `dangerously-disable-sandbox-agent: "<justification>"` - Required when `sandbox.agent: false` is set. Must be a plain string justification (minimum 20 characters; expressions are not allowed) that explains why disabling the sandbox is safe for this workflow.
6566

6667
- **`experiments:`** - A/B testing experiments for balanced variant selection (object)
6768
- Maps experiment names to variant lists (bare array) or full config objects
@@ -343,9 +344,11 @@ description: Agentic workflow specific frontmatter fields for GitHub Agentic Wor
343344
model-fallback: false # Optional: disable model fallback (default true); set false for BYOK Azure OpenAI to prevent deployment-name rewriting
344345
```
345346

346-
- To disable the agent firewall while keeping MCP gateway enabled (not allowed in strict mode):
347+
- To disable the agent firewall while keeping MCP gateway enabled, you must provide the dangerous-disable justification feature:
347348

348349
```yaml
350+
features:
351+
dangerously-disable-sandbox-agent: "controlled environment with no internet access"
349352
sandbox:
350353
agent: false
351354
```

.github/aw/upgrade-agentic-workflows.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,13 @@ Before attempting to compile, apply automatic codemods:
102102

103103
This will automatically update workflow files with changes like:
104104
- Replacing 'timeout_minutes' with 'timeout-minutes'
105-
- Replacing 'network.firewall' with 'sandbox.agent: false'
105+
- Replacing `network.firewall: false` with:
106+
```yaml
107+
features:
108+
dangerously-disable-sandbox-agent: "controlled environment with no internet access"
109+
sandbox:
110+
agent: false
111+
```
106112
- Removing deprecated 'mcp-scripts.mode' field
107113
108114
2. **Review the Changes**

docs/src/content/docs/reference/safe-outputs.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,8 @@ See [Cross-Repository Operations](/gh-aw/reference/cross-repository/) for compre
129129

130130
#### `create_issue` tool field schema (`fields`)
131131

132+
`create_issue.body` must be between **20** and **65000** characters.
133+
132134
| Parameter | Type | Required | Description | Example |
133135
|-----------|------|----------|-------------|---------|
134136
| `fields` | `array<object>` | No | Optional issue field updates to apply immediately after issue creation. | `[{"name":"Priority","value":"P1"}]` |

pkg/cli/codemod_network_firewall.go

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package cli
22

33
import (
4+
"fmt"
45
"strconv"
56
"strings"
67

@@ -9,6 +10,8 @@ import (
910

1011
var networkFirewallCodemodLog = logger.New("cli:codemod_network_firewall")
1112

13+
const migratedSandboxDisableJustification = "migrated from deprecated network.firewall disable setting"
14+
1215
// getNetworkFirewallCodemod creates a codemod for migrating network.firewall to sandbox.agent
1316
func getNetworkFirewallCodemod() Codemod {
1417
return newFieldRemovalCodemod(fieldRemovalCodemodConfig{
@@ -21,26 +24,44 @@ func getNetworkFirewallCodemod() Codemod {
2124
LogMsg: "Applied network.firewall migration (firewall now always enabled via sandbox.agent: awf default)",
2225
Log: networkFirewallCodemodLog,
2326
PostTransform: func(lines []string, frontmatter map[string]any, fieldValue any) []string {
27+
requiresDisableFlag := requiresSandboxDisableFeatureFlag(fieldValue)
2428
_, hasSandbox := frontmatter["sandbox"]
2529

2630
if !hasSandbox {
2731
sandboxLines := sandboxAgentLinesFromFirewall(fieldValue)
2832
if len(sandboxLines) > 0 {
2933
lines = insertSandboxAfterNetworkBlock(lines, sandboxLines)
34+
if requiresDisableFlag {
35+
lines = ensureSandboxDisableFeatureFlag(lines)
36+
}
3037
networkFirewallCodemodLog.Print("Converted deprecated network.firewall to sandbox.agent")
3138
}
3239
return lines
3340
}
3441

3542
lines, merged := mergeFirewallIntoExistingSandbox(lines, fieldValue)
3643
if merged {
44+
if requiresDisableFlag {
45+
lines = ensureSandboxDisableFeatureFlag(lines)
46+
}
3747
networkFirewallCodemodLog.Print("Merged deprecated network.firewall into existing sandbox.agent")
3848
}
3949
return lines
4050
},
4151
})
4252
}
4353

54+
func requiresSandboxDisableFeatureFlag(fieldValue any) bool {
55+
switch value := fieldValue.(type) {
56+
case bool:
57+
return !value
58+
case string:
59+
return strings.EqualFold(strings.TrimSpace(value), "disable")
60+
default:
61+
return false
62+
}
63+
}
64+
4465
func sandboxAgentLinesFromFirewall(fieldValue any) []string {
4566
switch value := fieldValue.(type) {
4667
case bool:
@@ -260,3 +281,59 @@ func indentLines(lines []string, indent string) []string {
260281
}
261282
return indented
262283
}
284+
285+
func ensureSandboxDisableFeatureFlag(lines []string) []string {
286+
featureKey := "dangerously-disable-sandbox-agent:"
287+
288+
featuresIdx := -1
289+
featuresIndent := ""
290+
featuresEnd := len(lines)
291+
for i, line := range lines {
292+
trimmed := strings.TrimSpace(line)
293+
if isTopLevelKey(line) && strings.HasPrefix(trimmed, "features:") {
294+
featuresIdx = i
295+
featuresIndent = getIndentation(line)
296+
for j := i + 1; j < len(lines); j++ {
297+
if isTopLevelKey(lines[j]) {
298+
featuresEnd = j
299+
break
300+
}
301+
}
302+
break
303+
}
304+
}
305+
306+
if featuresIdx >= 0 {
307+
featureIndent := featuresIndent + " "
308+
for i := featuresIdx + 1; i < featuresEnd; i++ {
309+
if strings.HasPrefix(strings.TrimSpace(lines[i]), featureKey) {
310+
return lines
311+
}
312+
}
313+
featureLine := fmt.Sprintf("%s%s %s", featureIndent, featureKey, strconv.Quote(migratedSandboxDisableJustification))
314+
newLines := make([]string, 0, len(lines)+1)
315+
newLines = append(newLines, lines[:featuresEnd]...)
316+
newLines = append(newLines, featureLine)
317+
newLines = append(newLines, lines[featuresEnd:]...)
318+
return newLines
319+
}
320+
321+
insertIndex := len(lines)
322+
for i, line := range lines {
323+
if isTopLevelKey(line) && strings.HasPrefix(strings.TrimSpace(line), "sandbox:") {
324+
insertIndex = i
325+
break
326+
}
327+
}
328+
329+
featureLines := []string{
330+
"features:",
331+
" " + featureKey + " " + strconv.Quote(migratedSandboxDisableJustification),
332+
}
333+
334+
newLines := make([]string, 0, len(lines)+len(featureLines))
335+
newLines = append(newLines, lines[:insertIndex]...)
336+
newLines = append(newLines, featureLines...)
337+
newLines = append(newLines, lines[insertIndex:]...)
338+
return newLines
339+
}

pkg/cli/codemod_network_firewall_test.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ permissions:
8282
assert.NotContains(t, result, "firewall:", "Should remove firewall field")
8383
assert.Contains(t, result, "sandbox:", "Should add sandbox block")
8484
assert.Contains(t, result, "agent: false", "Should convert firewall false to sandbox.agent: false")
85+
assert.Contains(t, result, `dangerously-disable-sandbox-agent: "migrated from deprecated network.firewall disable setting"`, "Should add required sandbox-disable justification feature")
8586
}
8687

8788
func TestNetworkFirewallCodemod_NoNetworkField(t *testing.T) {
@@ -208,6 +209,43 @@ sandbox:
208209
assert.Contains(t, result, "sandbox:", "Should preserve existing sandbox block")
209210
assert.Contains(t, result, "mcp: true", "Should preserve existing sandbox settings")
210211
assert.Contains(t, result, "agent: false", "Should migrate firewall false to sandbox.agent: false")
212+
assert.Contains(t, result, `dangerously-disable-sandbox-agent: "migrated from deprecated network.firewall disable setting"`, "Should add required sandbox-disable justification feature")
213+
}
214+
215+
func TestNetworkFirewallCodemod_PreservesExistingSandboxDisableJustification(t *testing.T) {
216+
codemod := getNetworkFirewallCodemod()
217+
218+
content := `---
219+
on: workflow_dispatch
220+
network:
221+
firewall: false
222+
features:
223+
dangerously-disable-sandbox-agent: "already documented justification string with enough detail"
224+
sandbox:
225+
mcp: true
226+
---
227+
228+
# Test Workflow`
229+
230+
frontmatter := map[string]any{
231+
"on": "workflow_dispatch",
232+
"network": map[string]any{
233+
"firewall": false,
234+
},
235+
"features": map[string]any{
236+
"dangerously-disable-sandbox-agent": "already documented justification string with enough detail",
237+
},
238+
"sandbox": map[string]any{
239+
"mcp": true,
240+
},
241+
}
242+
243+
result, applied, err := codemod.Apply(content, frontmatter)
244+
245+
require.NoError(t, err)
246+
assert.True(t, applied)
247+
assert.Contains(t, result, `dangerously-disable-sandbox-agent: "already documented justification string with enough detail"`)
248+
assert.NotContains(t, result, migratedSandboxDisableJustification, "Should not overwrite existing justification")
211249
}
212250

213251
func TestNetworkFirewallCodemod_MigratesFirewallVersionIntoExistingSandbox(t *testing.T) {

pkg/constants/feature_constants.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,6 @@ const (
8484
// Workflow frontmatter usage:
8585
//
8686
// features:
87-
// dangerously-disable-sandbox-agent: true
87+
// dangerously-disable-sandbox-agent: "controlled environment with no internet access"
8888
DangerouslyDisableSandboxAgentFeatureFlag FeatureFlag = "dangerously-disable-sandbox-agent"
8989
)

pkg/workflow/compiler_validators_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -385,7 +385,7 @@ func TestValidateToolConfiguration_EmitsSandboxWarningBeforeThreatDetectionError
385385
workflowData := &WorkflowData{
386386
Name: "Test",
387387
Features: map[string]any{
388-
"dangerously-disable-sandbox-agent": true,
388+
"dangerously-disable-sandbox-agent": "controlled environment with no internet access",
389389
},
390390
SandboxConfig: &SandboxConfig{
391391
Agent: &AgentSandboxConfig{Disabled: true},

0 commit comments

Comments
 (0)