Skip to content

fix: propagate tools.startup-timeout to gateway.startupTimeout (prevents safeoutputs eviction on slow runners)#46875

Merged
pelikhan merged 4 commits into
mainfrom
copilot/fix-safeoutputs-backend-issue
Jul 20, 2026
Merged

fix: propagate tools.startup-timeout to gateway.startupTimeout (prevents safeoutputs eviction on slow runners)#46875
pelikhan merged 4 commits into
mainfrom
copilot/fix-safeoutputs-backend-issue

Conversation

Copilot AI commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

MCP Gateway's built-in startupTimeout is 30s. When a safeoutputs backend starts later than that (common on busy runners), the gateway evicts it, permanently caches zero tools, and the workflow reports success with no output. gh-aw was not emitting gateway.startupTimeout, so the gateway always used its 30s default instead of gh-aw's documented 120s default.

Changes

  • MCPGatewayRuntimeConfig (tools_types.go): New StartupTimeout int field, emitted as gateway.startupTimeout in the JSON config pipe.

  • buildMCPGatewayConfig (mcp_gateway_config.go): Computes startupTimeout from tools.startup-timeout (integer seconds); falls back to constants.DefaultMCPStartupTimeout (120s) when unset or when value is a GitHub Actions expression not evaluable at compile time. time import added.

  • RenderJSONMCPConfig (mcp_renderer.go): Emits "startupTimeout": N in the gateway section whenever StartupTimeout > 0. Because the default is always 120, this field is now always present — preventing the gateway from applying its own 30s default.

Every compiled workflow now includes:

"gateway": {
  "port": $MCP_GATEWAY_PORT,
  "domain": "${MCP_GATEWAY_DOMAIN}",
  "apiKey": "${MCP_GATEWAY_API_KEY}",
  "payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}",
  "startupTimeout": 120
}
  • All 260 workflow .lock.yml files recompiled.
  • Unit tests added for buildMCPGatewayConfig (two new cases: explicit integer and expression fallback) and RenderJSONMCPConfig (four cases covering default, custom, zero, and minimum).
  • Golden test files and codex_engine_test.go updated to reflect the new field in expected output.

pr-sous-chef run https://github.com/github/gh-aw/actions/runs/29761732160

Warning

Firewall blocked 1 domain

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

  • pi.dev

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

network:
  allowed:
    - defaults
    - "pi.dev"

See Network Configuration for more information.

Generated by 👨‍🍳 PR Sous Chef · 31.2 AIC · ⌖ 6.13 AIC · ⊞ 6.9K ·
Comment /souschef to run again

…vent safeoutputs timeout

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Copilot AI changed the title [WIP] Fix safeoutputs backend startup timeout issue fix: propagate tools.startup-timeout to gateway.startupTimeout (prevents safeoutputs eviction on slow runners) Jul 20, 2026
Copilot AI requested a review from pelikhan July 20, 2026 16:13
@pelikhan
pelikhan marked this pull request as ready for review July 20, 2026 16:14
Copilot AI review requested due to automatic review settings July 20, 2026 16:14

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

Propagates MCP startup timeout configuration to the gateway to prevent slow backends from being prematurely evicted.

Changes:

  • Adds and renders gateway.startupTimeout.
  • Defaults the gateway timeout to 120 seconds.
  • Updates tests, golden fixtures, and generated workflows.
Show a summary per file
File Description
pkg/workflow/tools_types.go Adds runtime startup-timeout state.
pkg/workflow/mcp_gateway_config.go Builds the gateway timeout value.
pkg/workflow/mcp_renderer.go Emits startupTimeout in gateway JSON.
pkg/workflow/mcp_gateway_config_test.go Tests default, literal, and expression handling.
pkg/workflow/mcp_renderer_test.go Tests timeout rendering behavior.
pkg/workflow/codex_engine_test.go Updates expected MCP configuration.
pkg/workflow/testdata/**/*.golden Updates generated-output expectations.
.github/workflows/*.lock.yml Recompiles 260 workflows with the gateway timeout.

Review details

Tip

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

  • Files reviewed: 280/280 changed files
  • Comments generated: 1
  • Review effort level: Medium

Comment on lines +165 to +168
if workflowData.ToolsStartupTimeout != "" {
if n := templatableIntValue(&workflowData.ToolsStartupTimeout); n > 0 {
startupTimeout = n
}
@github-actions

github-actions Bot commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

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

@github-actions

github-actions Bot commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

Test Quality Sentinel completed test quality analysis.

@github-actions

github-actions Bot commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

PR Code Quality Reviewer completed the code quality review.

@github-actions

github-actions Bot commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

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

@github-actions

Copy link
Copy Markdown
Contributor

🧪 Test Quality Sentinel Report

Test Quality Score: 27/100 — Poor

Analyzed 16 test case(s): 5 design, 11 implementation, 2 violation(s).

📊 Metrics (16 tests)
Metric Value
Analyzed 16 (Go: 16, JS: 0)
✅ Design 5 (31%)
⚠️ Implementation 11 (69%)
Edge/error coverage 5 (31%)
Duplicate clusters 1 (14 identical assertions)
Inflation YES (6.35:1)
🚨 Violations 2
⚠️ Flagged Issues

Test Inflation Violation: Test-to-production ratio of 6.35:1 (127 test lines vs 20 production lines), far exceeding the 2:1 threshold.

  • mcp_gateway_config_test.go: +45 lines (mostly repetitive StartupTimeout field assertions added to 14 existing test cases)
  • mcp_renderer_test.go: +80 lines (new TestRenderJSONMCPConfig_StartupTimeout function)
  • Production changes: only +20 lines total

Duplication Violation: StartupTimeout field assertion repeated identically across 14 test cases in TestBuildMCPGatewayConfig. This pattern should be extracted into a helper function or consolidated:

assert.Equal(t, tt.expected.StartupTimeout, result.StartupTimeout, "StartupTimeout should match")

Occurs in each of the 14 test cases added during this change.

Recommendation: Refactor mcp_gateway_config_test.go to use a shared validation helper that accepts a pointer to the config struct, reducing duplication and improving maintainability.

Verdict

Failed. High test inflation (6.35:1, threshold 2:1) and significant duplication (14 identical assertions). While the new TestRenderJSONMCPConfig_StartupTimeout is well-designed with good edge-case coverage (default, custom, zero, boundary values), the overall test additions are not proportionate to the production changes.

Positive notes:

  • ✅ New TestRenderJSONMCPConfig_StartupTimeout covers 4 solid scenarios (default 120s, custom 180s, zero omission, minimum 1s)
  • ✅ All tests include required //go:build !integration tags
  • ✅ Good edge-case coverage for startup timeout values

Required fixes:

  1. Consolidate StartupTimeout assertions in TestBuildMCPGatewayConfig into a shared helper
  2. Add comments explaining why 14 test cases needed the field added (for reviewability)
  3. Consider batching related assertions to reduce visual duplication

🧪 Test quality analysis by Test Quality Sentinel · 16.4 AIC · ⌖ 8.18 AIC · ⊞ 7K ·
Comment /review to run again

@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: 27/100. Test inflation (6.35:1 > 2:1 threshold) and duplication (14 identical assertions) violations. See detailed comment above.

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

Two issues need fixing before merge

1. Expression split-brain (high): When tools.startup-timeout is a GitHub Actions expression, the gateway JSON is compiled with startupTimeout: 120 but the engine receives the raw expression string via GH_AW_STARTUP_TIMEOUT and evaluates it at runtime. If that expression resolves to >120s, the gateway will evict backends the engine is still waiting for — silently reproducing the original bug for expression-valued timeouts. No warning is emitted to the user.

2. Fragile > 0 guard in renderer (medium): The renderer only emits startupTimeout when StartupTimeout > 0, which contradicts the PR's own stated invariant ("this field is now always present"). Correctness depends on buildMCPGatewayConfig always producing 120; any future caller that zero-initialises the struct silently drops the field and reintroduces the 30s-eviction bug.

What works well
  • Core fix is sound: the 30s default is overridden for all existing compiled workflows.
  • templatableIntValue is the correct tool for compile-time expression detection.
  • Test coverage for the new cases is present and meaningful.
  • 260 lock files recompiled consistently.

🔎 Code quality review by PR Code Quality Reviewer · 67.7 AIC · ⌖ 4.74 AIC · ⊞ 5.6K
Comment /review to run again

if workflowData.ToolsStartupTimeout != "" {
if n := templatableIntValue(&workflowData.ToolsStartupTimeout); n > 0 {
startupTimeout = n
}

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.

Expression-valued startup-timeout creates a split-brain: gateway gets 120s hardcoded while the engine uses the runtime-evaluated expression. If the expression resolves to >120 at runtime, the gateway will evict backends the engine is still polling, silently reproducing the original bug.

💡 Details and suggested fix

buildMCPGatewayConfig falls back to 120 when templatableIntValue can't evaluate the expression:

if n := templatableIntValue(&workflowData.ToolsStartupTimeout); n > 0 {
    startupTimeout = n
}

Meanwhile engine_helpers.go passes the raw expression string to the engine via GH_AW_STARTUP_TIMEOUT, which is evaluated at runtime to whatever the user set (e.g. 300s). The gateway JSON is already compiled with "startupTimeout": 120.

Result: a user who sets tools.startup-timeout: 300 via an expression gets a gateway that kills slow-starting backends after 120s while the engine patiently waits up to 300s — the original eviction bug, just with a higher threshold.

At minimum, emit a log warning when the fallback is applied so operators can see it in debug output:

if workflowData.ToolsStartupTimeout != "" {
    if n := templatableIntValue(&workflowData.ToolsStartupTimeout); n > 0 {
        startupTimeout = n
    } else {
        mcpGatewayConfigLog.Printf("tools.startup-timeout %q is a GitHub Actions expression and cannot be evaluated at compile time; gateway.startupTimeout will be fixed at %ds", workflowData.ToolsStartupTimeout, startupTimeout)
    }
}

The only real fix is to document this limitation explicitly (the gateway JSON does not support runtime expressions) and surface it as a compiler warning to the user.

// Without this field, the gateway evicts safeoutputs backends that start after 30s
// and permanently caches zero tools, causing silent data loss. The gh-aw default is
// 120 seconds (constants.DefaultMCPStartupTimeout); users can override via tools.startup-timeout.
if options.GatewayConfig.StartupTimeout > 0 {

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.

StartupTimeout > 0 guard contradicts the stated invariant "this field is now always present" and creates a silent regression footgun.

💡 Details and suggested fix

The PR description says "Because the default is always 120, this field is now always present — preventing the gateway from applying its own 30s default." But the renderer guard:

if options.GatewayConfig.StartupTimeout > 0 {
    fmt.Fprintf(&configBuilder, ",\n              \"startupTimeout\": %d", options.GatewayConfig.StartupTimeout)
}

means the field is only emitted when the caller happens to set a positive value. The safety property depends entirely on the invariant that buildMCPGatewayConfig always produces StartupTimeout = 120. Any future caller that zero-initialises MCPGatewayRuntimeConfig (e.g. in tests, new code paths, or if the default is ever changed to 0 to mean use

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

Review: propagate tools.startup-timeout to gateway.startupTimeout

The fix is correct and well-scoped. Key observations:

  • StartupTimeout field on MCPGatewayRuntimeConfig is correctly tagged yaml:"-" (not user-settable, computed-only).
  • buildMCPGatewayConfig always sets the 120s default and overrides only when the user value parses as a positive integer — GitHub Actions expressions correctly fall back to the default.
  • RenderJSONMCPConfig guards with > 0, ensuring zero is never emitted.
  • All 260 lock files are uniformly regenerated.
  • Unit tests cover the four meaningful renderer cases (default, custom, zero-omit, minimum) and the two new config cases (explicit integer, expression fallback).

No blocking issues. ✅

🧵 Reviewed using Impeccable skills by Impeccable Skills Reviewer · 46 AIC · ⌖ 4.36 AIC · ⊞ 5K

…eout

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

🏗️ Design Decision Gate — ADR Required

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

📄 Draft ADR committed: docs/adr/46875-always-emit-gateway-startup-timeout.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
  2. Complete the missing sections — add context the AI could not infer, refine the decision rationale, and list real alternatives you 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-46875: Always Emit gateway.startupTimeout

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

❓ Why ADRs Matter

"AI made me procrastinate on key design decisions. Because refactoring was cheap, I could always say 'I'll deal with this later.' Deferring decisions corroded my ability to think clearly."

ADRs create a searchable, permanent record of why the codebase looks the way it does. Future contributors (and your future self) will thank you.

📋 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)?

All ADRs are stored in docs/adr/ as Markdown files numbered by PR number (e.g., 46875-always-emit-gateway-startup-timeout.md for PR #46875).

🏗️ ADR gate enforced by Design Decision Gate 🏗️ · 54.5 AIC · ⌖ 7.86 AIC · ⊞ 8.5K ·
Comment /review to run again

@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 /diagnosing-bugs and /tdd. The fix is correct and well-tested — commenting (not blocking) on two low-risk gaps.

Positive Highlights

  • Root cause correctly identified: startupTimeout was never emitted, so the gateway silently used its 30s default.
  • Clean propagation chain: ToolsStartupTimeout string → integer computation → struct field → JSON emission.
  • Regression tests cover explicit integer, expression fallback, and four renderer cases.
  • All 260 lock files recompiled consistently.
  • yaml:"-" tag correctly prevents the field leaking into serialised YAML.

🧠 Reviewed using Matt Pocock's skills by Matt Pocock Skills Reviewer · 62 AIC · ⌖ 4.68 AIC · ⊞ 6.7K
Comment /matt to run again

// slightly late. GitHub Actions expressions (non-numeric strings) fall back to the default.
startupTimeout := int(constants.DefaultMCPStartupTimeout / time.Second)
if workflowData.ToolsStartupTimeout != "" {
if n := templatableIntValue(&workflowData.ToolsStartupTimeout); n > 0 {

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] Negative integer values (e.g. startup-timeout: -1) silently fall through to the 120s default because n > 0 rejects them. Add a test case for -1 and "0" to document this clamping behaviour explicitly.

💡 Suggested test cases
{
    name: "clamps negative startup-timeout to default",
    workflowData: &WorkflowData{
        ToolsStartupTimeout: "-1",
        SandboxConfig:       &SandboxConfig{},
    },
    expected: &MCPGatewayRuntimeConfig{
        // ... other fields ...
        StartupTimeout: int(constants.DefaultMCPStartupTimeout / time.Second),
    },
},
{
    name: "clamps zero startup-timeout to default",
    workflowData: &WorkflowData{
        ToolsStartupTimeout: "0",
        SandboxConfig:       &SandboxConfig{},
    },
    expected: &MCPGatewayRuntimeConfig{
        StartupTimeout: int(constants.DefaultMCPStartupTimeout / time.Second),
    },
},

Without these tests, a user who sets startup-timeout: 0 or -1 gets the 120s default with no warning or documentation. The tests make that contract explicit.

@copilot please address this.

// Without this field, the gateway evicts safeoutputs backends that start after 30s
// and permanently caches zero tools, causing silent data loss. The gh-aw default is
// 120 seconds (constants.DefaultMCPStartupTimeout); users can override via tools.startup-timeout.
if options.GatewayConfig.StartupTimeout > 0 {

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.

[/diagnosing-bugs] The guard StartupTimeout > 0 means that if StartupTimeout is explicitly set to 0 (e.g. via a code path that zeroes it out), the field is silently omitted and the gateway falls back to its 30s default — the exact failure mode this fix is meant to prevent. Since buildMCPGatewayConfig always sets this to at least 120, the guard is safe today, but it creates a latent risk for future callers. Consider removing the guard or documenting the invariant clearly.

💡 Options

Option A — always emit (simplest, safest):

fmt.Fprintf(&configBuilder, ",\n              \"startupTimeout\": %d", options.GatewayConfig.StartupTimeout)

Option B — make the invariant explicit with a comment:

// StartupTimeout is always >= DefaultMCPStartupTimeout (120s) when set via buildMCPGatewayConfig.
// The > 0 guard protects against zero-value configs in tests or future callers.
if options.GatewayConfig.StartupTimeout > 0 {
    ...
}

The renderer test already covers StartupTimeout == 0 (omits the field), so the current contract is tested. The concern is whether future callers might accidentally pass zero expecting 120s behaviour.

@copilot please address this.

@pelikhan

Copy link
Copy Markdown
Collaborator

@copilot run pr-finisher skill

@gh-aw-bot

Copy link
Copy Markdown
Collaborator

@copilot please run the pr-finisher skill, address any remaining review feedback, and rerun checks once the branch is up to date.

Warning

Firewall blocked 1 domain

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

  • pi.dev

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

network:
  allowed:
    - defaults
    - "pi.dev"

See Network Configuration for more information.

Generated by 👨‍🍳 PR Sous Chef · 31.2 AIC · ⌖ 6.13 AIC · ⊞ 6.9K ·
Comment /souschef to run again

@pelikhan

Copy link
Copy Markdown
Collaborator

/smoke-copilot

@github-actions

github-actions Bot commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

📰 BREAKING: Smoke Copilot is now investigating this issue comment. Sources say the story is developing...

@pelikhan
pelikhan merged commit 8b820ae into main Jul 20, 2026
27 checks passed
@pelikhan
pelikhan deleted the copilot/fix-safeoutputs-backend-issue branch July 20, 2026 17:25
Copilot stopped work on behalf of pelikhan due to an error July 20, 2026 17:25
@github-actions

Copy link
Copy Markdown
Contributor

Comment Memory

Bot swing stone keys
Tests grunt under cold moon light
Green fire maybe dawn

Note

This comment is managed by comment memory.

It stores persistent context for this thread in the code block at the top of this comment.
Edit only the text inside the backtick fences; workflow metadata and the footer are regenerated automatically.

Learn more about comment memory

📰 BREAKING: Report filed by Smoke Copilot · 74.8 AIC · ⌖ 7.29 AIC · ⊞ 19K ·
Comment /smoke-copilot to run again
Add label smoke to run again

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

Smoke review done. Tools grunt okay.

📰 BREAKING: Report filed by Smoke Copilot · 74.8 AIC · ⌖ 7.29 AIC · ⊞ 19K
Comment /smoke-copilot to run again
Add label smoke to run again

@github-actions

Copy link
Copy Markdown
Contributor

🎉 This pull request is included in a new release.

Release: v0.82.14

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.

Safeoutputs backend starts after gateway timeout, remains at zero tools, and workflow reports success

4 participants