Skip to content

feat: require operator-authored justification for dangerously-disable-sandbox-agent#38325

Merged
pelikhan merged 2 commits into
mainfrom
copilot/implement-sandbox-agent-justification
Jun 10, 2026
Merged

feat: require operator-authored justification for dangerously-disable-sandbox-agent#38325
pelikhan merged 2 commits into
mainfrom
copilot/implement-sandbox-agent-justification

Conversation

Copilot AI commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

dangerously-disable-sandbox-agent: true (boolean) was accepted as a shorthand and the network-firewall-migration codemod silently injected a generic string justification — both paths let a workflow remove its agent sandbox trust boundary without reviewable operator intent.

Validation

  • Boolean values (true/false) and ${{ expressions }} are now rejected at compile time
  • Strings shorter than 20 characters after whitespace trimming are rejected
  • Error message explicitly states the flag removes a trust boundary, not just a validator check

Struct: AgentSandboxConfig.DisableReason

The validated justification is stored on the parsed struct — available to diagnostics, logging, and audit, not just the compilation gate.

Codemod: no silent justification insertion

ensureSandboxDisableFeatureFlag and migratedSandboxDisableJustification are removed. A workflow migrated from network.firewall: false now fails compilation until the operator writes their own reason. Pre-existing operator-authored justifications are preserved.

Example: valid frontmatter after migration

features:
  dangerously-disable-sandbox-agent: "controlled environment with no internet access"
sandbox:
  agent: false

Tests added

  • true rejected; false rejected; empty/short/whitespace-padded rejected; ${{ inputs.reason }} rejected
  • 20+ char literal passes; justification stored in DisableReason
  • Compiler diagnostic asserts "trust boundary" in message
  • Codemod tests assert the feature flag is not silently inserted

… trust boundary in diagnostics

Co-authored-by: pelikhan <[email protected]>
Copilot AI changed the title feat: require literal justification for dangerously-disable-sandbox-agent feat: require operator-authored justification for dangerously-disable-sandbox-agent Jun 10, 2026
Copilot AI requested a review from pelikhan June 10, 2026 10:23
@pelikhan pelikhan marked this pull request as ready for review June 10, 2026 10:23
Copilot AI review requested due to automatic review settings June 10, 2026 10:23

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

Tightens compilation-time controls around sandbox.agent: false by requiring an explicit, operator-authored justification string (and preserving it on the parsed workflow struct) so disabling the agent sandbox is always a reviewable, intentional trust-boundary change.

Changes:

  • Enforce features.dangerously-disable-sandbox-agent as a 20+ char literal string justification (no booleans / no ${{ }} expressions) and store it on AgentSandboxConfig.DisableReason.
  • Remove the codemod behavior that silently injected a generic justification during network.firewall migration; migrated workflows now require an operator-provided reason.
  • Add unit tests asserting rejection/acceptance cases and that validation diagnostics include “trust boundary”.
Show a summary per file
File Description
pkg/workflow/sandbox.go Adds DisableReason field to retain validated operator justification for downstream diagnostics/audit.
pkg/workflow/sandbox_validation.go Updates sandbox-disable validation messaging and stores the validated justification on the config.
pkg/workflow/sandbox_validation_test.go Adds tests for justification validation, trust-boundary wording, and struct storage.
pkg/cli/codemod_network_firewall.go Removes silent insertion of a generic sandbox-disable justification during firewall migration.
pkg/cli/codemod_network_firewall_test.go Updates codemod assertions to ensure no justification is silently injected and existing ones remain intact.
docs/src/content/docs/reference/sandbox.md Documents the required operator-authored justification and the values the compiler rejects.

Copilot's findings

Tip

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

  • Files reviewed: 6/6 changed files
  • Comments generated: 1

@@ -22,7 +22,9 @@ Configure the coding agent sandbox type to control how the AI engine is isolated
sandbox:
agent: awf

@github-actions

github-actions Bot commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

🧪 Test Quality Sentinel completed test quality analysis.

@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

🧠 Matt Pocock Skills Reviewer was skipped during the skills-based review.

@github-actions

github-actions Bot commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

PR Code Quality Reviewer completed the code quality review.

@github-actions

Copy link
Copy Markdown
Contributor

🏗️ Design Decision Gate — ADR Required

This PR makes significant changes to core business logic (>100 new lines in pkg/) but does not have a linked Architecture Decision Record (ADR).

📄 Draft ADR committed: docs/adr/38325-require-operator-justification-for-disabling-agent-sandbox.md — 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 was generated from the PR diff (validation tightening for dangerously-disable-sandbox-agent, plus the codemod change that stops silently injecting a justification).
  2. Complete the missing sections — confirm the inferred decision, add context the AI could not infer, and refine the alternatives you actually considered.
  3. Commit the finalized ADR to docs/adr/ on your branch.
  4. Reference the ADR in this PR body by adding a line such as:

    ADR: ADR-38325: Require Operator-Authored Justification 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. This PR makes a deliberate security trade-off — requiring operator intent before a workflow can remove a trust boundary — 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.

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

🏗️ ADR gate enforced by Design Decision Gate 🏗️ · 77.3 AIC · ⌖ 9.79 AIC ·

@github-actions

Copy link
Copy Markdown
Contributor

🧪 Test Quality Sentinel Report

Test Quality Score: 80/100 — Excellent

Analyzed 3 new test function(s) (15 scenarios via table-driven subtests): 3 design tests, 0 implementation tests, 0 guideline violations. Also reviewed 3 updated assertion sites in existing tests.

📊 Metrics & Test Classification (3 tests analyzed)
Metric Value
New/modified tests analyzed 3 new functions (+ 3 updated assertion sites in existing tests)
✅ Design tests (behavioral contracts) 3 (100%)
⚠️ Implementation tests (low value) 0 (0%)
Tests with error/edge cases 2 (67%)
Duplicate test clusters 0
Test inflation detected Yes — sandbox_validation_test.go +135 lines vs +3 production lines (45:1)
🚨 Coding-guideline violations 0

Test Classification Details

Test File Classification Notes
TestGetSandboxDisableJustification pkg/workflow/sandbox_validation_test.go ✅ Design 13 t.Run subtests; 10 error cases (bool inputs, empty/short/whitespace strings, expressions), 2 happy-path cases, 1 boundary (trim); comprehensive contract coverage
TestValidateSandboxConfigTrustBoundaryMessage pkg/workflow/sandbox_validation_test.go ✅ Design Verifies the compiler diagnostic message communicates a "trust boundary" change and names the required feature flag
TestValidateSandboxConfigStoresJustification pkg/workflow/sandbox_validation_test.go ✅ Design Verifies post-condition: valid justification is stored on AgentSandboxConfig.DisableReason for audit/logging

Modified Existing Tests (Behavioral Improvements)

Three assertion sites in pkg/cli/codemod_network_firewall_test.go were updated to correctly reflect the new behavior:

  • Two tests now assert NotContains for dangerously-disable-sandbox-agent — correctly verifying the codemod no longer silently invents a justification.
  • One test uses a literal string "migrated from deprecated" instead of the removed constant migratedSandboxDisableJustification — more explicit and self-documenting.

Language Support

Tests analyzed:

  • 🐹 Go (*_test.go): 3 tests (unit, //go:build !integration)
  • 🟨 JavaScript (*.test.cjs, *.test.js): 0 tests
⚠️ Minor Observations — Not Failures (2 items)

⚠️ Test Inflation — pkg/workflow/sandbox_validation_test.go

Observation: 135 lines added to the test file vs. 3 lines of production additions (45:1 ratio, threshold 2:1).
Context: The inflation is justified. The production change is small (a new struct field + one updated error message), but the tests exhaustively cover the full validation contract: 8 rejection cases (booleans, empty/short strings, whitespace collapsing, expression patterns), 2 acceptance cases, and nil/missing input guard rails. This is appropriate boundary testing for a security-relevant feature — requiring substantive human-authored justification before disabling a sandbox trust boundary.

⚠️ Bare require.Error Calls Without Explicit Message Arguments

Observation: Several require.Error(t, err) calls within TestGetSandboxDisableJustification subtests lack a message argument (e.g., require.Error(t, err) instead of require.Error(t, err, "expected rejection for bool input")).
Context: Each bare require.Error is immediately followed by an assert.Contains on the error message text with a descriptive message, and the t.Run subtest name provides strong context. This is a minor style gap, not a correctness issue. The subsequent assertions ensure the error content is verified.
Suggested improvement: Add a message argument to require.Error(t, err) calls: require.Error(t, err, "expected validation to reject bool input") — but this is a non-blocking suggestion.

Verdict

Check passed. 0% of new tests are implementation tests (threshold: 30%). All 3 new test functions enforce behavioral contracts of the new justification validation logic. No coding-guideline violations detected.

📖 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: §27269766773

🧪 Test quality analysis by Test Quality Sentinel · 373.9 AIC · ⌖ 34 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: 80/100. Test quality is excellent — 0% of new tests are implementation tests (threshold: 30%). All 3 new test functions enforce behavioral contracts of the new sandbox justification validation logic.

@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.

Four non-blocking findings across two medium and two low issues. No blocking correctness or security bugs, but three of the four touch the same getSandboxDisableJustification / test suite cluster and are worth cleaning up together.

### Findings summary

Medium — expression/length check ordering (test coverage gap exposes pre-existing bug)
All expression tests use ${{ inputs.reason }} which is exactly 20 chars — the minimum. A short expression like ${{ x }} (8 chars) hits the length gate first and returns the wrong error. Add a test for a sub-20-char expression and reorder the guards: expression check before length check.

Medium — DisableReason is write-only dead storage
The field is stored during validation but no production code reads it. The comment claims audit/diagnostics capabilities that do not exist yet. Either wire it to a real downstream pathway (lock-file metadata, structured log) or add an explicit TODO so the gap is visible.

Low — expression rejection test fragile against constant changes
The test input ${{ inputs.reason }} is exactly at the length boundary. Raise minSandboxDisableJustificationLength above 20 and the test silently starts failing for the wrong reason. Use an unambiguously long expression string to decouple the test from the constant.

Low — migrated YAML file gives no hint about the required next step
The # Migrated from deprecated network setting comment on agent: false is silent about the required dangerously-disable-sandbox-agent feature. The compiler error explains it, but the YAML file itself leaves operators without guidance on re-read. A one-line comment update would eliminate the second confused CI failure.

Warning

Firewall blocked 1 domain

The following domain was blocked by the firewall during workflow execution:

  • proxy.golang.org

To allow these domains, add them to the network.allowed list in your workflow frontmatter:

network:
  allowed:
    - defaults
    - "proxy.golang.org"

See Network Configuration for more information.

🔎 Code quality review by PR Code Quality Reviewer · ⌖ 13.8 AIC

Comments that could not be inline-anchored

pkg/workflow/sandbox_validation_test.go:136

Test coverage gap hides a check-ordering bug in the underlying function: all expression tests use ≥ 20-char values (${{ inputs.reason }} is exactly 20 chars), so no test exercises what happens when an operator provides a short expression like ${{ x }} (8 chars). That case hits the length check first and returns "at least 20 characters" instead of "expressions not allowed", misdirecting the operator.

<details>
<summary>💡 Add a test case and reorder checks in getSandboxDisableJustific…

pkg/workflow/sandbox.go:53

DisableReason is write-only dead storage: the field is assigned in validateSandboxConfig and documented as "available for diagnostics and audit logging", but nothing in this PR — or the existing codebase — reads it. This creates a false impression of an audit trail.

<details>
<summary>💡 Either wire it to a real consumer or mark it as deferred</summary>

If a downstream consumer (e.g. lock-file metadata, audit log, telemetry) genuinely exists or is planned in this sprint, wire it now w…

pkg/cli/codemod_network_firewall.go:55

Codemod silently leaves workflows in a compiler-failing state with no in-file guidance: removing ensureSandboxDisableFeatureFlag is the right call (operators must supply their own justification), but the migrated YAML now reads agent: false # Migrated from deprecated network setting with no hint that compilation will fail until the operator adds features.dangerously-disable-sandbox-agent. The compilation error message is clear, but the YAML file itself is silent.

<details>
<summary>…

pkg/workflow/sandbox_validation_test.go:139

Expression rejection test is fragile: it relies on the input being exactly minSandboxDisableJustificationLength chars: ${{ inputs.reason }} is exactly 20 characters, so the length check passes by a margin of zero and the expression check runs. If minSandboxDisableJustificationLength is ever bumped above 20, the length guard fires first, the error no longer contains &quot;expressions&quot;, and this test fails with a misleading message that hides the root cause.

<details>
<summary>💡 Use an e…

@pelikhan pelikhan merged commit 1aaa556 into main Jun 10, 2026
29 checks passed
@pelikhan pelikhan deleted the copilot/implement-sandbox-agent-justification branch June 10, 2026 11:02
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.

3 participants