Skip to content

Conversation

@robindiddams
Copy link
Member

@robindiddams robindiddams commented Jan 22, 2026

Summary

  • Move PEM bundle parsing into GenerateCertificate to reduce duplicate parsing work
  • Identify cert vs CA by checking x509 IsCA field (order-independent)
  • Identify private key by PEM block type (PRIVATE KEY, RSA PRIVATE KEY, EC PRIVATE KEY)
  • Add --private-key flag with support for:
    • Direct PEM string
    • Base64-encoded PEM
    • File path (containing PEM or base64-encoded PEM)

Changes

  • internal/stack/stack.go: GenerateCertificate now accepts bundle string and parses internally
  • cmd/start.go: Added --private-key flag and loadPrivateKeyPEM helper

Summary by CodeRabbit

  • New Features

    • Added --private-key support (PEM string, base64 PEM, or file path); --token is deprecated and disallowed.
    • Proxy now restarts automatically and gracefully when TLS certificates/configuration change.
    • Environment-aware CA selection (dev vs prod) for TLS validation.
  • Chores

    • Updated Go toolchain to 1.25.5 and bumped go-common dependency.

✏️ Tip: You can customize this high-level summary in your review settings.

- Move PEM bundle parsing into GenerateCertificate to reduce duplicate work
- Identify cert vs CA by checking x509 IsCA field (order-independent)
- Identify private key by PEM block type (PRIVATE KEY, RSA/EC PRIVATE KEY)
- Add --private-key flag with support for PEM string, base64, or file path
- Add loadPrivateKeyPEM helper for flexible key input handling
@coderabbitai
Copy link

coderabbitai bot commented Jan 22, 2026

Caution

Review failed

The pull request is closed.

📝 Walkthrough

Walkthrough

Adds ECDSA private-key input (PEM/file/base64) and deprecates the token flag; moves TLS certificate provisioning into a provider onConnect callback that manages proxy server lifecycle with mutexed swaps; updates AgentMetadata and CreateNetworkProvider signatures; adds PEM bundle parsing and Gravity CA selection; bumps module/tooling versions.

Changes

Cohort / File(s) Summary
Dependency and toolchain
go.mod
Bumps Go toolchain to 1.25.5, updates github.com/agentuity/go-common, and removes two local replace directives.
CLI: private-key parsing & lifecycle
cmd/start.go
Adds required --private-key flag (accepts PEM string, base64, or file path) via new unexported loadPrivateKeyPEM; parses/validates ECDSA private key and stores on agent metadata; deprecates --token (fatal if used); moves provisioning/proxy startup into provider callback and introduces mutexed server lifecycle management; retains health-check wiring.
Stack: TLS, PEM bundle parsing, CA selection
internal/stack/stack.go, internal/stack/certs.go
Adds AgentMetadata.PrivateKey *ecdsa.PrivateKey; changes GenerateCertificate to accept a PEM bundle string and adds parsePEMBundle to extract cert, CA, and private key; builds TLS config from parsed PEMs; adds GravityCACertificate and isDevelopmentGravityURL helpers and PEM constants.
Provider / Gravity client integration
internal/stack/gravityClient.go, internal/stack/stack.go
CreateNetworkProvider signature now accepts onConnect func(*provider.Configuration) error and drops the ProvisionResponse parameter; GravityClient gains an onConnect field; Configure now invokes onConnect(&config) after signaling connection to drive certificate/proxy lifecycle.
🚥 Pre-merge checks | ✅ 1
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


📜 Recent review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7a9d625 and 1707953.

📒 Files selected for processing (1)
  • cmd/start.go

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.


Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
internal/stack/gravityClient.go (1)

15-27: Guard against nil onConnect to avoid a panic.

If GravityClient is instantiated without onConnect, Configure will nil-deref. Add a nil check or default no-op handler.

🐛 Proposed fix
 func (p *GravityClient) Configure(config provider.Configuration) error {
 	p.logger.Debug("configuring provider")
 	p.config = config
 	p.Connected <- struct{}{}
-	return p.onConnect(&config)
+	if p.onConnect == nil {
+		return nil
+	}
+	return p.onConnect(&config)
 }
cmd/start.go (1)

65-88: Add support for PKCS#8 EC keys (BEGIN PRIVATE KEY) to prevent failures with valid keys.

The current code uses only x509.ParseECPrivateKey, which handles SEC1 EC keys (PEM label "EC PRIVATE KEY") but fails on PKCS#8 keys (PEM label "PRIVATE KEY"), even though both are valid EC private key formats. Use block.Type to distinguish and handle both formats:

Suggested fix
 import (
 	"context"
+	"crypto/ecdsa"
 	"crypto/x509"
 	"encoding/base64"
 	"encoding/pem"
@@
 	block, _ := pem.Decode(privateKeyPEM)
 	if block == nil {
 		logger.Fatal("no PEM block found in --private-key")
 	}
-	privateKey, err := x509.ParseECPrivateKey(block.Bytes)
-	if err != nil {
-		logger.Fatal("failed to parse private key: %v", err)
-	}
+	var privateKey *ecdsa.PrivateKey
+	switch block.Type {
+	case "EC PRIVATE KEY":
+		privateKey, err = x509.ParseECPrivateKey(block.Bytes)
+	case "PRIVATE KEY":
+		var k interface{}
+		k, err = x509.ParsePKCS8PrivateKey(block.Bytes)
+		if err == nil {
+			var ok bool
+			privateKey, ok = k.(*ecdsa.PrivateKey)
+			if !ok {
+				err = fmt.Errorf("private key is not ECDSA")
+			}
+		}
+	default:
+		err = fmt.Errorf("unsupported private key type: %s", block.Type)
+	}
+	if err != nil {
+		logger.Fatal("failed to parse private key: %v", err)
+	}
🤖 Fix all issues with AI agents
In `@cmd/start.go`:
- Around line 118-135: The shutdown call can dereference a nil server if the
server was never started; ensure every place that calls server.Shutdown first
checks server != nil. In the anonymous onConnect callback passed into
stack.CreateNetworkProvider (func(c *provider.Configuration) error) and in any
later cleanup/shutdown path that uses the captured server variable, wrap the
Shutdown call with a nil guard (if server != nil { if err :=
server.Shutdown(ctx); err != nil { logger.Error(...) } server = nil }). This
uses the existing server variable, provider.Configuration callback, and
stack.StartServer/stack.GenerateCertificate symbols to locate and fix the spots.
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1df34a0 and b532a5c.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (4)
  • cmd/start.go
  • go.mod
  • internal/stack/gravityClient.go
  • internal/stack/stack.go
🧰 Additional context used
🧬 Code graph analysis (2)
cmd/start.go (1)
internal/stack/stack.go (2)
  • AgentMetadata (38-43)
  • CreateNetworkProvider (301-385)
internal/stack/stack.go (1)
internal/stack/gravityClient.go (1)
  • GravityClient (15-21)
🔇 Additional comments (9)
internal/stack/stack.go (5)

3-22: Import updates align with the new key/provider wiring.


38-43: PrivateKey addition in AgentMetadata fits the new auth flow.


87-163: Bundle parsing and config assembly look coherent.


301-372: onConnect wiring and client config updates look consistent.


403-454: PEM bundle parsing logic is clear and defensive.

cmd/start.go (3)

3-19: Import additions look aligned with the new private-key and server flow.


234-239: Required private-key flag addition is good.


241-276: Input handling in the key-loading helper is clean and readable.

go.mod (1)

3-6: Verify go-common v1.0.142 availability and compatibility with current APIs.

Go 1.25.5 was released December 2, 2025 and is available in standard toolchains. However, go-common v1.0.142 is an internal module; verify this version is published in your module proxy/registry and confirm compatibility with any API changes in your codebase.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@cmd/start.go`:
- Around line 127-143: The server variable is accessed concurrently
(created/assigned in the provider callback and read/closed in shutdown), causing
a data race; introduce a sync.Mutex (or sync.RWMutex) next to the server
declaration and use it to guard all reads/writes to server inside the
CreateNetworkProvider callback (the anonymous func(c *provider.Configuration)
error) and the shutdown path where server is checked/Shutdown/cleared,
performing a safe swap: lock, capture current server into local, set shared
server to nil, unlock, then perform Shutdown on the captured instance; apply the
same mutex protection to the other server access site referenced in the review.
- Around line 76-91: The error log currently prints the raw privateKeyPEM on
parse failure (in the block handling x509.ParsePKCS8PrivateKey /
x509.ParseECPrivateKey), leaking secrets; modify the failure path to remove the
privateKeyPEM from the logger.Fatal call and instead log a non-sensitive
indicator (e.g., error only, or a fingerprint/length) when
loadPrivateKeyPEM/privateKeyPEM parsing fails; update the logger.Fatal
invocation(s) near x509.ParsePKCS8PrivateKey / x509.ParseECPrivateKey to exclude
string(privateKeyPEM) and optionally include only safe metadata such as
len(privateKeyPEM) or a hash digest.
🧹 Nitpick comments (3)
internal/stack/certs.go (1)

49-69: Optional: accept URLs without a scheme to avoid false prod selection.
If callers pass a host without a scheme (e.g., devmode.agentuity.com:443), url.Parse yields an empty hostname and you’ll default to prod CA. A lightweight fallback keeps this resilient.

♻️ Suggested tweak
-	parsedURL, err := url.Parse(apiURL)
-	if err != nil {
-		return false
-	}
+	parsedURL, err := url.Parse(apiURL)
+	if err == nil && parsedURL.Hostname() == "" {
+		parsedURL, err = url.Parse("https://" + apiURL)
+	}
+	if err != nil {
+		return false
+	}
cmd/start.go (1)

257-288: Optional: surface file read errors before base64 fallback.
If os.ReadFile fails due to permissions or IO errors, the current path treats the input as base64 and loses the real cause.

♻️ Suggested tweak
-	// Check if it's a file path
-	if data, err := os.ReadFile(input); err == nil {
+	// Check if it's a file path
+	if data, err := os.ReadFile(input); err == nil {
 		if strings.HasPrefix(strings.TrimSpace(string(data)), "-----BEGIN") {
 			return data, nil
 		}
 		// File contains base64
 		decoded, err := base64.StdEncoding.DecodeString(strings.TrimSpace(string(data)))
 		if err == nil && strings.HasPrefix(string(decoded), "-----BEGIN") {
 			return decoded, nil
 		}
 		return nil, fmt.Errorf("file does not contain valid PEM or base64-encoded PEM")
+	} else if !os.IsNotExist(err) {
+		return nil, fmt.Errorf("failed to read private key file: %w", err)
 	}
internal/stack/stack.go (1)

87-129: Optional: decode CA PEM before parsing for logging.
x509.ParseCertificate expects DER bytes; decoding the PEM first avoids the guaranteed failure and simplifies the logging block.

♻️ Suggested tweak
-	// Parse and log CA certificate details
-	caCert, err := x509.ParseCertificate(caCertPEM)
-	if err == nil {
-		logger.Debug("Loaded CA certificate: CN=%s, Issuer=%s", caCert.Subject.CommonName, caCert.Issuer.CommonName)
-	} else {
-		// Try parsing as PEM
-		block, _ := pem.Decode(caCertPEM)
-		if block != nil {
-			caCert, err = x509.ParseCertificate(block.Bytes)
-			if err == nil {
-				logger.Debug("Loaded CA certificate (PEM): CN=%s, Issuer=%s", caCert.Subject.CommonName, caCert.Issuer.CommonName)
-			}
-		}
-	}
+	// Parse and log CA certificate details
+	if block, _ := pem.Decode(caCertPEM); block != nil {
+		if caCert, err := x509.ParseCertificate(block.Bytes); err == nil {
+			logger.Debug("Loaded CA certificate: CN=%s, Issuer=%s", caCert.Subject.CommonName, caCert.Issuer.CommonName)
+		} else {
+			logger.Warn("Failed to parse CA certificate for logging: %v", err)
+		}
+	}
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b532a5c and 7110ef1.

📒 Files selected for processing (3)
  • cmd/start.go
  • internal/stack/certs.go
  • internal/stack/stack.go
🧰 Additional context used
🧬 Code graph analysis (2)
cmd/start.go (1)
internal/stack/stack.go (2)
  • AgentMetadata (38-43)
  • CreateNetworkProvider (301-386)
internal/stack/stack.go (2)
internal/stack/gravityClient.go (1)
  • GravityClient (15-21)
internal/stack/certs.go (1)
  • GravityCACertificate (38-47)
🔇 Additional comments (6)
internal/stack/certs.go (2)

10-35: CA PEM constants look good.
Embedded prod/dev CA PEMs are fine here.


37-47: LGTM for environment-based CA selection.
Clear, small, and easy to follow.

cmd/start.go (1)

246-250: Required --private-key flag wiring looks good.
Clear CLI contract and help text.

internal/stack/stack.go (3)

38-43: AgentMetadata private key field addition looks good.
Matches the new auth flow.


301-355: Provider wiring with onConnect + ECDSA key looks consistent.
The new config surface aligns with the updated Gravity client flow.


404-455: PEM bundle parsing and CA detection looks solid.
The block-type scan + IsCA detection is straightforward and robust for the two-cert bundle assumption.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

@robindiddams robindiddams merged commit 6f56940 into main Jan 23, 2026
@robindiddams robindiddams deleted the feat/parse-pem-bundle-and-private-key branch January 23, 2026 19:35
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Fix all issues with AI agents
In `@cmd/start.go`:
- Around line 67-75: Change the unclear deprecation log for the token flag: when
reading token via flags.GetString("token") and detecting token != "", update the
logger.Fatal message (the code around maybePrivateKey and flags) to explicitly
instruct users to stop using --token/AGENTUITY_SDK_KEY and to use --private-key
(or the private key env/flag) instead — reference the token variable and the
logger.Fatal call and replace the generic "update your Agentuity CLI" text with
a clear, actionable message that says remove --token/AGENTUITY_SDK_KEY and
supply the private key via --private-key.
- Around line 82-109: The current logic decodes only the first PEM block from
privateKeyPEM and fails if that block isn't a key; update the parsing near
pem.Decode(privateKeyPEM) to iterate with pem.Decode in a loop over the
remaining bytes and select a block whose block.Type contains "PRIVATE KEY" (or
ends with "PRIVATE KEY") before attempting x509.ParsePKCS8PrivateKey /
x509.ParseECPrivateKey; for each candidate block try PKCS#8 then EC parsing (as
the existing fallback does) and only break/assign privateKey when parsing
succeeds, otherwise continue to the next PEM block; if no private-key block
parses successfully, call logger.Fatal with an informative message (include
len(privateKeyPEM) as currently done).
- Around line 133-155: The shutdown of oldServer in the provider reconfigure
callback currently calls oldServer.Shutdown(ctx) with the long-lived root
context and can block indefinitely; change that call to use a bounded timeout
context (e.g., via context.WithTimeout(parentCtx, timeout)) so the shutdown
completes or aborts within a fixed grace period, ensure you call cancel() and
handle/log Shutdown errors including context deadline exceeded, and keep the
rest of the callback (GenerateCertificate, StartServer, and assignment to server
under serverMu) unchanged; update the closure where oldServer is set and
Shutdown is invoked (the variables server, serverMu, oldServer in the
CreateNetworkProvider callback).
🧹 Nitpick comments (1)
cmd/start.go (1)

269-305: Consider accepting unpadded base64 for better ergonomics.
Some base64 encoders omit padding; falling back to RawStdEncoding improves interoperability. This is a nice-to-have robustness tweak.

♻️ Optional base64 fallback
-		decoded, err := base64.StdEncoding.DecodeString(strings.TrimSpace(string(data)))
+		cleaned := strings.TrimSpace(string(data))
+		decoded, err := base64.StdEncoding.DecodeString(cleaned)
+		if err != nil {
+			decoded, err = base64.RawStdEncoding.DecodeString(cleaned)
+		}
 		if err == nil && strings.HasPrefix(string(decoded), "-----BEGIN") {
 			return decoded, nil
 		}
@@
-	decoded, err := base64.StdEncoding.DecodeString(strings.TrimSpace(input))
+	cleaned := strings.TrimSpace(input)
+	decoded, err := base64.StdEncoding.DecodeString(cleaned)
+	if err != nil {
+		decoded, err = base64.RawStdEncoding.DecodeString(cleaned)
+	}
 	if err != nil {
 		return nil, fmt.Errorf("input is not valid PEM, base64, or file path: %w", err)
 	}
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7cb93ff and 7a9d625.

📒 Files selected for processing (1)
  • cmd/start.go
🧰 Additional context used
🧬 Code graph analysis (1)
cmd/start.go (2)
internal/utils/utils.go (1)
  • GetPrivateIPv4 (8-20)
internal/stack/stack.go (4)
  • AgentMetadata (38-43)
  • CreateNetworkProvider (301-386)
  • GenerateCertificate (87-163)
  • StartServer (165-238)
🔇 Additional comments (3)
cmd/start.go (3)

5-19: No review note for the import additions.


210-217: No additional notes on the shutdown block.


262-267: No review note for the new flag wiring.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Comment on lines +67 to +75
maybePrivateKey, _ := flags.GetString("private-key")
endpointID, _ := flags.GetString("endpoint-id")
gravityUrl, _ := flags.GetString("url")
healthCheck, _ := flags.GetBool("health-check")
token, _ := flags.GetString("token")

if token != "" {
logger.Fatal("The --token flag is no longer supported. Please update your Agentuity CLI to the latest version.")
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Make the --token deprecation message actionable.
The current fatal message implies “update CLI,” but the fix is to remove --token/AGENTUITY_SDK_KEY and use --private-key. Consider updating the text to reduce user confusion.

💡 Suggested wording tweak
-		if token != "" {
-			logger.Fatal("The --token flag is no longer supported. Please update your Agentuity CLI to the latest version.")
-		}
+		if token != "" {
+			logger.Fatal("The --token flag is no longer supported. Remove --token/AGENTUITY_SDK_KEY and use --private-key instead.")
+		}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
maybePrivateKey, _ := flags.GetString("private-key")
endpointID, _ := flags.GetString("endpoint-id")
gravityUrl, _ := flags.GetString("url")
healthCheck, _ := flags.GetBool("health-check")
token, _ := flags.GetString("token")
if token != "" {
logger.Fatal("The --token flag is no longer supported. Please update your Agentuity CLI to the latest version.")
}
maybePrivateKey, _ := flags.GetString("private-key")
endpointID, _ := flags.GetString("endpoint-id")
gravityUrl, _ := flags.GetString("url")
healthCheck, _ := flags.GetBool("health-check")
token, _ := flags.GetString("token")
if token != "" {
logger.Fatal("The --token flag is no longer supported. Remove --token/AGENTUITY_SDK_KEY and use --private-key instead.")
}
🤖 Prompt for AI Agents
In `@cmd/start.go` around lines 67 - 75, Change the unclear deprecation log for
the token flag: when reading token via flags.GetString("token") and detecting
token != "", update the logger.Fatal message (the code around maybePrivateKey
and flags) to explicitly instruct users to stop using --token/AGENTUITY_SDK_KEY
and to use --private-key (or the private key env/flag) instead — reference the
token variable and the logger.Fatal call and replace the generic "update your
Agentuity CLI" text with a clear, actionable message that says remove
--token/AGENTUITY_SDK_KEY and supply the private key via --private-key.

Comment on lines +82 to 109
privateKeyPEM, err := loadPrivateKeyPEM(maybePrivateKey)
if err != nil {
logger.Fatal("failed to load private key: %v", err)
}

block, _ := pem.Decode(privateKeyPEM)
if block == nil {
logger.Fatal("no PEM block found in --private-key")
}
key, err := x509.ParsePKCS8PrivateKey(block.Bytes)
if err != nil {
// Try SEC 1/EC format as fallback
key, err = x509.ParseECPrivateKey(block.Bytes)
if err != nil {
logger.Fatal("failed to parse private key: %v (pem length: %d)", err, len(privateKeyPEM))
}
}
privateKey, ok := key.(*ecdsa.PrivateKey)
if !ok {
logger.Fatal("private key is not an ECDSA key")
}

agent := stack.AgentMetadata{
OrgID: orgID,
ProjectID: projectID,
InstanceID: endpointID,
PrivateKey: privateKey,
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# First, let's find and read the file cmd/start.go
find . -name "cmd" -type d | head -5
find . -name "start.go" -type f | head -5

Repository: agentuity/gravity

Length of output: 81


🏁 Script executed:

#!/bin/bash
# Read the specific lines in question (82-109)
if [ -f "cmd/start.go" ]; then
  echo "=== cmd/start.go lines 75-115 ===" 
  sed -n '75,115p' "cmd/start.go" | cat -n
else
  echo "File not found, searching..."
  find . -name "start.go" -type f -exec wc -l {} \; -exec echo "File: {}" \;
fi

Repository: agentuity/gravity

Length of output: 1535


🏁 Script executed:

#!/bin/bash
# Also check if there are any tests or examples showing how privateKeyPEM is used
rg -A 5 -B 5 "pem.Decode" --type go

Repository: agentuity/gravity

Length of output: 1754


🏁 Script executed:

#!/bin/bash
# Find the loadPrivateKeyPEM function
rg -A 15 "func loadPrivateKeyPEM" cmd/start.go

Repository: agentuity/gravity

Length of output: 524


🏁 Script executed:

#!/bin/bash
# Check for tests or examples that show input format
find . -name "*test*.go" -type f -exec rg -l "privateKeyPEM\|loadPrivateKeyPEM" {} \;

Repository: agentuity/gravity

Length of output: 43


🏁 Script executed:

#!/bin/bash
# Check Go's crypto/x509 documentation via looking at how EC PARAMETERS blocks work
# EC PARAMETERS are sometimes generated in openssl EC key output
# Let's see if there are any examples in the codebase
rg -i "EC PARAMETERS|ec parameters" .

Repository: agentuity/gravity

Length of output: 43


🏁 Script executed:

#!/bin/bash
# Get the full loadPrivateKeyPEM function
rg -A 30 "func loadPrivateKeyPEM" cmd/start.go

Repository: agentuity/gravity

Length of output: 1116


🏁 Script executed:

#!/bin/bash
# Look at the parsePEMBundle implementation in stack.go to see the pattern
rg -A 25 "func parsePEMBundle" internal/stack/stack.go

Repository: agentuity/gravity

Length of output: 665


🏁 Script executed:

#!/bin/bash
# Check Go's crypto documentation online to confirm pem.Decode behavior
# Let's also look for any comments about key format expectations
rg -B 5 -A 5 "PKCS8\|ParseEC" cmd/start.go

Repository: agentuity/gravity

Length of output: 43


Loop through PEM blocks to find a private key block instead of parsing the first block unconditionally.

PEM files may contain EC PARAMETERS blocks, certificates, or other data before the private key. The current code calls pem.Decode() once and attempts to parse whatever block it returns, which fails if that block isn't a key. Search for a block matching "PRIVATE KEY" to ensure valid keys aren't rejected.

This pattern is already established in parsePEMBundle() in internal/stack/stack.go, which iterates through blocks and checks block.Type.

Suggested fix
-		block, _ := pem.Decode(privateKeyPEM)
-		if block == nil {
-			logger.Fatal("no PEM block found in --private-key")
-		}
+		var block *pem.Block
+		rest := privateKeyPEM
+		for {
+			var b *pem.Block
+			b, rest = pem.Decode(rest)
+			if b == nil {
+				break
+			}
+			if strings.Contains(b.Type, "PRIVATE KEY") {
+				block = b
+				break
+			}
+		}
+		if block == nil {
+			logger.Fatal("no private key PEM block found in --private-key")
+		}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
privateKeyPEM, err := loadPrivateKeyPEM(maybePrivateKey)
if err != nil {
logger.Fatal("failed to load private key: %v", err)
}
block, _ := pem.Decode(privateKeyPEM)
if block == nil {
logger.Fatal("no PEM block found in --private-key")
}
key, err := x509.ParsePKCS8PrivateKey(block.Bytes)
if err != nil {
// Try SEC 1/EC format as fallback
key, err = x509.ParseECPrivateKey(block.Bytes)
if err != nil {
logger.Fatal("failed to parse private key: %v (pem length: %d)", err, len(privateKeyPEM))
}
}
privateKey, ok := key.(*ecdsa.PrivateKey)
if !ok {
logger.Fatal("private key is not an ECDSA key")
}
agent := stack.AgentMetadata{
OrgID: orgID,
ProjectID: projectID,
InstanceID: endpointID,
PrivateKey: privateKey,
}
privateKeyPEM, err := loadPrivateKeyPEM(maybePrivateKey)
if err != nil {
logger.Fatal("failed to load private key: %v", err)
}
var block *pem.Block
rest := privateKeyPEM
for {
var b *pem.Block
b, rest = pem.Decode(rest)
if b == nil {
break
}
if strings.Contains(b.Type, "PRIVATE KEY") {
block = b
break
}
}
if block == nil {
logger.Fatal("no private key PEM block found in --private-key")
}
key, err := x509.ParsePKCS8PrivateKey(block.Bytes)
if err != nil {
// Try SEC 1/EC format as fallback
key, err = x509.ParseECPrivateKey(block.Bytes)
if err != nil {
logger.Fatal("failed to parse private key: %v (pem length: %d)", err, len(privateKeyPEM))
}
}
privateKey, ok := key.(*ecdsa.PrivateKey)
if !ok {
logger.Fatal("private key is not an ECDSA key")
}
agent := stack.AgentMetadata{
OrgID: orgID,
ProjectID: projectID,
InstanceID: endpointID,
PrivateKey: privateKey,
}
🤖 Prompt for AI Agents
In `@cmd/start.go` around lines 82 - 109, The current logic decodes only the first
PEM block from privateKeyPEM and fails if that block isn't a key; update the
parsing near pem.Decode(privateKeyPEM) to iterate with pem.Decode in a loop over
the remaining bytes and select a block whose block.Type contains "PRIVATE KEY"
(or ends with "PRIVATE KEY") before attempting x509.ParsePKCS8PrivateKey /
x509.ParseECPrivateKey; for each candidate block try PKCS#8 then EC parsing (as
the existing fallback does) and only break/assign privateKey when parsing
succeeds, otherwise continue to the next PEM block; if no private-key block
parses successfully, call logger.Fatal with an informative message (include
len(privateKeyPEM) as currently done).

Comment on lines +133 to +155
var server *http.Server
var serverMu sync.Mutex
provider, client, err := stack.CreateNetworkProvider(ctx, logger, linkEP, urls, agent, func(c *provider.Configuration) error {
serverMu.Lock()
oldServer := server
server = nil
serverMu.Unlock()
if oldServer != nil {
if err := oldServer.Shutdown(ctx); err != nil {
logger.Error("failed to shutdown server: %v", err)
}
}
tlsConfig, err := stack.GenerateCertificate(ctx, logger, c.MachineCertBundle)
if err != nil {
return fmt.Errorf("failed to generate certificate: %w", err)
}
newServer, err := stack.StartServer(ctx, logger, tlsConfig, urls)
if err != nil {
return fmt.Errorf("failed to start server: %w", err)
}
serverMu.Lock()
server = newServer
serverMu.Unlock()
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

❓ Verification inconclusive

Script executed:

git ls-files cmd/ | head -20

Repository: agentuity/gravity


Repository: agentuity/gravity
Exit code: 0

stdout:

cmd/start.go

Script executed:

cat -n cmd/start.go | sed -n '120,170p'

Repository: agentuity/gravity


Repository: agentuity/gravity
Exit code: 0

stdout:

   120				LocalPort: localPort,
   121				ProxyPort: proxyPort,
   122				URL:       gravityUrl,
   123				Version:   version,
   124			}
   125	
   126			netStack, linkEP, err := stack.CreateNetworkStack(logger, urls)
   127			if err != nil {
   128				logger.Fatal("failed to create network stack: %v", err)
   129			}
   130			defer netStack.Close()
   131			defer linkEP.Close()
   132	
   133			var server *http.Server
   134			var serverMu sync.Mutex
   135			provider, client, err := stack.CreateNetworkProvider(ctx, logger, linkEP, urls, agent, func(c *provider.Configuration) error {
   136				serverMu.Lock()
   137				oldServer := server
   138				server = nil
   139				serverMu.Unlock()
   140				if oldServer != nil {
   141					if err := oldServer.Shutdown(ctx); err != nil {
   142						logger.Error("failed to shutdown server: %v", err)
   143					}
   144				}
   145				tlsConfig, err := stack.GenerateCertificate(ctx, logger, c.MachineCertBundle)
   146				if err != nil {
   147					return fmt.Errorf("failed to generate certificate: %w", err)
   148				}
   149				newServer, err := stack.StartServer(ctx, logger, tlsConfig, urls)
   150				if err != nil {
   151					return fmt.Errorf("failed to start server: %w", err)
   152				}
   153				serverMu.Lock()
   154				server = newServer
   155				serverMu.Unlock()
   156				return nil
   157			})
   158			if err != nil {
   159				logger.Fatal("failed to create network provider: %v", err)
   160			}
   161	
   162			// Wait for provider connection
   163			select {
   164			case <-ctx.Done():
   165				logger.Fatal("context done: %v", ctx.Err())
   166			case <-time.After(time.Second * 10):
   167				logger.Error("timed out waiting for provider connection")
   168				os.Exit(1)
   169			case <-provider.Connected:
   170				logger.Debug("✅ Connected to Gravity! Proxy is ready.")

Script executed:

cat -n cmd/start.go | sed -n '1,140p' | grep -E "(func |ctx\s*:=|ctx\s*=)"

Repository: agentuity/gravity


Repository: agentuity/gravity
Exit code: 1


Script executed:

cat -n cmd/start.go | head -80

Repository: agentuity/gravity


Repository: agentuity/gravity
Exit code: 0

stdout:

     1	package cmd
     2	
     3	import (
     4		"context"
     5		"crypto/ecdsa"
     6		"crypto/x509"
     7		"encoding/base64"
     8		"encoding/pem"
     9		"fmt"
    10		"net/http"
    11		"os"
    12		"os/signal"
    13		"runtime/debug"
    14		"strings"
    15		"sync"
    16		"syscall"
    17		"time"
    18	
    19		"github.com/agentuity/go-common/gravity/provider"
    20		_logger "github.com/agentuity/go-common/logger"
    21		cnet "github.com/agentuity/go-common/network"
    22		csys "github.com/agentuity/go-common/sys"
    23		"github.com/agentuity/gravity/internal/heartbeat"
    24		"github.com/agentuity/gravity/internal/stack"
    25		"github.com/agentuity/gravity/internal/utils"
    26		"github.com/spf13/cobra"
    27	)
    28	
    29	var version string = "dev"
    30	
    31	var rootCmd = &cobra.Command{
    32		Use:   "gravity",
    33		Short: "Run the gravity client",
    34		Args:  cobra.NoArgs,
    35		Run: func(cmd *cobra.Command, args []string) {
    36			flags := cmd.Flags()
    37	
    38			var logLevel _logger.LogLevel
    39			logLevelStr, _ := flags.GetString("log-level")
    40			if logLevelStr == "" {
    41				logLevel = _logger.GetLevelFromEnv()
    42			} else {
    43				switch logLevelStr {
    44				case "info", "INFO":
    45					logLevel = _logger.LevelInfo
    46				case "debug", "DEBUG":
    47					logLevel = _logger.LevelDebug
    48				case "warn", "WARN":
    49					logLevel = _logger.LevelWarn
    50				case "trace", "TRACE":
    51					logLevel = _logger.LevelTrace
    52				case "error", "ERROR":
    53					logLevel = _logger.LevelError
    54				default:
    55					logLevel = _logger.LevelTrace
    56				}
    57			}
    58	
    59			logger := _logger.NewConsoleLogger(logLevel)
    60	
    61			ctx, cancel := signal.NotifyContext(cmd.Context(), os.Interrupt, syscall.SIGTERM)
    62			defer cancel()
    63	
    64			localPort, _ := flags.GetInt("port")
    65			orgID, _ := flags.GetString("org-id")
    66			projectID, _ := flags.GetString("project-id")
    67			maybePrivateKey, _ := flags.GetString("private-key")
    68			endpointID, _ := flags.GetString("endpoint-id")
    69			gravityUrl, _ := flags.GetString("url")
    70			healthCheck, _ := flags.GetBool("health-check")
    71			token, _ := flags.GetString("token")
    72	
    73			if token != "" {
    74				logger.Fatal("The --token flag is no longer supported. Please update your Agentuity CLI to the latest version.")
    75			}
    76	
    77			ipv4addr, err := utils.GetPrivateIPv4()
    78			if err != nil {
    79				logger.Fatal("failed to get private IPv4: %v", err)
    80			}

Script executed:

fd -t f "\.go$" internal/stack/ | head -20

Repository: agentuity/gravity


Repository: agentuity/gravity
Exit code: 0

stdout:

internal/stack/certs.go
internal/stack/gravityClient.go
internal/stack/networkProvider.go
internal/stack/stack.go

Script executed:

rg "func CreateNetworkProvider" --type go -A 20

Repository: agentuity/gravity


Repository: agentuity/gravity
Exit code: 0

stdout:

internal/stack/stack.go:func CreateNetworkProvider(
internal/stack/stack.go-	ctx context.Context,
internal/stack/stack.go-	logger _logger.Logger,
internal/stack/stack.go-	linkEP *channel.Endpoint,
internal/stack/stack.go-	urls UrlsMetadata,
internal/stack/stack.go-	agent AgentMetadata,
internal/stack/stack.go-	onConnect func(*provider.Configuration) error,
internal/stack/stack.go-) (*GravityClient, *gravity.GravityClient, error) {
internal/stack/stack.go-
internal/stack/stack.go-	// Egress pump to send outbound packets to Gravity
internal/stack/stack.go-	var network networkProvider
internal/stack/stack.go-
internal/stack/stack.go-	go func() {
internal/stack/stack.go-		for {
internal/stack/stack.go-			select {
internal/stack/stack.go-			case <-ctx.Done():
internal/stack/stack.go-				return
internal/stack/stack.go-			default:
internal/stack/stack.go-				pkt := linkEP.ReadContext(ctx)
internal/stack/stack.go-				if pkt == nil {
internal/stack/stack.go-					continue

Script executed:

rg "onConnect" internal/stack/stack.go -B 3 -A 3

Repository: agentuity/gravity


Repository: agentuity/gravity
Exit code: 0

stdout:

	linkEP *channel.Endpoint,
	urls UrlsMetadata,
	agent AgentMetadata,
	onConnect func(*provider.Configuration) error,
) (*GravityClient, *gravity.GravityClient, error) {

	// Egress pump to send outbound packets to Gravity
--
	}()

	prov := GravityClient{
		onConnect: onConnect,
		logger:    logger,
		ep:        linkEP,
		Connected: make(chan struct{}, 1),

Script executed:

cat -n internal/stack/stack.go | wc -l

Repository: agentuity/gravity


Repository: agentuity/gravity
Exit code: 0

stdout:

455

Script executed:

cat -n internal/stack/stack.go | sed -n '50,150p'

Repository: agentuity/gravity


Repository: agentuity/gravity
Exit code: 0

stdout:

    50		Token     string
    51		URL       string
    52		Version   string
    53	}
    54	
    55	func ProvisionGravity(ctx context.Context, logger _logger.Logger, agent AgentMetadata, urls UrlsMetadata) (*proto.ProvisionResponse, error) {
    56	
    57		hostname, err := os.Hostname()
    58		if err != nil {
    59			return nil, fmt.Errorf("error getting hostname: %w", err)
    60		}
    61	
    62		requestObj := gravity.ProvisionRequest{
    63			Context:    ctx,
    64			GravityURL: urls.URL,
    65			InstanceID: agent.InstanceID,
    66			Region:     "unknown",
    67			Provider:   "other",
    68			PrivateIP:  urls.IPv4Addr,
    69			Token:      urls.Token,
    70			Hostname:   hostname,
    71			Ephemeral:  true,
    72			Capabilities: &proto.ClientCapabilities{
    73				DynamicHostname: true,
    74				// DynamicProjectRouting: "",
    75			},
    76		}
    77	
    78		logger.Debug("Provisioning gravity connection...")
    79		resp, err := gravity.Provision(requestObj)
    80		if err != nil {
    81			return nil, fmt.Errorf("failed to provision: %w", err)
    82		}
    83	
    84		return resp, nil
    85	}
    86	
    87	func GenerateCertificate(_ context.Context, logger _logger.Logger, bundle string) (*tls.Config, error) {
    88		certPEM, caCertPEM, keyPEM, err := parsePEMBundle(bundle)
    89		if err != nil {
    90			return nil, fmt.Errorf("failed to parse certificate bundle: %w", err)
    91		}
    92	
    93		// Set up TLS config
    94		cert, err := tls.X509KeyPair(certPEM, keyPEM)
    95		if err != nil {
    96			return nil, fmt.Errorf("failed to load certificate: %w", err)
    97		}
    98	
    99		// Parse the certificate to log details
   100		if len(cert.Certificate) > 0 {
   101			x509Cert, err := x509.ParseCertificate(cert.Certificate[0])
   102			if err == nil {
   103				cert.Leaf = x509Cert
   104				logger.Debug("Loaded certificate: CN=%s, SANs=%v, NotBefore=%v, NotAfter=%v",
   105					x509Cert.Subject.CommonName, x509Cert.DNSNames, x509Cert.NotBefore, x509Cert.NotAfter)
   106			} else {
   107				logger.Warn("Failed to parse certificate for logging: %v", err)
   108			}
   109		}
   110	
   111		caCertPool := x509.NewCertPool()
   112		if !caCertPool.AppendCertsFromPEM(caCertPEM) {
   113			return nil, fmt.Errorf("failed to parse CA certificate")
   114		}
   115	
   116		// Parse and log CA certificate details
   117		caCert, err := x509.ParseCertificate(caCertPEM)
   118		if err == nil {
   119			logger.Debug("Loaded CA certificate: CN=%s, Issuer=%s", caCert.Subject.CommonName, caCert.Issuer.CommonName)
   120		} else {
   121			// Try parsing as PEM
   122			block, _ := pem.Decode(caCertPEM)
   123			if block != nil {
   124				caCert, err = x509.ParseCertificate(block.Bytes)
   125				if err == nil {
   126					logger.Debug("Loaded CA certificate (PEM): CN=%s, Issuer=%s", caCert.Subject.CommonName, caCert.Issuer.CommonName)
   127				}
   128			}
   129		}
   130	
   131		tlsConfig := &tls.Config{
   132			Certificates:     []tls.Certificate{cert},
   133			RootCAs:          caCertPool,
   134			ClientCAs:        caCertPool, // Also set ClientCAs for mutual TLS
   135			ClientAuth:       tls.VerifyClientCertIfGiven,
   136			MinVersion:       tls.VersionTLS13,
   137			CurvePreferences: []tls.CurveID{tls.X25519, tls.X25519MLKEM768, tls.CurveP256},
   138			NextProtos:       []string{"h2", "http/1.1"},
   139			GetCertificate: func(hello *tls.ClientHelloInfo) (*tls.Certificate, error) {
   140				remoteAddr := "<no-remote>"
   141				if hello != nil && hello.Conn != nil {
   142					remoteAddr = hello.Conn.RemoteAddr().String()
   143				}
   144				logger.Trace("TLS GetCertificate: ServerName=%s, SupportedProtos=%v, RemoteAddr=%v",
   145					hello.ServerName, hello.SupportedProtos, remoteAddr)
   146				return &cert, nil
   147			},
   148			VerifyPeerCertificate: func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error {
   149				logger.Trace("TLS VerifyPeerCertificate: chains=%d, rawCerts=%d", len(verifiedChains), len(rawCerts))
   150				if len(rawCerts) > 0 {

Script executed:

rg "\.onConnect\(" internal/stack/ -B 5 -A 5

Repository: agentuity/gravity


Repository: agentuity/gravity
Exit code: 0

stdout:

internal/stack/gravityClient.go-
internal/stack/gravityClient.go-func (p *GravityClient) Configure(config provider.Configuration) error {
internal/stack/gravityClient.go-	p.logger.Debug("configuring provider")
internal/stack/gravityClient.go-	p.config = config
internal/stack/gravityClient.go-	p.Connected <- struct{}{}
internal/stack/gravityClient.go:	return p.onConnect(&config)
internal/stack/gravityClient.go-}
internal/stack/gravityClient.go-
internal/stack/gravityClient.go-func (p *GravityClient) Deprovision(ctx context.Context, resourceID string, reason provider.DeprovisionReason) error {
internal/stack/gravityClient.go-	return nil
internal/stack/gravityClient.go-}

Script executed:

cat -n internal/stack/gravityClient.go

Repository: agentuity/gravity


Repository: agentuity/gravity
Exit code: 0

stdout:

     1	package stack
     2	
     3	import (
     4		"context"
     5	
     6		"github.com/agentuity/go-common/gravity/proto"
     7		"github.com/agentuity/go-common/gravity/provider"
     8		"github.com/agentuity/go-common/logger"
     9		"gvisor.dev/gvisor/pkg/buffer"
    10		"gvisor.dev/gvisor/pkg/tcpip/link/channel"
    11		"gvisor.dev/gvisor/pkg/tcpip/network/ipv6"
    12		"gvisor.dev/gvisor/pkg/tcpip/stack"
    13	)
    14	
    15	type GravityClient struct {
    16		ep        *channel.Endpoint
    17		logger    logger.Logger
    18		config    provider.Configuration
    19		Connected chan struct{}
    20		onConnect func(*provider.Configuration) error
    21	}
    22	
    23	func (p *GravityClient) Configure(config provider.Configuration) error {
    24		p.logger.Debug("configuring provider")
    25		p.config = config
    26		p.Connected <- struct{}{}
    27		return p.onConnect(&config)
    28	}
    29	
    30	func (p *GravityClient) Deprovision(ctx context.Context, resourceID string, reason provider.DeprovisionReason) error {
    31		return nil
    32	}
    33	
    34	func (p *GravityClient) Resources() []*proto.ExistingDeployment {
    35		return nil
    36	}
    37	
    38	func (p *GravityClient) SetMetricsCollector(collector provider.ProjectRuntimeStatsCollector) {
    39	}
    40	
    41	func (p *GravityClient) ProcessInPacket(payload []byte) {
    42		if p.ep == nil {
    43			p.logger.Error("ProcessInPacket called but endpoint is nil")
    44			return
    45		}
    46		pkt := stack.NewPacketBuffer(stack.PacketBufferOptions{})
    47		view := buffer.NewView(len(payload))
    48		view.Write(payload)
    49		pkt.Data().AppendView(view)
    50		p.ep.InjectInbound(ipv6.ProtocolNumber, pkt)
    51		// Note: Don't call DecRef here - the netstack will handle the packet lifecycle
    52	}

Script executed:

rg "\.Configure\(" --type go | head -20

Repository: agentuity/gravity


Repository: agentuity/gravity
Exit code: 0


Script executed:

rg "provider\.Configuration" --type go -B 3 -A 3 | head -50

Repository: agentuity/gravity


Repository: agentuity/gravity
Exit code: 0

stdout:

internal/stack/gravityClient.go-type GravityClient struct {
internal/stack/gravityClient.go-	ep        *channel.Endpoint
internal/stack/gravityClient.go-	logger    logger.Logger
internal/stack/gravityClient.go:	config    provider.Configuration
internal/stack/gravityClient.go-	Connected chan struct{}
internal/stack/gravityClient.go:	onConnect func(*provider.Configuration) error
internal/stack/gravityClient.go-}
internal/stack/gravityClient.go-
internal/stack/gravityClient.go:func (p *GravityClient) Configure(config provider.Configuration) error {
internal/stack/gravityClient.go-	p.logger.Debug("configuring provider")
internal/stack/gravityClient.go-	p.config = config
internal/stack/gravityClient.go-	p.Connected <- struct{}{}
--
internal/stack/stack.go-	linkEP *channel.Endpoint,
internal/stack/stack.go-	urls UrlsMetadata,
internal/stack/stack.go-	agent AgentMetadata,
internal/stack/stack.go:	onConnect func(*provider.Configuration) error,
internal/stack/stack.go-) (*GravityClient, *gravity.GravityClient, error) {
internal/stack/stack.go-
internal/stack/stack.go-	// Egress pump to send outbound packets to Gravity
--
cmd/start.go-
cmd/start.go-		var server *http.Server
cmd/start.go-		var serverMu sync.Mutex
cmd/start.go:		provider, client, err := stack.CreateNetworkProvider(ctx, logger, linkEP, urls, agent, func(c *provider.Configuration) error {
cmd/start.go-			serverMu.Lock()
cmd/start.go-			oldServer := server
cmd/start.go-			server = nil

Script executed:

rg "func.*Shutdown" --type go -B 2 -A 5

Repository: agentuity/gravity


Repository: agentuity/gravity
Exit code: 0

stdout:

internal/heartbeat/heartbeat.go-}
internal/heartbeat/heartbeat.go-
internal/heartbeat/heartbeat.go:func (s *Server) Shutdown() {
internal/heartbeat/heartbeat.go-	s.shutdownOnce.Do(func() {
internal/heartbeat/heartbeat.go-		if s.cancel != nil {
internal/heartbeat/heartbeat.go-			s.cancel()
internal/heartbeat/heartbeat.go-		}
internal/heartbeat/heartbeat.go-		ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)

Script executed:

cat -n cmd/start.go | sed -n '135,160p'

Repository: agentuity/gravity


Repository: agentuity/gravity
Exit code: 0

stdout:

   135			provider, client, err := stack.CreateNetworkProvider(ctx, logger, linkEP, urls, agent, func(c *provider.Configuration) error {
   136				serverMu.Lock()
   137				oldServer := server
   138				server = nil
   139				serverMu.Unlock()
   140				if oldServer != nil {
   141					if err := oldServer.Shutdown(ctx); err != nil {
   142						logger.Error("failed to shutdown server: %v", err)
   143					}
   144				}
   145				tlsConfig, err := stack.GenerateCertificate(ctx, logger, c.MachineCertBundle)
   146				if err != nil {
   147					return fmt.Errorf("failed to generate certificate: %w", err)
   148				}
   149				newServer, err := stack.StartServer(ctx, logger, tlsConfig, urls)
   150				if err != nil {
   151					return fmt.Errorf("failed to start server: %w", err)
   152				}
   153				serverMu.Lock()
   154				server = newServer
   155				serverMu.Unlock()
   156				return nil
   157			})
   158			if err != nil {
   159				logger.Fatal("failed to create network provider: %v", err)
   160			}

Script executed:

web_search
http.Server.Shutdown behavior with context timeout

Repository: agentuity/gravity


Repository: agentuity/gravity
Exit code: 127

stderr:

/bin/bash: line 1: web_search: command not found
/bin/bash: line 2: http.Server.Shutdown: command not found

http.Server.Shutdown context timeout behavior golang


In Go’s net/http, (*http.Server).Shutdown(ctx) is graceful and the ctx timeout only bounds how long Shutdown will wait, not how long active requests are allowed to run.

What Shutdown does

Shutdown performs this sequence: (1) close listeners (stop accepting new connections), (2) close idle connections, (3) wait for remaining connections to become idle so they can be closed. If ctx expires first, Shutdown returns ctx.Err() (typically context.DeadlineExceeded). It also does not close/wait for hijacked connections (e.g., WebSockets). [1]

What happens when the context times out

From the actual implementation, when the context is done, Shutdown just returns ctx.Err(); it does not forcibly close active connections for you. Active handlers/connections can continue running until they finish or until something else closes them. [2]

Practical implications

  • Your in-flight request r.Context() is not automatically canceled just because Shutdown was called; Shutdown is explicitly “without interrupting any active connections.” [1][3]
  • If you need a hard cutoff after the graceful period, a common pattern is: call Shutdown(timeoutCtx), and if it returns context.DeadlineExceeded, then call srv.Close() to force-close remaining connections. (Close closes connections in StateNew, StateActive, and StateIdle.) [1]

Sources:
[1] Go net/http docs for Server.Shutdown / Server.Close (pkg.go.dev) (pkg.go.dev)
[2] Go net/http Server.Shutdown implementation showing case <-ctx.Done(): return ctx.Err() (go.googlesource.com) (go.googlesource.com)
[3] Discussion confirming Shutdown doesn’t interrupt active connections / doesn’t cancel request contexts (Stack Overflow) (stackoverflow.com)

Citations:


Use a bounded shutdown context to prevent blocking reconfiguration.

During provider reconfiguration, oldServer.Shutdown(ctx) uses the long-lived root context which can block indefinitely if a client connection is stuck. This blocks the entire onConnect callback, preventing the new server from starting and effectively halting provider reconfiguration.

Use a timeout context for shutdown to ensure the callback completes within a reasonable grace period:

⏱️ Use a bounded shutdown context
 			if oldServer != nil {
+				shutdownCtx, cancel := context.WithTimeout(ctx, 10*time.Second)
+				defer cancel()
-				if err := oldServer.Shutdown(ctx); err != nil {
+				if err := oldServer.Shutdown(shutdownCtx); err != nil {
 					logger.Error("failed to shutdown server: %v", err)
 				}
 			}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
var server *http.Server
var serverMu sync.Mutex
provider, client, err := stack.CreateNetworkProvider(ctx, logger, linkEP, urls, agent, func(c *provider.Configuration) error {
serverMu.Lock()
oldServer := server
server = nil
serverMu.Unlock()
if oldServer != nil {
if err := oldServer.Shutdown(ctx); err != nil {
logger.Error("failed to shutdown server: %v", err)
}
}
tlsConfig, err := stack.GenerateCertificate(ctx, logger, c.MachineCertBundle)
if err != nil {
return fmt.Errorf("failed to generate certificate: %w", err)
}
newServer, err := stack.StartServer(ctx, logger, tlsConfig, urls)
if err != nil {
return fmt.Errorf("failed to start server: %w", err)
}
serverMu.Lock()
server = newServer
serverMu.Unlock()
var server *http.Server
var serverMu sync.Mutex
provider, client, err := stack.CreateNetworkProvider(ctx, logger, linkEP, urls, agent, func(c *provider.Configuration) error {
serverMu.Lock()
oldServer := server
server = nil
serverMu.Unlock()
if oldServer != nil {
shutdownCtx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
if err := oldServer.Shutdown(shutdownCtx); err != nil {
logger.Error("failed to shutdown server: %v", err)
}
}
tlsConfig, err := stack.GenerateCertificate(ctx, logger, c.MachineCertBundle)
if err != nil {
return fmt.Errorf("failed to generate certificate: %w", err)
}
newServer, err := stack.StartServer(ctx, logger, tlsConfig, urls)
if err != nil {
return fmt.Errorf("failed to start server: %w", err)
}
serverMu.Lock()
server = newServer
serverMu.Unlock()
🤖 Prompt for AI Agents
In `@cmd/start.go` around lines 133 - 155, The shutdown of oldServer in the
provider reconfigure callback currently calls oldServer.Shutdown(ctx) with the
long-lived root context and can block indefinitely; change that call to use a
bounded timeout context (e.g., via context.WithTimeout(parentCtx, timeout)) so
the shutdown completes or aborts within a fixed grace period, ensure you call
cancel() and handle/log Shutdown errors including context deadline exceeded, and
keep the rest of the callback (GenerateCertificate, StartServer, and assignment
to server under serverMu) unchanged; update the closure where oldServer is set
and Shutdown is invoked (the variables server, serverMu, oldServer in the
CreateNetworkProvider callback).

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.

2 participants