Skip to content

Enforce sandbox-disable justification strings and surface new AWF import/safe-output constraints#38228

Merged
pelikhan merged 3 commits into
mainfrom
copilot/awf-feature-surfacing
Jun 10, 2026
Merged

Enforce sandbox-disable justification strings and surface new AWF import/safe-output constraints#38228
pelikhan merged 3 commits into
mainfrom
copilot/awf-feature-surfacing

Conversation

Copilot AI commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

This updates gh-aw surfacing for newly introduced AWF/compiler behavior and closes migration gaps that could lead to compile-time failures. It enforces stronger sandbox-disable intent, keeps import conditional behavior covered by tests, and aligns safe-output docs with current validators.

  • Sandbox disable feature now requires explicit justification

    • sandbox.agent: false now requires features.dangerously-disable-sandbox-agent to be a plain string justification (min 20 chars, expressions disallowed).
    • Compiler validation now rejects missing/boolean/short/expression values for this feature.
    • Codemod migration (network.firewall -> sandbox.agent) now injects a safe default justification when migrating disable cases.
    features:
      dangerously-disable-sandbox-agent: "controlled environment with no internet access"
    sandbox:
      agent: false
  • Import-level conditional gating behavior is covered

    • Added compiler test coverage asserting import-level if: gating affects imported prompt/steps as expected.
    • Removed imports[].if mentions from documentation per review feedback.
  • Upgrade and syntax docs corrected for current behavior

    • Updated upgrade guidance to require the sandbox-disable justification alongside sandbox.agent: false.
    • Updated syntax-agentic.md to describe the flag’s required shape and security intent.
  • Safe-output create_issue.body limits surfaced

    • Documented create_issue.body constraints (min 20, max 65000) in safe-output references and workflow-authoring guidance so generated workflows avoid placeholder-short bodies.

…safe-output docs

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Copilot AI changed the title [WIP] Update documentation for AWF surfacing gaps Enforce sandbox-disable justification strings and surface new AWF import/safe-output constraints Jun 9, 2026
Copilot AI requested a review from pelikhan June 9, 2026 23:44
@pelikhan

pelikhan commented Jun 9, 2026

Copy link
Copy Markdown
Collaborator

@copilot remove imports.if mentioned in documentation.

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
@pelikhan pelikhan marked this pull request as ready for review June 10, 2026 01:04
Copilot AI review requested due to automatic review settings June 10, 2026 01:04

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This pull request updates gh-aw validation, codemods, tests, and documentation to align with newer AWF/compiler behavior—most notably requiring an explicit string justification to disable the agent sandbox, and surfacing safe-output field constraints to prevent downstream validation failures.

Changes:

  • Enforces features.dangerously-disable-sandbox-agent as a non-expression justification string (min length 20) when sandbox.agent: false is used, and updates validation/tests accordingly.
  • Extends the network firewall codemod to inject the new sandbox-disable feature value when migrating network.firewall: false.
  • Updates docs to reflect the new sandbox-disable requirement and safe-output create_issue.body length limits, and adds a compiler test covering conditional import gating.
Show a summary per file
File Description
pkg/workflow/workflow_run_validation_test.go Updates test fixtures to use the new sandbox-disable justification string.
pkg/workflow/sandbox_validation.go Implements validation requiring a justification string (min length, no expressions) to disable sandbox.
pkg/workflow/sandbox_test.go Adds unit tests for valid/missing/short/expression sandbox-disable justifications.
pkg/workflow/sandbox_agent_false_test.go Expands integration tests for rejecting short/expression justifications.
pkg/workflow/pull_request_target_validation_test.go Updates fixtures to use justification strings for sandbox disable.
pkg/workflow/imports_test.go Adds test coverage for imports[].if conditional gating behavior.
pkg/workflow/importable_tools_test.go Updates fixtures to use justification strings for sandbox disable.
pkg/workflow/compiler_validators_test.go Updates validator tests to reflect new justification-string requirement.
pkg/constants/feature_constants.go Updates feature flag usage example to the new string form.
pkg/cli/codemod_network_firewall.go Injects sandbox-disable justification when codemodding network.firewall disable cases.
pkg/cli/codemod_network_firewall_test.go Adds assertions/tests verifying codemod adds/preserves sandbox-disable justification strings.
docs/src/content/docs/reference/safe-outputs.md Documents create_issue.body min/max length constraints.
.github/aw/upgrade-agentic-workflows.md Updates upgrade guidance to include the required sandbox-disable justification.
.github/aw/syntax-agentic.md Documents the required justification-string shape and intent for sandbox disabling.
.github/aw/safe-outputs-content.md Adds explicit create_issue.body length requirements to safe-outputs guidance.
.github/aw/experiments.md Refines guidance text around evaluation order across import boundaries.
.github/aw/create-agentic-workflow.md Adds authoring guidance to avoid too-short create-issue bodies.

Copilot's findings

Tip

Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

  • Files reviewed: 17/17 changed files
  • Comments generated: 2

)
}
sandboxValidationLog.Printf("sandbox.agent: false permitted by %s feature flag", constants.DangerouslyDisableSandboxAgentFeatureFlag)
sandboxValidationLog.Printf("sandbox.agent: false permitted by %s justification: %q", constants.DangerouslyDisableSandboxAgentFeatureFlag, justification)
Comment on lines +308 to +311
for i := featuresIdx + 1; i < featuresEnd; i++ {
if strings.HasPrefix(strings.TrimSpace(lines[i]), featureKey) {
return lines
}
@github-actions

github-actions Bot commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

Design Decision Gate 🏗️ completed the design decision gate check.

@github-actions

github-actions Bot commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

⚠️ PR Code Quality Reviewer failed during code quality review.

@github-actions

github-actions Bot commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

🧠 Matt Pocock Skills Reviewer has completed the skills-based review. ✅

@github-actions

github-actions Bot commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

🧪 Test Quality Sentinel completed test quality analysis.

@pelikhan pelikhan merged commit 2d177db into main Jun 10, 2026
67 of 69 checks passed
@pelikhan pelikhan deleted the copilot/awf-feature-surfacing branch June 10, 2026 01:15
@github-actions

Copy link
Copy Markdown
Contributor

🏗️ Design Decision Gate — ADR Required

This PR makes significant changes to core business logic (377 new lines in pkg/), but no Architecture Decision Record (ADR) is linked in the PR body.

📄 Draft ADR committed: docs/adr/38228-require-justification-to-disable-agent-sandbox.md — generated from the diff. Review and complete it before merging.

🔒 This PR cannot merge until an ADR is linked in the PR body.

📋 What to do next
  1. Review the draft ADR committed to your branch — it captures the decision to require dangerously-disable-sandbox-agent to be a justification string (≥20 chars, no expressions) as a precondition for sandbox.agent: false.
  2. Complete / refine the sections — add context the gate could not infer, confirm the alternatives, and adjust the normative requirements if needed.
  3. Commit the finalized ADR to docs/adr/ on your branch.
  4. Reference the ADR in this PR body, e.g.:

    ADR: ADR-38228: Require a Justification String to Disable the Agent Sandbox

Once an ADR is linked in the PR body, this gate will re-run and verify the implementation matches the decision.

❓ Why ADRs Matter

ADRs create a searchable, permanent record of why the codebase looks the way it does. A security-sensitive escape hatch like disabling the agent sandbox is exactly the kind of decision future contributors will want the rationale for.

📋 Michael Nygard ADR Format Reference

An ADR must contain these four sections to be considered complete:

  • Context — What is the problem? What forces are at play?
  • Decision — What did you decide? Why?
  • Alternatives Considered — What else could have been done?
  • Consequences — What are the trade-offs (positive and negative)?

ADRs are stored in docs/adr/ as Markdown files numbered by PR number (e.g., 38228-...md).

🔒 Blocking: link a completed ADR in the PR body to clear this gate.

🏗️ ADR gate enforced by Design Decision Gate 🏗️ · 97.5 AIC · ⌖ 10.1 AIC ·

@github-actions

Copy link
Copy Markdown
Contributor

🧪 Test Quality Sentinel Report

Test Quality Score: 96/100 — Excellent

Analyzed 8 new test scenario(s): 8 design, 0 implementation, 0 guideline violations. All new tests enforce behavioral contracts.

📊 Metrics & Test Classification (8 new scenarios analyzed)
Metric Value
New/modified test scenarios analyzed 8
✅ Design tests (behavioral contracts) 8 (100%)
⚠️ Implementation tests (low value) 0 (0%)
Tests with error/edge cases 7 (87.5%)
Duplicate test clusters 0
Test inflation detected No
🚨 Coding-guideline violations 0

Test Classification Details

Test File Classification Notes
TestNetworkFirewallCodemod_PreservesExistingSandboxDisableJustification pkg/cli/codemod_network_firewall_test.go:215 ✅ Design Behavioral: codemod must not overwrite existing justification; has require.NoError error path
TestCompileWorkflowWithConditionalImport pkg/workflow/imports_test.go:177 ✅ Design Behavioral: end-to-end compilation produces correct conditional-import output; all 5 assertions check error paths
"sandbox.agent: false with short justification is rejected" pkg/workflow/sandbox_agent_false_test.go ✅ Design Behavioral contract: compiler rejects dangerously-disable-sandbox-agent values shorter than 20 chars
"sandbox.agent: false with expression justification is rejected" pkg/workflow/sandbox_agent_false_test.go ✅ Design Behavioral contract: compiler rejects expression values like ${{ inputs.reason }}
TestValidateSandboxConfig — row: "valid justification" pkg/workflow/sandbox_test.go ✅ Design Happy path: valid string justification passes validation
TestValidateSandboxConfig — row: "without justification" pkg/workflow/sandbox_test.go ✅ Design Error case: missing feature flag is rejected
TestValidateSandboxConfig — row: "short justification" pkg/workflow/sandbox_test.go ✅ Design Error case: string too short (< 20 chars) is rejected
TestValidateSandboxConfig — row: "expression justification" pkg/workflow/sandbox_test.go ✅ Design Error case: expression syntax in justification is rejected

Note on fixture-update tests: ~11 additional tests in importable_tools_test.go, pull_request_target_validation_test.go, compiler_validators_test.go, and workflow_run_validation_test.go had their fixture data updated from dangerously-disable-sandbox-agent: true → a string value, keeping existing coverage valid under the new type constraint. These are not counted as new scenarios.

Language Support

Tests analyzed:

  • 🐹 Go (*_test.go): 8 new scenarios across unit (//go:build !integration) and integration (//go:build integration) files
  • 🟨 JavaScript (*.test.cjs, *.test.js): 0 tests (no JS tests in this PR)

Inflation Ratios

Test File Lines Added Production File Production Lines Added Ratio
codemod_network_firewall_test.go 38 codemod_network_firewall.go 77 0.49:1 ✅
sandbox_test.go 55 sandbox_validation.go 55 1.0:1 ✅
sandbox_agent_false_test.go 73 sandbox_validation.go 55 1.33:1 ✅
imports_test.go 63 imports.go 0 N/A*

*imports_test.go tests pre-existing conditional-import compilation behavior with no corresponding production change — not test inflation.

⚠️ Flagged Tests — Minor Issues (1 item)

⚠️ TestNetworkFirewallCodemod_PreservesExistingSandboxDisableJustification (pkg/cli/codemod_network_firewall_test.go:215)

Classification: Design test (high value) — no impact on pass/fail
Issue: 3 of 4 assertion calls are missing descriptive message arguments, inconsistent with the sibling tests in the same file that all include messages.

// Missing messages:
require.NoError(t, err)                          // should be: require.NoError(t, err, "Apply should succeed")
assert.True(t, applied)                          // should be: assert.True(t, applied, "Codemod should be applied")
assert.Contains(t, result, `dangerously-...`)    // should add a descriptive message string
assert.NotContains(t, result, ..., "Should not overwrite existing justification")  // ✅ has message

Suggested improvement: Add a descriptive message to the three bare assertion calls to match the convention of all surrounding tests in this file.

Verdict

Check passed. 0% of new tests are implementation tests (threshold: 30%). No coding-guideline violations detected. Minor: 3 assertions in TestNetworkFirewallCodemod_PreservesExistingSandboxDisableJustification are missing descriptive message arguments (non-blocking).

📖 Understanding Test Classifications

Design Tests (High Value) verify what the system does:

  • Assert on observable outputs, return values, or state changes
  • Cover error paths and boundary conditions
  • Would catch a behavioral regression if deleted
  • Remain valid even after internal refactoring

Implementation Tests (Low Value) verify how the system does it:

  • Assert on internal function calls (mocking internals)
  • Only test the happy path with typical inputs
  • Break during legitimate refactoring even when behavior is correct
  • Give false assurance: they pass even when the system is wrong

Goal: Shift toward tests that describe the system's behavioral contract — the promises it makes to its users and collaborators.

References: §27246096002

🧪 Test quality analysis by Test Quality Sentinel · 378.8 AIC · ⌖ 29.1 AIC ·

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Test Quality Sentinel: 96/100. Test quality is excellent — 0% of new tests are implementation tests (threshold: 30%). All 8 new test scenarios enforce behavioral contracts, with 87.5% covering error/edge cases. No mock-library violations or missing build tags detected. One minor non-blocking note: 3 assertions in TestNetworkFirewallCodemod_PreservesExistingSandboxDisableJustification are missing descriptive message arguments.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Skills-Based Review 🧠

Applied /tdd and /grill-with-docs — requesting changes on test-coverage gaps in the new security-critical validation path.

📋 Key Themes & Highlights

Key Themes

  • Missing breaking-change test case: dangerously-disable-sandbox-agent: true (the old boolean) is not covered by a unit test. This is the most likely migration failure mode for users upgrading from the boolean form, and the resulting error message is generic and unhelpful.
  • Codemod injection path undertested: ensureSandboxDisableFeatureFlag has no test for when features: already exists with other flags. The key injection into an existing block is the most structurally risky code path in the codemod.
  • Security-gate regex lacks adversarial coverage: The expression-blocking regex guards a meaningful security constraint. Without near-miss test cases, a future regex edit could silently allow bypasses.
  • Docs terminology inconsistency: "dangerous-disable justification feature" does not match dangerously-disable-sandbox-agent.

Positive Highlights

  • ✅ Boolean → string breaking change is well-motivated and the error message + suggestion text are clear and actionable
  • ✅ Idempotent codemod (TestNetworkFirewallCodemod_PreservesExistingSandboxDisableJustification) is a good defensive test
  • getFeatureValueCaseInsensitive is a nice robustness touch
  • ✅ Three new integration-level reject tests (missing, too short, expression) in sandbox_agent_false_test.go provide solid end-to-end coverage
  • ✅ Conditional import test is a good first exercise of the import gating path

🧠 Reviewed using Matt Pocock's skills by Matt Pocock Skills Reviewer · 592.3 AIC · ⌖ 14 AIC

},
expectError: true,
errorMsg: "expressions",
},

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/tdd] Missing test case: dangerously-disable-sandbox-agent: true (the old boolean form) being rejected. This is the most common migration failure mode — existing workflows with true will hit validation and see a generic "feature must be a string, got bool" error with no migration guidance.

💡 Suggested test case to add after this block
{
    name: "sandbox.agent false with legacy boolean flag is rejected",
    data: &WorkflowData{
        Features: map[string]any{
            "dangerously-disable-sandbox-agent": true,
        },
        SandboxConfig: &SandboxConfig{
            Agent: &AgentSandboxConfig{Disabled: true},
        },
    },
    expectError: true,
    errorMsg:    "string",
},

This locks the breaking-change path: true → "feature must be a string" error, ensuring a future refactor of getSandboxDisableJustification cannot silently drop the type check.

require.NoError(t, err)
assert.True(t, applied)
assert.Contains(t, result, `dangerously-disable-sandbox-agent: "already documented justification string with enough detail"`)
assert.NotContains(t, result, migratedSandboxDisableJustification, "Should not overwrite existing justification")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/tdd] There is no test for when features: already exists with other keys (not dangerously-disable-sandbox-agent). The if featuresIdx >= 0 branch in ensureSandboxDisableFeatureFlag injects the key into the existing block — this path is currently untested.

💡 Suggested test scenario

Add a test where the content includes:

features:
  some-other-flag: true

but not dangerously-disable-sandbox-agent. After the codemod runs, assert:

  • dangerously-disable-sandbox-agent appears inside the features: block
  • some-other-flag: true is preserved
  • No duplicate features: top-level key is added

Without this, a line-index off-by-one in ensureSandboxDisableFeatureFlag could corrupt the features: block without any test catching it.


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

const migratedSandboxDisableJustification = "migrated from deprecated network.firewall disable setting"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/tdd] The migratedSandboxDisableJustification constant is auto-injected into migrated workflows, which are then validated by getSandboxDisableJustification() (≥20 chars, no expressions). There is no test asserting the constant itself satisfies those rules. A future edit that shortens the string below 20 characters would silently produce codemod output that fails validation.

💡 Suggested cross-validation test
func TestMigratedJustificationPassesValidation(t *testing.T) {
    data := &workflow.WorkflowData{
        Features: map[string]any{
            "dangerously-disable-sandbox-agent": migratedSandboxDisableJustification,
        },
        SandboxConfig: &workflow.SandboxConfig{
            Agent: &workflow.AgentSandboxConfig{Disabled: true},
        },
    }
    // This would require exporting getSandboxDisableJustification or
    // testing via the full compiler path:
    require.NoError(t, compiler.CompileWorkflow(workflowWithMigratedJustification))
}

Alternatively, a simple length assertion in a package test is enough to lock the contract.

```

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/grill-with-docs] Wording inconsistency: "the dangerous-disable justification feature" does not match the actual feature key dangerously-disable-sandbox-agent. A reader skimming this doc will not know what key to write.

Suggest replacing with: "you must provide the dangerously-disable-sandbox-agent justification:"

name: "sandbox.agent false with expression justification",
data: &WorkflowData{
Features: map[string]any{
"dangerously-disable-sandbox-agent": "${{ inputs.reason }}",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/tdd] The expression-blocking regex is a security gate, but only one expression form is tested (${{ inputs.reason }}). There is no adversarial coverage for near-misses that could indicate bypass or false-positive.

💡 Suggested additional cases in sandbox_test.go or a dedicated regex test
Input Expected
"no expression, long enough to pass the min length check" ✅ pass
"long enough ${{ a }} and ${{ b }} double expression" ❌ rejected (still matches)
"single brace ${inputs.reason} long enough string here" ✅ pass (not GH Actions syntax)
"$(inputs.reason) long enough alternative syntax here" ✅ pass (not GH Actions syntax)

Without this, a future regex change (e.g. swapping [\s\S]* to .+) could silently create a bypass without any test failing.

`{{#if experiments.strategy == "eager"}}`,
"{{/if}}",
"needs.activation.outputs.strategy == 'eager'",
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/tdd] These assertions verify that the template guard syntax is present in the compiled output, but not that the imported step content is wrapped inside the {{#if}} block. The step echo "from import" could appear outside the guard and all three assertions would still pass.

💡 Stronger structural assertion
ifIdx := strings.Index(compiled, `{{#if experiments.strategy == "eager"}}`)
endifIdx := strings.Index(compiled, "{{/if}}")
stepIdx := strings.Index(compiled, `echo "from import"`)

require.Greater(t, ifIdx, -1, "{{#if}} guard should be present")
require.Greater(t, endifIdx, -1, "{{/if}} guard should be present")
require.Greater(t, stepIdx, ifIdx, "imported step should appear after {{#if}} guard")
require.Less(t, stepIdx, endifIdx, "imported step should appear before {{/if}} guard")

This turns a structural smoke test into a real gating assertion.

}

trimmed := strings.TrimSpace(justification)
if len(trimmed) < minSandboxDisableJustificationLength {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/tdd] len(trimmed) counts UTF-8 bytes, but the documentation says "minimum 20 characters". For ASCII-only justifications (the expected case) these are equivalent, but a user writing a justification with multi-byte Unicode characters (e.g. "Ü" = 2 bytes) or non-Latin scripts could see surprising behavior.

Consider utf8.RuneCountInString(trimmed) to match what the docs say, or update docs to say "20 bytes".

return trimmed, nil
}

func getFeatureValueCaseInsensitive(features map[string]any, flagName string) (any, bool) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/grill-with-docs] getFeatureValueCaseInsensitive silently accepts case variants of the feature key (e.g. DANGEROUSLY-DISABLE-SANDBOX-AGENT: "..." is valid). This behaviour is undocumented in syntax-agentic.md and has no test coverage for the case-insensitive match path (the loop is only exercised when the exact-match on line 179 fails).

Either add a test that passes DANGEROUSLY-DISABLE-SANDBOX-AGENT as a key and verifies it is accepted, or document the case-insensitivity explicitly so it is a deliberate, tested contract rather than an implementation side-effect.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔎 Code quality review by PR Code Quality Reviewer · 12.7 AIC · ⌖ 13.3 AIC

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[awf-feature-surfacing] AWF surfacing gaps: conditional if: imports, dangerously-disable-sandbox-agent flag, and create_issue body minLength

3 participants