Skip to content

Commit c2053db

Browse files
committed
Merge branch 'main' of github.com:e2b-dev/infra into lev-compression-final
2 parents 94beb22 + 758e726 commit c2053db

44 files changed

Lines changed: 3182 additions & 862 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.pr_agent.toml

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -4,24 +4,24 @@ publish_output_progress = false
44
verbosity_level = 1
55

66
[github_app]
7-
# Only run /review. Drops the /describe walkthrough, diagram, and file-changes summary.
8-
pr_commands = ["/review"]
7+
# Only run /agentic_review. Drops the /describe walkthrough, diagram, and file-changes summary.
8+
# /review is deprecated by qodo and removed after 2026-05-31.
9+
pr_commands = ["/agentic_review"]
910

10-
[pr_reviewer]
11-
require_score_review = false
12-
require_tests_review = false
13-
require_estimate_effort_to_review = false
14-
require_can_be_split_review = false
15-
require_security_review = false
16-
require_ticket_analysis_review = false
17-
enable_review_labels_security = false
18-
enable_review_labels_effort = false
19-
inline_code_comments = true
20-
num_code_suggestions = 3
21-
persistent_comment = true
22-
final_update_message = false
23-
enable_help_text = false
24-
extra_instructions = """
11+
[review_agent]
12+
# Style rules apply to both agents that /agentic_review runs:
13+
# - issues_user_guidelines -> Issues Agent (bugs, security, perf)
14+
# - compliance_user_guidelines -> Compliance Agent (rule checks)
15+
issues_user_guidelines = """
16+
Output style rules (strict):
17+
- Be terse. One short paragraph per finding, no preamble.
18+
- Only report concrete bugs, correctness issues, or rule violations. Do not summarize the PR.
19+
- No headers, no bullet lists, no tables, no diagrams, no code-fence wrapping unless quoting code.
20+
- No emojis. No severity badges. No tags like "Bug", "Enhancement", "Correctness", "Maintainability".
21+
- No Qodo branding, logos, dividers, or footer links.
22+
- If nothing to flag, post nothing.
23+
"""
24+
compliance_user_guidelines = """
2525
Output style rules (strict):
2626
- Be terse. One short paragraph per finding, no preamble.
2727
- Only report concrete bugs, correctness issues, or rule violations. Do not summarize the PR.

packages/api/go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ tool (
1616
)
1717

1818
require (
19+
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2
1920
github.com/bsm/redislock v0.9.4
2021
github.com/caarlos0/env/v11 v11.3.1
2122
github.com/coreos/go-oidc/v3 v3.15.0

packages/api/go.sum

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/api/internal/api/api.gen.go

Lines changed: 185 additions & 162 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/api/internal/handlers/sandbox_create.go

Lines changed: 217 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,13 @@ import (
1111
"strings"
1212
"time"
1313

14+
"github.com/asaskevich/govalidator"
1415
"github.com/gin-gonic/gin"
1516
"github.com/google/uuid"
1617
"github.com/launchdarkly/go-sdk-common/v3/ldcontext"
1718
"go.opentelemetry.io/otel/attribute"
1819
"go.opentelemetry.io/otel/trace"
19-
"go.uber.org/zap"
20+
"golang.org/x/net/http/httpguts"
2021
"golang.org/x/net/idna"
2122

2223
"github.com/e2b-dev/infra/packages/api/internal/api"
@@ -32,7 +33,6 @@ import (
3233
"github.com/e2b-dev/infra/packages/shared/pkg/ginutils"
3334
"github.com/e2b-dev/infra/packages/shared/pkg/grpc/orchestrator"
3435
"github.com/e2b-dev/infra/packages/shared/pkg/id"
35-
"github.com/e2b-dev/infra/packages/shared/pkg/logger"
3636
sbxlogger "github.com/e2b-dev/infra/packages/shared/pkg/logger/sandbox"
3737
"github.com/e2b-dev/infra/packages/shared/pkg/middleware/otel/metrics"
3838
sandbox_network "github.com/e2b-dev/infra/packages/shared/pkg/sandbox-network"
@@ -47,6 +47,13 @@ const (
4747

4848
// Network validation error messages
4949
ErrMsgDomainsRequireBlockAll = "When specifying allowed domains in allow out, you must include 'ALL_TRAFFIC' in deny out to block all other traffic."
50+
51+
maxNetworkRuleDomains = 10
52+
maxNetworkRuleTransformsPerDomain = 1
53+
maxNetworkRuleDomainLen = 128
54+
maxNetworkRuleHeaderNameLen = 64
55+
maxNetworkRuleHeaderValueLen = 2048
56+
maxNetworkRuleHeadersPerRule = 20
5057
)
5158

5259
func (a *APIStore) PostSandboxes(c *gin.Context) {
@@ -174,7 +181,7 @@ func (a *APIStore) PostSandboxes(c *gin.Context) {
174181

175182
var network *types.SandboxNetworkConfig
176183
if n := body.Network; n != nil {
177-
if err := validateNetworkConfig(n); err != nil {
184+
if err := validateNetworkConfig(ctx, a.featureFlags, teamInfo.Team.ID, sharedUtils.DerefOrDefault(build.EnvdVersion, ""), n); err != nil {
178185
telemetry.ReportError(ctx, "invalid network config", err.Err, telemetry.WithSandboxID(sandboxID))
179186
a.sendAPIStoreError(c, err.Code, err.ClientMsg)
180187

@@ -189,6 +196,7 @@ func (a *APIStore) PostSandboxes(c *gin.Context) {
189196
Egress: &types.SandboxNetworkEgressConfig{
190197
AllowedAddresses: sharedUtils.DerefOrDefault(n.AllowOut, nil),
191198
DeniedAddresses: sharedUtils.DerefOrDefault(n.DenyOut, nil),
199+
Rules: apiRulesToDBRules(n.Rules),
192200
},
193201
}
194202

@@ -205,7 +213,7 @@ func (a *APIStore) PostSandboxes(c *gin.Context) {
205213
ctx, a.sqlcDB, a.featureFlags, teamInfo.ID, apiVolumeMounts, build,
206214
)
207215
if err != nil {
208-
if errors.Is(err, errVolumesNotSupported) {
216+
if errors.Is(err, errVolumesNotSupported) || errors.Is(err, errNoEnvdVersion) {
209217
a.sendAPIStoreError(c, http.StatusBadRequest, err.Error())
210218

211219
return
@@ -265,6 +273,19 @@ func (a *APIStore) PostSandboxes(c *gin.Context) {
265273
return
266274
}
267275

276+
if n := body.Network; n != nil && n.Rules != nil && len(*n.Rules) > 0 {
277+
domains := make([]string, 0, len(*n.Rules))
278+
for domain := range *n.Rules {
279+
domains = append(domains, domain)
280+
}
281+
282+
a.posthog.CreateAnalyticsTeamEvent(ctx, teamInfo.Team.ID.String(), "sandbox with network transform rules created",
283+
a.posthog.GetPackageToPosthogProperties(&c.Request.Header).
284+
Set("sandbox_id", sandboxID).
285+
Set("domains", domains),
286+
)
287+
}
288+
268289
c.JSON(http.StatusCreated, &sbx)
269290
}
270291

@@ -324,10 +345,35 @@ func (im InvalidVolumeMountsError) Error() string {
324345

325346
var errVolumesNotSupported = errors.New("volumes are not supported")
326347

327-
var errNoEnvdVersion = errors.New("no envd version provided")
348+
var errNetworkRulesNotSupported = errors.New("network transform rules are not supported")
349+
350+
var errNoEnvdVersion = errors.New("template must be rebuilt: envd version is not set")
351+
352+
const minEnvdVersionForNetworkRules = "0.5.13"
328353

329354
const minEnvdVersionForVolumes = "0.5.14"
330355

356+
// checkEnvdVersionRequirement returns errNoEnvdVersion when buildVersion is empty, a parse
357+
// error when the version string is invalid, or a wrapped featureErr when the build does not
358+
// meet requiredMinVersion. The caller decides how to convert the returned error into an API
359+
// response so each call-site can produce its own status code / message.
360+
func checkEnvdVersionRequirement(buildVersion, requiredMinVersion string, featureErr error) error {
361+
if buildVersion == "" {
362+
return errNoEnvdVersion
363+
}
364+
365+
ok, err := sharedUtils.IsGTEVersion(buildVersion, requiredMinVersion)
366+
if err != nil {
367+
return fmt.Errorf("invalid envd version %q: %w", buildVersion, err)
368+
}
369+
370+
if !ok {
371+
return fmt.Errorf("%w; template must be rebuilt. Template envd version is %s, must be at least %s", featureErr, buildVersion, requiredMinVersion)
372+
}
373+
374+
return nil
375+
}
376+
331377
func convertAPIVolumesToOrchestratorVolumes(ctx context.Context, sqlClient *sqlcdb.Client, featureFlags featureFlagsClient, teamID uuid.UUID, volumeMounts []api.SandboxVolumeMount, env *queries.EnvBuild) ([]*orchestrator.SandboxVolumeMount, error) {
332378
// are any volumes configured?
333379
if len(volumeMounts) == 0 {
@@ -340,16 +386,9 @@ func convertAPIVolumesToOrchestratorVolumes(ctx context.Context, sqlClient *sqlc
340386
}
341387

342388
// does your envd version support volumes?
343-
if envdVersion := sharedUtils.DerefOrDefault(env.EnvdVersion, ""); envdVersion == "" {
344-
logger.L().Warn(ctx, "envd version is unset")
345-
346-
return nil, errNoEnvdVersion
347-
} else if ok, err := sharedUtils.IsGTEVersion(envdVersion, minEnvdVersionForVolumes); err != nil {
348-
logger.L().Warn(ctx, "failed to check envd version", zap.Error(err), zap.String("envd_version", envdVersion))
349-
350-
return nil, fmt.Errorf("invalid envd version %q: %w", envdVersion, err)
351-
} else if !ok {
352-
return nil, fmt.Errorf("%w; template must be rebuilt. Template envd version is %s, must be at least %s to support volumes", errVolumesNotSupported, envdVersion, minEnvdVersionForVolumes)
389+
envdVersion := sharedUtils.DerefOrDefault(env.EnvdVersion, "")
390+
if err := checkEnvdVersionRequirement(envdVersion, minEnvdVersionForVolumes, errVolumesNotSupported); err != nil {
391+
return nil, err
353392
}
354393

355394
// get volumes from the database
@@ -514,7 +553,33 @@ func splitHostPortOptional(hostport string) (host string, port string, err error
514553
return host, port, nil
515554
}
516555

517-
func validateNetworkConfig(network *api.SandboxNetworkConfig) *api.APIError {
556+
func apiRulesToDBRules(apiRules *map[string][]api.SandboxNetworkRule) map[string][]types.SandboxNetworkRule {
557+
if apiRules == nil {
558+
return nil
559+
}
560+
561+
dbRules := make(map[string][]types.SandboxNetworkRule, len(*apiRules))
562+
for domain, rules := range *apiRules {
563+
dbDomainRules := make([]types.SandboxNetworkRule, 0, len(rules))
564+
for _, r := range rules {
565+
dbRule := types.SandboxNetworkRule{}
566+
567+
if r.Transform != nil {
568+
dbRule.Transform = &types.SandboxNetworkTransform{
569+
Headers: sharedUtils.DerefOrDefault(r.Transform.Headers, nil),
570+
}
571+
}
572+
573+
dbDomainRules = append(dbDomainRules, dbRule)
574+
}
575+
576+
dbRules[domain] = dbDomainRules
577+
}
578+
579+
return dbRules
580+
}
581+
582+
func validateNetworkConfig(ctx context.Context, featureFlags featureFlagsClient, teamID uuid.UUID, envdVersion string, network *api.SandboxNetworkConfig) *api.APIError {
518583
if network == nil {
519584
return nil
520585
}
@@ -550,7 +615,11 @@ func validateNetworkConfig(network *api.SandboxNetworkConfig) *api.APIError {
550615
denyOut := sharedUtils.DerefOrDefault(network.DenyOut, nil)
551616
allowOut := sharedUtils.DerefOrDefault(network.AllowOut, nil)
552617

553-
return validateEgressRules(allowOut, denyOut)
618+
if err := validateEgressRules(allowOut, denyOut); err != nil {
619+
return err
620+
}
621+
622+
return validateNetworkRules(ctx, featureFlags, teamID, envdVersion, network.Rules)
554623
}
555624

556625
// validateEgressRules validates egress allow/deny rules:
@@ -593,3 +662,134 @@ func validateEgressRules(allowOut, denyOut []string) *api.APIError {
593662

594663
return nil
595664
}
665+
666+
func validateNetworkRules(ctx context.Context, featureFlags featureFlagsClient, teamID uuid.UUID, envdVersion string, rules *map[string][]api.SandboxNetworkRule) *api.APIError {
667+
if rules == nil {
668+
return nil
669+
}
670+
671+
if !featureFlags.BoolFlag(ctx, featureflags.NetworkTransformRulesFlag, featureflags.TeamContext(teamID.String())) {
672+
return &api.APIError{
673+
Code: http.StatusBadRequest,
674+
Err: fmt.Errorf("team %s is not allowed to use network transform rules", teamID),
675+
ClientMsg: "Network transform rules are not available for your team.",
676+
}
677+
}
678+
679+
if err := checkEnvdVersionRequirement(envdVersion, minEnvdVersionForNetworkRules, errNetworkRulesNotSupported); err != nil {
680+
if errors.Is(err, errNetworkRulesNotSupported) || errors.Is(err, errNoEnvdVersion) {
681+
return &api.APIError{
682+
Code: http.StatusBadRequest,
683+
Err: err,
684+
ClientMsg: err.Error(),
685+
}
686+
}
687+
688+
return &api.APIError{
689+
Code: http.StatusInternalServerError,
690+
Err: err,
691+
ClientMsg: "internal error while validating network rules",
692+
}
693+
}
694+
695+
if len(*rules) > maxNetworkRuleDomains {
696+
return &api.APIError{
697+
Code: http.StatusBadRequest,
698+
Err: fmt.Errorf("too many rule domains: %d (max %d)", len(*rules), maxNetworkRuleDomains),
699+
ClientMsg: fmt.Sprintf("Network rules can have at most %d domains.", maxNetworkRuleDomains),
700+
}
701+
}
702+
703+
for domain, domainRules := range *rules {
704+
if len(domain) == 0 {
705+
return &api.APIError{
706+
Code: http.StatusBadRequest,
707+
Err: errors.New("rule domain must not be empty"),
708+
ClientMsg: "Rule domain must not be empty.",
709+
}
710+
}
711+
712+
if len(domain) > maxNetworkRuleDomainLen {
713+
return &api.APIError{
714+
Code: http.StatusBadRequest,
715+
Err: fmt.Errorf("rule domain %q exceeds max length %d", domain, maxNetworkRuleDomainLen),
716+
ClientMsg: fmt.Sprintf("Rule domain %q exceeds maximum length of %d characters.", domain, maxNetworkRuleDomainLen),
717+
}
718+
}
719+
720+
if !govalidator.IsDNSName(domain) {
721+
return &api.APIError{
722+
Code: http.StatusBadRequest,
723+
Err: fmt.Errorf("rule domain %q is not a valid domain", domain),
724+
ClientMsg: fmt.Sprintf("Rule domain %q is not a valid domain name.", domain),
725+
}
726+
}
727+
728+
if len(domainRules) > maxNetworkRuleTransformsPerDomain {
729+
return &api.APIError{
730+
Code: http.StatusBadRequest,
731+
Err: fmt.Errorf("domain %q has %d transforms (max %d)", domain, len(domainRules), maxNetworkRuleTransformsPerDomain),
732+
ClientMsg: fmt.Sprintf("Domain %q can have at most %d transform rule.", domain, maxNetworkRuleTransformsPerDomain),
733+
}
734+
}
735+
736+
for _, rule := range domainRules {
737+
if rule.Transform == nil {
738+
continue
739+
}
740+
741+
headers := sharedUtils.DerefOrDefault(rule.Transform.Headers, nil)
742+
if len(headers) > maxNetworkRuleHeadersPerRule {
743+
return &api.APIError{
744+
Code: http.StatusBadRequest,
745+
Err: fmt.Errorf("domain %q has %d headers (max %d)", domain, len(headers), maxNetworkRuleHeadersPerRule),
746+
ClientMsg: fmt.Sprintf("Domain %q can have at most %d headers per rule.", domain, maxNetworkRuleHeadersPerRule),
747+
}
748+
}
749+
750+
for name, value := range headers {
751+
if len(name) == 0 {
752+
return &api.APIError{
753+
Code: http.StatusBadRequest,
754+
Err: fmt.Errorf("header name in rule for domain %q must not be empty", domain),
755+
ClientMsg: fmt.Sprintf("Header name in rule for domain %q must not be empty.", domain),
756+
}
757+
}
758+
759+
if !httpguts.ValidHeaderFieldName(name) {
760+
return &api.APIError{
761+
Code: http.StatusBadRequest,
762+
Err: fmt.Errorf("header name %q in rule for domain %q contains invalid characters", name, domain),
763+
ClientMsg: fmt.Sprintf("Header name %q in rule for domain %q must contain only valid HTTP token characters.", name, domain),
764+
}
765+
}
766+
767+
if len(name) > maxNetworkRuleHeaderNameLen {
768+
return &api.APIError{
769+
Code: http.StatusBadRequest,
770+
Err: fmt.Errorf("header name %q in rule for domain %q exceeds max length %d", name, domain, maxNetworkRuleHeaderNameLen),
771+
ClientMsg: fmt.Sprintf("Header name %q in rule for domain %q exceeds maximum length of %d characters.", name, domain, maxNetworkRuleHeaderNameLen),
772+
}
773+
}
774+
775+
if !httpguts.ValidHeaderFieldValue(value) {
776+
return &api.APIError{
777+
Code: http.StatusBadRequest,
778+
Err: fmt.Errorf("value for header %q in rule for domain %q contains invalid characters", name, domain),
779+
ClientMsg: fmt.Sprintf("Value for header %q in rule for domain %q contains invalid characters.", name, domain),
780+
}
781+
}
782+
783+
if len(value) > maxNetworkRuleHeaderValueLen {
784+
return &api.APIError{
785+
Code: http.StatusBadRequest,
786+
Err: fmt.Errorf("value for header %q in rule for domain %q exceeds max length %d", name, domain, maxNetworkRuleHeaderValueLen),
787+
ClientMsg: fmt.Sprintf("Value for header %q in rule for domain %q exceeds maximum length of %d characters.", name, domain, maxNetworkRuleHeaderValueLen),
788+
}
789+
}
790+
}
791+
}
792+
}
793+
794+
return nil
795+
}

0 commit comments

Comments
 (0)