Skip to content

fix(enclaves): encode dynamic enclave max_identity_ttl in nanoseconds - #59290

Merged
lpcox merged 2 commits into
mainfrom
lpcox-fix-enclave-ttl-nanoseconds
Sep 7, 2026
Merged

fix(enclaves): encode dynamic enclave max_identity_ttl in nanoseconds#59290
lpcox merged 2 commits into
mainfrom
lpcox-fix-enclave-ttl-nanoseconds

Conversation

@lpcox

@lpcox lpcox commented Sep 7, 2026

Copy link
Copy Markdown
Collaborator

Fixes #59258.

The bug

buildMCPGatewayDelegationEnvelope in pkg/workflow/enclaves.go emitted:

"max_identity_ttl": enclave.Timeout,

enclave.Timeout is an integer number of seconds (see EnclaveConfig.Timeout, and buildDynamicEnclaveExpiryScript which computes $(date -u +%s) + <Timeout>). But mcpg v0.4.17 decodes max_identity_ttl into a Go time.Duration, whose JSON representation is an integer number of nanoseconds (gh-aw-mcpg internal/delegation/envelope.go: MaxIdentityTTL time.Duration \json:"max_identity_ttl"``).

A configured 120-second enclave therefore installed a 120-nanosecond identity ceiling. mcpg's Store.validateAgainstEnvelope then rejected every realistic AWF create-or-confirm request with 403 delegation_request_denied, making the merged dynamic enclave runtime in github/gh-aw-firewall#8276 non-functional in production. AWF already sends the contract-correct nanosecond value (secondsToGoDurationNanos in src/enclave/delegation-control-client.ts), so gh-aw was the mismatched side.

Before / after wire value

For a workflow with enclaves[].timeout: 120, the envelope embedded in MCP_GATEWAY_DELEGATION_ENVELOPE:

max_identity_ttl
Before 120
After 120000000000

What changed

  • buildMCPGatewayDelegationEnvelope now stores time.Duration(enclave.Timeout) * time.Second in max_identity_ttl, so encoding/json serializes the exact nanosecond integer mcpg expects.
  • validateDynamicEnclaveBounds now rejects enclaves[].timeout > 4740 (new maxDynamicEnclaveTimeoutSeconds). This matches gh-aw-firewall's MAX_ENCLAVE_TIMEOUT_SECONDS preflight limit and the awf-config schema, so gh-aw and AWF cannot disagree about what compiles. It also keeps the time.Duration(int) * time.Second multiplication comfortably inside int64.
  • The runtime envelope expiry clamp (expires_at, MCP_GATEWAY_DELEGATION_EXPIRES_AT, buildDynamicEnclaveExpiryScript) is a separate contract and is deliberately unchanged; it remains in seconds / RFC3339.

Tests

  • TestBuildMCPGatewayDelegationEnvelopeMaxIdentityTTLNanoseconds (new) pins the cross-component contract: it round-trips the emitted JSON through a struct mirroring mcpg v0.4.17's Envelope.MaxIdentityTTL time.Duration, asserts the exact 120000000000 wire value, and confirms an AWF-requested 120 s TTL is accepted while a 121 s one exceeds the ceiling — exactly the comparison Store.validateAgainstEnvelope performs.
  • TestValidateDynamicEnclaveBoundsRejectsOversizedTimeout (new) locks the new upper bound.
  • TestDynamicEnclaveGatewayContract now asserts the exact \"max_identity_ttl\":120000000000 substring in the generated shell and explicitly rejects the pre-fix \"max_identity_ttl\":120, to prevent a units regression.

Recompile

grep -rl max_identity_ttl .github/workflows/ returns nothing — no workflow currently compiles a dynamic enclave envelope — so no .lock.yml regeneration is required.

Validation

  • go build ./... clean
  • go vet ./pkg/workflow/... clean
  • go test ./pkg/workflow/... — all enclave / delegation tests pass. One unrelated pre-existing failure remains (TestGeneratePackageLock_UsesNormalizedWorkflowDir, a macOS /private/var vs /var symlink mismatch that reproduces on main).

Unblocks github/gh-aw-firewall#8195 and the merged github/gh-aw-firewall#8276.

buildMCPGatewayDelegationEnvelope emitted "max_identity_ttl": enclave.Timeout,
where enclave.Timeout is an integer number of seconds. mcpg v0.4.17 decodes
max_identity_ttl into a Go time.Duration, whose JSON representation is an
integer number of nanoseconds. A configured 120-second enclave therefore
installed a 120-nanosecond identity ceiling, so mcpg's
Store.validateAgainstEnvelope rejected every realistic AWF create-or-confirm
request with 403 delegation_request_denied. AWF already sends nanoseconds
(secondsToGoDurationNanos in src/enclave/delegation-control-client.ts), so
gh-aw was the mismatched side.

Encode the value as time.Duration(enclave.Timeout) * time.Second so
encoding/json emits the exact nanosecond integer mcpg expects. Before:
"max_identity_ttl":120. After: "max_identity_ttl":120000000000.

The runtime envelope expiry clamp (expires_at,
MCP_GATEWAY_DELEGATION_EXPIRES_AT, buildDynamicEnclaveExpiryScript) is a
separate contract and remains in seconds / RFC3339, unchanged.

Also bound enclaves[].timeout for dynamic enclaves at
maxDynamicEnclaveTimeoutSeconds = 4740, matching gh-aw-firewall's
MAX_ENCLAVE_TIMEOUT_SECONDS preflight and the awf-config schema. Previously
gh-aw only enforced Timeout > 0, so gh-aw and AWF could disagree about what
compiled, and time.Duration(enclave.Timeout) * time.Second could theoretically
overflow int64 for pathological values. The new bound also keeps the
multiplication trivially inside int64.

Tests added:
- TestBuildMCPGatewayDelegationEnvelopeMaxIdentityTTLNanoseconds pins the
  units contract by round-tripping the emitted JSON through a struct
  mirroring mcpg's Envelope.MaxIdentityTTL time.Duration, and by asserting
  that a 120-second AWF-requested TTL is accepted while a 121-second one is
  not - exactly the comparison mcpg's Store.validateAgainstEnvelope performs.
- TestValidateDynamicEnclaveBoundsRejectsOversizedTimeout locks the new
  upper bound.
- TestDynamicEnclaveGatewayContract now asserts the exact
  "max_identity_ttl":120000000000 wire value and explicitly rejects the
  pre-fix 120 to prevent a units regression.

No .lock.yml files reference max_identity_ttl, so no workflows need
recompilation.

Fixes #59258

Unblocks github/gh-aw-firewall#8195 and the merged
github/gh-aw-firewall#8276.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot AI balanced review requested due to automatic review settings September 7, 2026 19:23
The new maxDynamicEnclaveTimeoutSeconds comment split the const block's
alignment group, so gofmt rewrapped the two following assignments. CI's
lint-go-format caught it. Whitespace only; no behaviour change.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

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.

Copilot review overview

🟢 Approval recommended

The implementation matches the external contracts and includes focused regression coverage.

Review tier: Balanced
Findings: None

What changed in this PR

Fixes dynamic enclave delegation TTL serialization to match mcpg’s nanosecond-based time.Duration contract.

Changes:

  • Serializes enclave timeouts as nanoseconds.
  • Enforces the 4,740-second dynamic enclave limit.
  • Adds regression and boundary tests.
File Description
pkg/​workflow/​enclaves.go Corrects TTL encoding and validates timeout bounds.
pkg/​workflow/​enclaves_test.go Verifies wire encoding, TTL comparisons, and limits.

💡 Add a code-review agent skill for context-aware, tailored reviews. Learn more in the docs.

@lpcox
lpcox merged commit e4a47e9 into main Sep 7, 2026
32 checks passed
@lpcox
lpcox deleted the lpcox-fix-enclave-ttl-nanoseconds branch September 7, 2026 19:41
@pelikhan

pelikhan commented Sep 7, 2026

Copy link
Copy Markdown
Collaborator

@lpcox can we use seconds in user configurations and perform the conversion during parsing

@github-actions

github-actions Bot commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

🎉 This pull request is included in a new release.

Release: v0.88.6

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.

Encode dynamic enclave identity TTL in nanoseconds

3 participants