Skip to content

Static enclave write-sink references missing lockdown step when tools.github is false #60336

Description

@lpcox

Summary

gh-aw v0.89.6 still emits an invalid MCP Gateway configuration for a static GitHub agent enclave when primary-agent GitHub tools are explicitly disabled with tools.github: false.

The recent fixes in #59816 and #59878 corrected the original static-enclave guard, startup, deferred-tool, and information-budget problems tracked in #59826. However, the newly generated static-enclave safeoutputs write-sink policy exposes a related dangling-reference bug: the compiler emits GH_AW_SINK_VISIBILITY from steps.determine-automatic-lockdown.outputs.visibility even though that step is deliberately not generated for this configuration.

At runtime the expression resolves to an empty string, producing:

"sink-visibility": ""

MCP Gateway v0.4.20 rejects the configuration before the agent or enclave starts.

Confirmed reproduction

Repository: https://github.com/githubnext/gh-aw-enclave-demo-public

Source workflow: https://github.com/githubnext/gh-aw-enclave-demo-public/blob/060f47ae89fd93185090a34dfa2065d964fdebf4/.github/workflows/roadmap-triage-enclave.md

Generated workflow: https://github.com/githubnext/gh-aw-enclave-demo-public/blob/060f47ae89fd93185090a34dfa2065d964fdebf4/.github/workflows/roadmap-triage-enclave.lock.yml

Failed run: https://github.com/githubnext/gh-aw-enclave-demo-public/actions/runs/34650415396

The workflow was recompiled from current source with the verified v0.89.6 prerelease binary in githubnext/gh-aw-enclave-demo-public#43.

Relevant source configuration:

tools:
  github: false
enclaves:
  - repos:
      - repo: githubnext/gh-aw-enclave-demo-private
        sensitivity: confidential
safe-outputs:
  add-comment:
    max: 1

The generated Start MCP Gateway environment contains:

GH_AW_SINK_VISIBILITY: ${{ steps.determine-automatic-lockdown.outputs.visibility }}

The generated safeoutputs policy contains:

"guard-policies": {
  "write-sink": {
    "accept": [
      "private:githubnext/gh-aw-enclave-demo-private"
    ],
    "sink-visibility": "${GH_AW_SINK_VISIBILITY}"
  }
}

There is no step with id: determine-automatic-lockdown in this workflow. The run confirms GH_AW_SINK_VISIBILITY is empty and fails in job agent, step Start MCP Gateway:

Location: /mcpServers/safeoutputs/guard-policies/write-sink/sink-visibility
Error: value must be one of 'public', 'private', 'internal'
Details: Invalid value - allowed values: public, private, internal
...
failed to load config: Configuration validation error (MCP Gateway version: v0.4.20)
...
Process completed with exit code 1

The agent command and enclave never start. The later detection, safe-output, and conclusion jobs succeed only as cleanup/fallback processing.

Work completed so far

#48589 — sink visibility environment wiring

#48589 moved sink-visibility through GH_AW_SINK_VISIBILITY to avoid embedding a GitHub Actions expression inside the MCP config heredoc. This established the current relationship:

determine-automatic-lockdown output
  -> GH_AW_SINK_VISIBILITY
  -> safeoutputs.write-sink.sink-visibility

That was correct for workflows where the primary GitHub tool caused the lockdown step to be generated, but it left an assumption that every emitted sink-visibility value has that producer step.

#57973 — enclave GitHub delegation

#57973 added shared MCP Gateway support for enclave-only GitHub access, including a dedicated enclave identity and a synthesized GitHub backend when the primary agent has tools.github: false.

This introduced an important distinction:

  • GitHub is disabled for the primary agent.
  • A GitHub backend still needs to be rendered for the enclave identity.

Some later generation paths correctly preserve this distinction; the sink-visibility environment path currently does not.

#58880 — dynamic enclave policies

#58880 added dynamic enclave repository policies and established dynamic-enclave guard/write-sink paths. Static enclaves initially remained uncovered in several related paths.

#59816 — static enclave GitHub server guard

#59816 fixed the invalid empty GitHub server allow-only policy for enclave-only delegation. It added githubLockdownDetectionStepEnabled() as the authoritative predicate for whether determine-automatic-lockdown is generated and made static enclave-only GitHub server guards derive concrete values from the enclave declaration.

This correctly fixed the previous output:

{"allow-only":{"min-integrity":"","repos":""}}

The v0.89.6 output now correctly contains concrete values such as:

{"allow-only":{"min-integrity":"none","repos":["githubnext/gh-aw-enclave-demo-private"]}}

However, the same predicate was not applied to GH_AW_SINK_VISIBILITY.

#59878 — static enclave startup fixes

#59878 addressed the remaining failures collected in #59826:

  1. Added a narrow static-enclave safeoutputs write-sink policy.
  2. Made startup connectivity checks use the enclave identity for an enclave-only GitHub backend.
  3. Refreshed deferred awf-enclave CLI tool discovery after the backend registers.
  4. Reinforced concrete static-enclave GitHub guard generation.
  5. Added disclosure-budget prompt guidance and a runtime hint for bit-budget-exhausted.

These fixes are present in v0.89.6. In particular, the GitHub server guard in the failing workflow is valid. The new failure is specifically in the safeoutputs write-sink policy added for static enclaves.

Root cause

The relevant generation paths use different definitions of “GitHub enabled.”

githubLockdownDetectionStepEnabled(workflowData) correctly uses the original workflow state, including ExplicitlyDisabledTools, and returns false for tools.github: false. Therefore the lockdown step is not generated.

During MCP setup, toolsWithEnclaveGitHubIssues() synthesizes a non-false tools["github"] entry so that the enclave-scoped GitHub backend can still be rendered. This is necessary for enclave delegation, but that reconstructed tools map must not be treated as evidence that the primary-agent lockdown step exists.

collectMCPEnvironmentVariables() correctly gates GITHUB_MCP_GUARD_MIN_INTEGRITY and GITHUB_MCP_GUARD_REPOS on githubLockdownDetectionStepEnabled(workflowData). Its sink-visibility branch instead uses the reconstructed tools map:

if githubToolEnabledInTools || enclaveDynamicRepositoryPolicyEnabled(workflowData) {
    envVars[sinkVisibilityEnvVar] = "${{ steps.determine-automatic-lockdown.outputs.visibility }}"
}

For static enclave-only delegation, githubToolEnabledInTools is true because the backend was synthesized, while githubLockdownDetectionStepEnabled(workflowData) is false. This emits a reference to a nonexistent step.

writeSinkGuardPolicy() compounds the problem by unconditionally adding:

"sink-visibility": sinkVisibilityRuntimeExpr

for the new static-enclave write-sink policy.

Why existing tests missed it

TestCompileEnclaveOnlyGitHubToolsGuardPolicy covers this exact configuration and correctly asserts that:

  • Determine automatic lockdown mode is absent;
  • GITHUB_MCP_GUARD_MIN_INTEGRITY does not reference the missing step;
  • GITHUB_MCP_GUARD_REPOS does not reference the missing step;
  • ${GH_AW_SINK_VISIBILITY} is present in the generated policy.

It does not make the equivalent assertion for the environment variable's producer:

assert.NotContains(t, lock,
    "GH_AW_SINK_VISIBILITY: ${{ steps.determine-automatic-lockdown.outputs.visibility }}")

The unit/golden tests therefore verify the consumer syntax but not that its referenced producer exists. Compile validation also passes because GitHub Actions accepts the missing-step expression and the invalid empty enum is only observed after runtime expansion by MCP Gateway.

Required fix

The compiler needs a single consistent rule for every value derived from determine-automatic-lockdown.

At minimum:

  1. Gate GH_AW_SINK_VISIBILITY on the same authoritative githubLockdownDetectionStepEnabled(workflowData) predicate used for the other lockdown outputs.
  2. Ensure static-enclave write-sink generation never emits sink-visibility unless it has a valid source.
  3. Preserve the distinction between a GitHub backend rendered for an enclave and GitHub tools enabled for the primary agent.
  4. Decide the intended secure behavior when a static enclave write-sink needs target-repository visibility but the normal lockdown step is absent:
    • generate a visibility-only/detection step for this case; or
    • derive a valid target visibility through another trusted runtime source; or
    • omit the field only if that preserves the intended DIFC policy and does not weaken private-to-public protections.

Simply substituting an empty value is invalid. Silently defaulting to a less restrictive visibility would be unsafe.

Regression coverage

Please add coverage that:

  • compiles tools.github: false + static GitHub enclave + safe outputs;
  • asserts the lockdown step and all references to its outputs are either both present or both absent;
  • validates all three related values (min_integrity, repos, and visibility) consistently;
  • validates the fully expanded MCP Gateway configuration, not only the pre-expansion YAML/heredoc;
  • confirms the enclave-only GitHub backend remains available solely to the enclave identity;
  • confirms the static write-sink retains its narrow accepted secrecy labels;
  • exercises both public and private/internal target-repository visibility behavior;
  • reruns the downstream enclave demo end-to-end.

A general generated-workflow invariant would also help: every steps.<id>.outputs.* reference should correspond to an emitted step ID in the same job.

Additional remaining work from #59826

The disclosure-budget work in #59878 improved prompt guidance and runtime error reporting, but does not appear to implement the proposed compile-time rejection or warning for example response schemas whose cardinality cannot fit the enclave's sensitivity-derived bit budget. That preventive validation remains useful, but it is separate from this startup-blocking regression.

Acceptance criteria

  • The reproduction workflow compiles without a dangling step-output reference.
  • MCP Gateway v0.4.20 accepts the expanded configuration.
  • The primary agent has no GitHub access when tools.github: false.
  • The enclave identity retains its scoped GitHub repository access.
  • The safeoutputs write-sink policy remains narrow and enforces the correct target visibility.
  • The agent, enclave invocation, and safe-output path complete end to end in the demo repository.
  • A regression test fails on v0.89.6 behavior and passes with the fix.

Required test-first reproduction and validation

The fix must be developed with a red-green regression test, not only assertions added after implementation.

Before changing production code, add a test fixture using representative frontmatter equivalent to the real githubnext/gh-aw-enclave-demo-public workflow, including:

tools:
  github: false
enclaves:
  - repos:
      - repo: githubnext/gh-aw-enclave-demo-private
        sensitivity: confidential
safe-outputs:
  add-comment:
    max: 1

The test must first be run against the current compiler and demonstrated to fail for the observed gap: the generated workflow references steps.determine-automatic-lockdown.outputs.visibility even though no step with id: determine-automatic-lockdown exists, and the expanded MCP Gateway configuration contains an invalid empty sink-visibility. This failing result should be captured in the PR description or test commit history before the production fix is applied.

After implementing the fix, run the unchanged regression test and require it to prove all of the following:

  • the generated workflow contains no dangling steps.<id>.outputs.* references;
  • the actual MCP Gateway JSON produced after GitHub Actions and environment expansion validates against the pinned MCP Gateway schema/runtime;
  • sink-visibility is one of public, private, or internal when present;
  • the primary agent still has no GitHub access with tools.github: false;
  • the enclave identity retains only its declared repository access;
  • the static write-sink retains the narrow accepted secrecy labels;
  • the concrete GitHub server guard remains valid (min-integrity is non-empty and repos is an array);
  • identity-aware startup probing, deferred enclave tool refresh, and disclosure-budget guidance from Fix static GitHub agent enclave startup #59878 remain present.

A golden-string assertion alone is insufficient. The test must exercise or faithfully reproduce runtime variable expansion and pass the resulting configuration through MCP Gateway configuration validation. Finally, the corrected compiler/setup action must be tested end to end against the demo workflow so the fix is shown to close this regression without reopening any of the original gaps from #59826.

Activity

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

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions