Skip to content

Commit c0a97cb

Browse files
Copilotpelikhangithub-actions[bot]claude
authored
feat(workflow): support sandbox.agent.version and migrate deprecated network.firewall to sandbox.agent (#27626)
* feat(workflow): support sandbox.agent.version for awf resolution Agent-Logs-Url: https://github.com/github/gh-aw/sessions/d204dbf8-0c30-4183-91ab-662ce23a4ebc Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> * fix(workflow): refine sandbox agent awf version override handling Agent-Logs-Url: https://github.com/github/gh-aw/sessions/d204dbf8-0c30-4183-91ab-662ce23a4ebc Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> * chore(workflow): improve sandbox agent version override logging Agent-Logs-Url: https://github.com/github/gh-aw/sessions/d204dbf8-0c30-4183-91ab-662ce23a4ebc Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> * chore(workflow): polish awf version override handling and tests Agent-Logs-Url: https://github.com/github/gh-aw/sessions/d204dbf8-0c30-4183-91ab-662ce23a4ebc Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> * refactor: remove network.firewall frontmatter and migrate via codemod Agent-Logs-Url: https://github.com/github/gh-aw/sessions/b86247d5-111e-4b6e-9574-3730e58aa0fe Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> * chore(cli): tighten firewall version migration normalization Agent-Logs-Url: https://github.com/github/gh-aw/sessions/b86247d5-111e-4b6e-9574-3730e58aa0fe Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> * docs(adr): add draft ADR-27626 for sandbox.agent.version and network.firewall migration Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(codemod): merge firewall disable/version into existing sandbox Agent-Logs-Url: https://github.com/github/gh-aw/sessions/3917e2dd-b6b3-4753-a749-56d561d560fa Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> Co-authored-by: Peli de Halleux <pelikhan@users.noreply.github.com>
1 parent adc5ceb commit c0a97cb

21 files changed

Lines changed: 616 additions & 252 deletions
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
# ADR-27626: Introduce sandbox.agent.version and Remove Deprecated network.firewall Field
2+
3+
**Date**: 2026-04-21
4+
**Status**: Draft
5+
**Deciders**: Unknown (copilot-swe-agent / pelikhan)
6+
7+
---
8+
9+
## Part 1 — Narrative (Human-Friendly)
10+
11+
### Context
12+
13+
The `network.firewall` frontmatter field was previously used to configure the Agent Workflow Firewall (AWF) in agentic workflow definitions. It was deprecated in favor of the unified `sandbox.agent` configuration block, but the migration codemod only handled the `true` case, leaving `false`, `null`, `"disable"`, and object-with-version forms unmigrated. Additionally, there was no mechanism to pin a specific AWF version via frontmatter — users who needed to run a particular AWF release had no stable, documented way to express that constraint. This PR addresses both gaps simultaneously: removing the deprecated field from the schema and expanding the codemod to cover all value variants.
14+
15+
### Decision
16+
17+
We will remove `network.firewall` from the frontmatter schema entirely and add `sandbox.agent.version` as a first-class string field for pinning the AWF version used during installation and runtime. The codemod will be expanded to migrate all `network.firewall` value forms — `true`, `false`, `null`, `"disable"`, and `{version: ...}` objects — to their `sandbox.agent` equivalents. This consolidates AWF configuration under a single `sandbox.agent` surface and ensures the migration path covers every variant that appears in the wild.
18+
19+
### Alternatives Considered
20+
21+
#### Alternative 1: Retain network.firewall as a Deprecated Alias
22+
23+
Keep `network.firewall` in the schema with a deprecation warning, parsing it alongside `sandbox.agent` and merging the values at runtime. This avoids a hard removal and gives teams more migration runway, but perpetuates two competing configuration surfaces indefinitely, increases parser complexity, and makes it harder to reason about precedence when both fields are set.
24+
25+
#### Alternative 2: Introduce a Flat sandbox.awf-version Field
26+
27+
Add a top-level sibling key `sandbox.awf-version` (or similar) rather than nesting the version under `sandbox.agent`. This is marginally more ergonomic to type but diverges from the `sandbox.agent` object model already established, creates a second place where AWF version information lives, and complicates the precedence rules for effective version resolution.
28+
29+
### Consequences
30+
31+
#### Positive
32+
- All `network.firewall` value forms now have a deterministic, tested migration path to `sandbox.agent`.
33+
- Users can pin an explicit AWF version via `sandbox.agent.version`, enabling reproducible builds without relying on the latest release.
34+
- The schema surface is reduced by removing a deprecated field and its associated validation rules.
35+
36+
#### Negative
37+
- Behavioral change: `network.firewall: false` previously produced no `sandbox` block; it now migrates to `sandbox.agent: false`. Workflows relying on the old no-op behavior will see a new block added by the codemod.
38+
- The `normalizeFirewallVersion` helper must handle all numeric YAML types (int8 through uint64, float32/float64) because YAML parsers may unmarshal numeric version values into any of these types, increasing codemod surface area.
39+
40+
#### Neutral
41+
- The codemod expansion requires updating existing tests that asserted `sandbox:` was *not* added for `false` and nested object cases; these expectations were inverted.
42+
- The `aw-info` AWF version reporting now reads `sandbox.agent.version` in addition to the legacy `firewallVersion` field, requiring both paths to be tested.
43+
44+
---
45+
46+
## Part 2 — Normative Specification (RFC 2119)
47+
48+
> The key words **MUST**, **MUST NOT**, **REQUIRED**, **SHALL**, **SHALL NOT**, **SHOULD**, **SHOULD NOT**, **RECOMMENDED**, **MAY**, and **OPTIONAL** in this section are to be interpreted as described in [RFC 2119](https://www.rfc-editor.org/rfc/rfc2119).
49+
50+
### Frontmatter Schema
51+
52+
1. Implementations **MUST NOT** include `network.firewall` in the frontmatter schema as a valid, non-deprecated field.
53+
2. Implementations **MUST** expose `sandbox.agent.version` as an optional string field in the frontmatter schema for specifying an AWF version override.
54+
3. The `sandbox.agent.version` field **MUST** be treated as a string type; numeric-like values written in YAML **MUST** be quoted at generation time to prevent YAML parsers from interpreting them as numbers.
55+
56+
### Codemod Migration
57+
58+
1. The `network.firewall` codemod **MUST** produce a `sandbox.agent` block for every non-absent value of `network.firewall`, including `true`, `false`, `null`, `"disable"`, and object forms.
59+
2. When `network.firewall` is `true` or `null`, the codemod **MUST** emit `sandbox.agent: awf`.
60+
3. When `network.firewall` is `false` or `"disable"`, the codemod **MUST** emit `sandbox.agent: false`.
61+
4. When `network.firewall` is an object containing a `version` key, the codemod **MUST** emit a `sandbox.agent` object block with `id: awf` and `version: "<migrated-value>"`.
62+
5. The codemod **MUST NOT** add a `sandbox` block when one already exists in the frontmatter.
63+
6. Numeric version values encountered during migration **MUST** be normalized to their string representation before being written as `sandbox.agent.version`.
64+
65+
### AWF Version Resolution
66+
67+
1. When `sandbox.agent.version` is set, it **MUST** take precedence over any version derived from the legacy `network.firewall` configuration when resolving the effective AWF version for installation and runtime.
68+
2. Implementations **SHOULD** surface the resolved AWF version in `aw-info` metadata so that workflow authors can verify which version is active.
69+
70+
### Conformance
71+
72+
An implementation is considered conformant with this ADR if it satisfies all **MUST** and **MUST NOT** requirements above. Failure to meet any **MUST** or **MUST NOT** requirement constitutes non-conformance.
73+
74+
---
75+
76+
*This is a DRAFT ADR generated by the [Design Decision Gate](https://github.com/github/gh-aw/actions/runs/24736713102) workflow. The PR author must review, complete, and finalize this document before the PR can merge.*

docs/src/content/docs/reference/frontmatter-full.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1309,6 +1309,10 @@ sandbox:
13091309
# (optional)
13101310
type: "awf"
13111311

1312+
# AWF version override used to install and run the matching firewall version.
1313+
# (optional)
1314+
version: "example-value"
1315+
13121316
# Container mounts to add when using AWF. Each mount is specified using Docker
13131317
# mount syntax: 'source:destination:mode' where mode can be 'ro' (read-only) or
13141318
# 'rw' (read-write). Example: '/host/path:/container/path:ro'
Lines changed: 230 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package cli
22

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

67
"github.com/github/gh-aw/pkg/logger"
@@ -20,53 +21,242 @@ func getNetworkFirewallCodemod() Codemod {
2021
LogMsg: "Applied network.firewall migration (firewall now always enabled via sandbox.agent: awf default)",
2122
Log: networkFirewallCodemodLog,
2223
PostTransform: func(lines []string, frontmatter map[string]any, fieldValue any) []string {
23-
// Note: We no longer set sandbox.agent: false since the firewall is mandatory
24-
// The firewall is always enabled via the default sandbox.agent: awf
25-
2624
_, hasSandbox := frontmatter["sandbox"]
2725

28-
// Add sandbox.agent if not already present AND if firewall was explicitly true
29-
// (no need to add sandbox.agent: awf if firewall was false, since awf is now the default)
30-
if !hasSandbox && fieldValue == true {
31-
// Only add sandbox.agent: awf if firewall was explicitly set to true
32-
sandboxLines := []string{
33-
"sandbox:",
34-
" agent: awf # Firewall enabled (migrated from network.firewall)",
26+
if !hasSandbox {
27+
sandboxLines := sandboxAgentLinesFromFirewall(fieldValue)
28+
if len(sandboxLines) > 0 {
29+
lines = insertSandboxAfterNetworkBlock(lines, sandboxLines)
30+
networkFirewallCodemodLog.Print("Converted deprecated network.firewall to sandbox.agent")
3531
}
32+
return lines
33+
}
3634

37-
// Try to place it after network block
38-
insertIndex := -1
39-
inNet := false
40-
for i, line := range lines {
41-
trimmed := strings.TrimSpace(line)
42-
if strings.HasPrefix(trimmed, "network:") {
43-
inNet = true
44-
} else if inNet && len(trimmed) > 0 {
45-
// Check if this is a top-level key (no leading whitespace)
46-
if isTopLevelKey(line) {
47-
// Found next top-level key
48-
insertIndex = i
49-
break
50-
}
51-
}
52-
}
35+
lines, merged := mergeFirewallIntoExistingSandbox(lines, fieldValue)
36+
if merged {
37+
networkFirewallCodemodLog.Print("Merged deprecated network.firewall into existing sandbox.agent")
38+
}
39+
return lines
40+
},
41+
})
42+
}
5343

54-
if insertIndex >= 0 {
55-
// Insert after network block
56-
newLines := make([]string, 0, len(lines)+len(sandboxLines))
57-
newLines = append(newLines, lines[:insertIndex]...)
58-
newLines = append(newLines, sandboxLines...)
59-
newLines = append(newLines, lines[insertIndex:]...)
60-
lines = newLines
61-
} else {
62-
// Append at the end
63-
lines = append(lines, sandboxLines...)
44+
func sandboxAgentLinesFromFirewall(fieldValue any) []string {
45+
switch value := fieldValue.(type) {
46+
case bool:
47+
if value {
48+
return []string{
49+
"sandbox:",
50+
" agent: awf # Migrated from deprecated network setting",
51+
}
52+
}
53+
return []string{
54+
"sandbox:",
55+
" agent: false # Migrated from deprecated network setting",
56+
}
57+
case string:
58+
if strings.EqualFold(strings.TrimSpace(value), "disable") {
59+
return []string{
60+
"sandbox:",
61+
" agent: false # Migrated from deprecated network setting",
62+
}
63+
}
64+
case map[string]any:
65+
versionValue, hasVersion := value["version"]
66+
if hasVersion {
67+
if version, ok := normalizeFirewallVersion(versionValue); ok {
68+
return []string{
69+
"sandbox:",
70+
" agent:",
71+
" id: awf # Migrated from deprecated network setting",
72+
" version: " + formatSandboxVersionYAML(version),
6473
}
74+
}
75+
}
76+
return []string{
77+
"sandbox:",
78+
" agent: awf # Migrated from deprecated network setting",
79+
}
80+
case nil:
81+
return []string{
82+
"sandbox:",
83+
" agent: awf # Migrated from deprecated network setting",
84+
}
85+
}
86+
return nil
87+
}
88+
89+
func normalizeFirewallVersion(versionValue any) (string, bool) {
90+
switch value := versionValue.(type) {
91+
case string:
92+
trimmed := strings.TrimSpace(value)
93+
return trimmed, trimmed != ""
94+
case int:
95+
return strconv.Itoa(value), true
96+
case int8:
97+
return strconv.FormatInt(int64(value), 10), true
98+
case int16:
99+
return strconv.FormatInt(int64(value), 10), true
100+
case int32:
101+
return strconv.FormatInt(int64(value), 10), true
102+
case int64:
103+
return strconv.FormatInt(value, 10), true
104+
case uint:
105+
return strconv.FormatUint(uint64(value), 10), true
106+
case uint8:
107+
return strconv.FormatUint(uint64(value), 10), true
108+
case uint16:
109+
return strconv.FormatUint(uint64(value), 10), true
110+
case uint32:
111+
return strconv.FormatUint(uint64(value), 10), true
112+
case uint64:
113+
return strconv.FormatUint(value, 10), true
114+
case float32:
115+
return strconv.FormatFloat(float64(value), 'f', -1, 32), true
116+
case float64:
117+
return strconv.FormatFloat(value, 'f', -1, 64), true
118+
default:
119+
return "", false
120+
}
121+
}
122+
123+
func formatSandboxVersionYAML(version string) string {
124+
// Always quote because sandbox.agent.version is a string field, and this prevents
125+
// YAML from interpreting numeric-like versions as numbers.
126+
return strconv.Quote(version)
127+
}
128+
129+
func insertSandboxAfterNetworkBlock(lines []string, sandboxLines []string) []string {
130+
insertIndex := -1
131+
inNetworkBlock := false
132+
for i, line := range lines {
133+
trimmed := strings.TrimSpace(line)
134+
if strings.HasPrefix(trimmed, "network:") {
135+
inNetworkBlock = true
136+
continue
137+
}
138+
if inNetworkBlock && len(trimmed) > 0 && isTopLevelKey(line) {
139+
insertIndex = i
140+
break
141+
}
142+
}
65143

66-
networkFirewallCodemodLog.Print("Added sandbox.agent: awf (firewall was explicitly enabled)")
144+
if insertIndex >= 0 {
145+
newLines := make([]string, 0, len(lines)+len(sandboxLines))
146+
newLines = append(newLines, lines[:insertIndex]...)
147+
newLines = append(newLines, sandboxLines...)
148+
newLines = append(newLines, lines[insertIndex:]...)
149+
return newLines
150+
}
151+
152+
return append(lines, sandboxLines...)
153+
}
154+
155+
func mergeFirewallIntoExistingSandbox(lines []string, fieldValue any) ([]string, bool) {
156+
agentLines := sandboxAgentLinesForExistingSandbox(fieldValue)
157+
if len(agentLines) == 0 {
158+
return lines, false
159+
}
160+
161+
sandboxIdx := -1
162+
for i, line := range lines {
163+
trimmed := strings.TrimSpace(line)
164+
if isTopLevelKey(line) && strings.HasPrefix(trimmed, "sandbox:") {
165+
sandboxIdx = i
166+
break
167+
}
168+
}
169+
if sandboxIdx == -1 {
170+
return lines, false
171+
}
172+
173+
sandboxIndent := getIndentation(lines[sandboxIdx])
174+
agentIndent := sandboxIndent + " "
175+
sandboxEnd := len(lines)
176+
for i := sandboxIdx + 1; i < len(lines); i++ {
177+
if isTopLevelKey(lines[i]) {
178+
sandboxEnd = i
179+
break
180+
}
181+
}
182+
183+
agentStart := -1
184+
for i := sandboxIdx + 1; i < sandboxEnd; i++ {
185+
trimmed := strings.TrimSpace(lines[i])
186+
if strings.HasPrefix(trimmed, "agent:") && getIndentation(lines[i]) == agentIndent {
187+
agentStart = i
188+
break
189+
}
190+
}
191+
192+
indentedAgentLines := indentLines(agentLines, agentIndent)
193+
if agentStart == -1 {
194+
newLines := make([]string, 0, len(lines)+len(indentedAgentLines))
195+
newLines = append(newLines, lines[:sandboxIdx+1]...)
196+
newLines = append(newLines, indentedAgentLines...)
197+
newLines = append(newLines, lines[sandboxIdx+1:]...)
198+
return newLines, true
199+
}
200+
201+
agentEnd := agentStart + 1
202+
agentFieldIndent := getIndentation(lines[agentStart])
203+
for agentEnd < sandboxEnd {
204+
trimmed := strings.TrimSpace(lines[agentEnd])
205+
if trimmed == "" {
206+
agentEnd++
207+
continue
208+
}
209+
if strings.HasPrefix(trimmed, "#") {
210+
if len(getIndentation(lines[agentEnd])) > len(agentFieldIndent) {
211+
agentEnd++
212+
continue
67213
}
214+
break
215+
}
216+
if len(getIndentation(lines[agentEnd])) > len(agentFieldIndent) {
217+
agentEnd++
218+
continue
219+
}
220+
break
221+
}
68222

69-
return lines
70-
},
71-
})
223+
newLines := make([]string, 0, len(lines)-((agentEnd-agentStart)-len(indentedAgentLines)))
224+
newLines = append(newLines, lines[:agentStart]...)
225+
newLines = append(newLines, indentedAgentLines...)
226+
newLines = append(newLines, lines[agentEnd:]...)
227+
return newLines, true
228+
}
229+
230+
func sandboxAgentLinesForExistingSandbox(fieldValue any) []string {
231+
switch value := fieldValue.(type) {
232+
case bool:
233+
if !value {
234+
return []string{"agent: false # Migrated from deprecated network setting"}
235+
}
236+
case string:
237+
if strings.EqualFold(strings.TrimSpace(value), "disable") {
238+
return []string{"agent: false # Migrated from deprecated network setting"}
239+
}
240+
case map[string]any:
241+
versionValue, hasVersion := value["version"]
242+
if hasVersion {
243+
if version, ok := normalizeFirewallVersion(versionValue); ok {
244+
return []string{
245+
"agent:",
246+
" id: awf # Migrated from deprecated network setting",
247+
" version: " + formatSandboxVersionYAML(version),
248+
}
249+
}
250+
}
251+
}
252+
253+
return nil
254+
}
255+
256+
func indentLines(lines []string, indent string) []string {
257+
indented := make([]string, 0, len(lines))
258+
for _, line := range lines {
259+
indented = append(indented, indent+line)
260+
}
261+
return indented
72262
}

0 commit comments

Comments
 (0)