STET

agent_patch

runs/2026-02-28__13-38-10__gpt-5-4/flux-pr-1241/flux-pr-1241.1-of-1.2026-02-28__13-38-10__gpt-5-4/agent-logs/agent.patch

19081 bytes

Back to adjudication
diff --git a/tmp/agent-patch-flux-pr-1241.1-of-1.2026-02-28__13-38-10__gpt-5-4/app/pkg/astparser/parser.go b/app/pkg/astparser/parser.go
index 0847a97..71c83a4 100644
--- a/tmp/agent-patch-flux-pr-1241.1-of-1.2026-02-28__13-38-10__gpt-5-4/app/pkg/astparser/parser.go
+++ b/app/pkg/astparser/parser.go
@@ -23,6 +23,12 @@ func ParseGraphqlDocumentString(input string) (ast.Document, operationreport.Rep
 	return ParseGraphqlDocumentBytes([]byte(input))
 }
 
+// ParseGraphqlDocumentStringWithOptions takes a raw GraphQL document in string format and parses it into an AST
+// while collecting parser stats and optionally enforcing parser limits.
+func ParseGraphqlDocumentStringWithOptions(input string, options ParseOptions) (ast.Document, ParseStats, operationreport.Report) {
+	return ParseGraphqlDocumentBytesWithOptions([]byte(input), options)
+}
+
 // ParseGraphqlDocumentBytes takes a raw GraphQL document in byte slice format and parses it into an AST.
 // This function creates a new parser as well as a new AST for every call.
 // Therefore you shouldn't use this function in a hot path.
@@ -36,6 +42,27 @@ func ParseGraphqlDocumentBytes(input []byte) (ast.Document, operationreport.Repo
 	return doc, report
 }
 
+// ParseGraphqlDocumentBytesWithOptions takes a raw GraphQL document in byte slice format and parses it into an AST
+// while collecting parser stats and optionally enforcing parser limits.
+func ParseGraphqlDocumentBytesWithOptions(input []byte, options ParseOptions) (ast.Document, ParseStats, operationreport.Report) {
+	parser := NewParser()
+	doc := *ast.NewDocument()
+	doc.Input.ResetInputBytes(input)
+	report := operationreport.Report{}
+	stats := parser.ParseWithOptions(&doc, &report, options)
+	return doc, stats, report
+}
+
+type ParseOptions struct {
+	MaxDepth      int
+	MaxFieldCount int
+}
+
+type ParseStats struct {
+	Depth      int
+	FieldCount int
+}
+
 // Parser takes a raw input and turns it into an AST
 // use NewParser() to create a parser
 // Don't create new parsers in the hot path, re-use them.
@@ -45,6 +72,10 @@ type Parser struct {
 	tokenizer            *Tokenizer
 	shouldIndex          bool
 	reportInternalErrors bool
+	options              ParseOptions
+	stats                ParseStats
+	currentDepth         int
+	currentRootMaxDepth  int
 }
 
 // NewParser returns a new parser with all values properly initialized
@@ -65,16 +96,88 @@ func (p *Parser) PrepareImport(document *ast.Document, report *operationreport.R
 
 // Parse parses all input in a Document.Input into the Document
 func (p *Parser) Parse(document *ast.Document, report *operationreport.Report) {
+	_ = p.ParseWithOptions(document, report, ParseOptions{})
+}
+
+// ParseWithOptions parses all input in a Document.Input into the Document while collecting parser stats
+// and enforcing optional parser limits.
+func (p *Parser) ParseWithOptions(document *ast.Document, report *operationreport.Report, options ParseOptions) ParseStats {
 	p.document = document
 	p.report = report
+	p.options = options
+	p.stats = ParseStats{}
+	p.currentDepth = 0
+	p.currentRootMaxDepth = 0
 	p.tokenize()
-	p.parse()
+	if !p.report.HasErrors() {
+		p.parse()
+	}
+	stats := p.stats
+	if p.report.HasErrors() && p.currentRootMaxDepth > 0 {
+		stats.Depth += p.currentRootMaxDepth
+	}
+	p.options = ParseOptions{}
+	return stats
 }
 
 func (p *Parser) tokenize() {
 	p.tokenizer.Tokenize(&p.document.Input)
 }
 
+func (p *Parser) beginExecutableDefinition() {
+	p.currentDepth = 0
+	p.currentRootMaxDepth = 0
+}
+
+func (p *Parser) finishExecutableDefinition() {
+	p.stats.Depth += p.currentRootMaxDepth
+	p.currentDepth = 0
+	p.currentRootMaxDepth = 0
+}
+
+func (p *Parser) abortExecutableDefinition() {
+	p.currentDepth = 0
+	p.currentRootMaxDepth = 0
+}
+
+func (p *Parser) enterSelectionSet(pos position.Position) {
+	p.currentDepth++
+	if p.currentDepth <= p.currentRootMaxDepth {
+		return
+	}
+	p.currentRootMaxDepth = p.currentDepth
+	if p.options.MaxDepth > 0 && p.stats.Depth+p.currentRootMaxDepth > p.options.MaxDepth {
+		p.report.AddExternalError(operationreport.ExternalError{
+			Message: fmt.Sprintf(
+				"parser depth limit exceeded: depth %d exceeds max depth %d",
+				p.stats.Depth+p.currentRootMaxDepth,
+				p.options.MaxDepth,
+			),
+			Locations: operationreport.LocationsFromPosition(pos),
+		})
+	}
+}
+
+func (p *Parser) leaveSelectionSet() {
+	if p.currentDepth > 0 {
+		p.currentDepth--
+	}
+}
+
+func (p *Parser) countField(pos position.Position) {
+	p.stats.FieldCount++
+	if p.options.MaxFieldCount > 0 && p.stats.FieldCount > p.options.MaxFieldCount {
+		p.report.AddExternalError(operationreport.ExternalError{
+			Message: fmt.Sprintf(
+				"parser field limit exceeded: field count %d exceeds max field count %d",
+				p.stats.FieldCount,
+				p.options.MaxFieldCount,
+			),
+			Locations: operationreport.LocationsFromPosition(pos),
+		})
+	}
+}
+
 func (p *Parser) parse() {
 	for {
 		key, literalReference := p.peekLiteral()
@@ -1277,6 +1380,8 @@ func (p *Parser) parseSelectionSet() (int, bool) {
 	set.SelectionRefs = p.document.Refs[p.document.NextRefIndex()][:0]
 	lbraceToken := p.mustRead(keyword.LBRACE)
 	set.LBrace = lbraceToken.TextPosition
+	p.enterSelectionSet(lbraceToken.TextPosition)
+	defer p.leaveSelectionSet()
 
 	for {
 		switch p.peek() {
@@ -1374,6 +1479,10 @@ func (p *Parser) parseField() int {
 		field.Name = firstToken.Literal
 	}
 	field.Position = firstToken.TextPosition
+	p.countField(field.Position)
+	if p.report.HasErrors() {
+		return ast.InvalidRef
+	}
 
 	if p.peekEquals(keyword.LPAREN) {
 		field.Arguments = p.parseArgumentList()
@@ -1454,7 +1563,12 @@ func (p *Parser) parseOperationDefinition() {
 		}
 	case keyword.LBRACE:
 		operationDefinition.OperationType = ast.OperationTypeQuery
+		p.beginExecutableDefinition()
 		operationDefinition.SelectionSet, operationDefinition.HasSelections = p.parseSelectionSet()
+		if p.report.HasErrors() {
+			return
+		}
+		p.finishExecutableDefinition()
 		p.document.OperationDefinitions = append(p.document.OperationDefinitions, operationDefinition)
 		ref := len(p.document.OperationDefinitions) - 1
 		rootNode := ast.Node{
@@ -1480,7 +1594,12 @@ func (p *Parser) parseOperationDefinition() {
 		operationDefinition.HasDirectives = len(operationDefinition.Directives.Refs) > 0
 	}
 
+	p.beginExecutableDefinition()
 	operationDefinition.SelectionSet, operationDefinition.HasSelections = p.parseSelectionSet()
+	if p.report.HasErrors() {
+		return
+	}
+	p.finishExecutableDefinition()
 
 	p.document.OperationDefinitions = append(p.document.OperationDefinitions, operationDefinition)
 	ref := len(p.document.OperationDefinitions) - 1
@@ -1560,7 +1679,12 @@ func (p *Parser) parseFragmentDefinition() {
 		fragmentDefinition.Directives = p.parseDirectiveList()
 		fragmentDefinition.HasDirectives = len(fragmentDefinition.Directives.Refs) > 0
 	}
+	p.beginExecutableDefinition()
 	fragmentDefinition.SelectionSet, fragmentDefinition.HasSelections = p.parseSelectionSet()
+	if p.report.HasErrors() {
+		return
+	}
+	p.finishExecutableDefinition()
 	p.document.FragmentDefinitions = append(p.document.FragmentDefinitions, fragmentDefinition)
 
 	ref := len(p.document.FragmentDefinitions) - 1
diff --git a/tmp/agent-patch-flux-pr-1241.1-of-1.2026-02-28__13-38-10__gpt-5-4/app/pkg/astparser/parser_test.go b/app/pkg/astparser/parser_test.go
index bd17e57..f8e57d9 100644
--- a/tmp/agent-patch-flux-pr-1241.1-of-1.2026-02-28__13-38-10__gpt-5-4/app/pkg/astparser/parser_test.go
+++ b/app/pkg/astparser/parser_test.go
@@ -11,6 +11,7 @@ import (
 	"github.com/wundergraph/graphql-go-tools/pkg/operationreport"
 
 	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
 )
 
 func TestParser_Parse(t *testing.T) {
@@ -2266,6 +2267,68 @@ func TestErrorReport(t *testing.T) {
 	})
 }
 
+func TestParseGraphqlDocumentWithOptionsStats(t *testing.T) {
+	doc, stats, report := ParseGraphqlDocumentStringWithOptions(`
+		query One {
+			user {
+				id
+			}
+			...Frag
+		}
+
+		fragment Frag on Query {
+			account {
+				name
+			}
+		}
+
+		query Two {
+			version
+		}
+	`, ParseOptions{})
+
+	require.False(t, report.HasErrors(), report.Error())
+	require.Equal(t, 5, stats.Depth)
+	require.Equal(t, 5, stats.FieldCount)
+	require.Len(t, doc.OperationDefinitions, 2)
+	require.Len(t, doc.FragmentDefinitions, 1)
+}
+
+func TestParseGraphqlDocumentWithOptionsDepthLimit(t *testing.T) {
+	_, stats, report := ParseGraphqlDocumentStringWithOptions(`
+		query {
+			user {
+				id
+			}
+		}
+	`, ParseOptions{MaxDepth: 1})
+
+	require.True(t, report.HasErrors())
+	require.Equal(t, 2, stats.Depth)
+	require.Equal(t, 2, stats.FieldCount)
+	require.Len(t, report.ExternalErrors, 1)
+	assert.Contains(t, report.ExternalErrors[0].Message, "depth limit exceeded")
+	assert.Contains(t, report.ExternalErrors[0].Message, "max depth 1")
+}
+
+func TestParseGraphqlDocumentWithOptionsFieldLimit(t *testing.T) {
+	_, stats, report := ParseGraphqlDocumentStringWithOptions(`
+		query {
+			me {
+				id
+			}
+			version
+		}
+	`, ParseOptions{MaxFieldCount: 2})
+
+	require.True(t, report.HasErrors())
+	require.Equal(t, 2, stats.Depth)
+	require.Equal(t, 3, stats.FieldCount)
+	require.Len(t, report.ExternalErrors, 1)
+	assert.Contains(t, report.ExternalErrors[0].Message, "field limit exceeded")
+	assert.Contains(t, report.ExternalErrors[0].Message, "max field count 2")
+}
+
 func TestParseStarwars(t *testing.T) {
 
 	starWarsSchema, err := os.ReadFile("./testdata/starwars.schema.graphql")
diff --git a/tmp/agent-patch-flux-pr-1241.1-of-1.2026-02-28__13-38-10__gpt-5-4/app/v2/pkg/astparser/parser.go b/app/v2/pkg/astparser/parser.go
index b950b4a..0e5b338 100644
--- a/tmp/agent-patch-flux-pr-1241.1-of-1.2026-02-28__13-38-10__gpt-5-4/app/v2/pkg/astparser/parser.go
+++ b/app/v2/pkg/astparser/parser.go
@@ -44,6 +44,18 @@ func ParseGraphqlDocumentString(input string) (ast.Document, operationreport.Rep
 	return *doc, report
 }
 
+// ParseGraphqlDocumentStringWithOptions takes a raw GraphQL document in string format and parses it into an AST
+// while collecting parser stats and optionally enforcing parser limits.
+func ParseGraphqlDocumentStringWithOptions(input string, options ParseOptions) (ast.Document, ParseStats, operationreport.Report) {
+	parser := getParser()
+	defer releaseParser(parser)
+	doc := ast.NewSmallDocument()
+	doc.Input.ResetInputString(input)
+	report := operationreport.Report{}
+	stats := parser.ParseWithOptions(doc, &report, options)
+	return *doc, stats, report
+}
+
 // ParseGraphqlDocumentBytes takes a raw GraphQL document in byte slice format and parses it into an AST.
 // This function creates a new parser as well as a new AST for every call.
 // Therefore you shouldn't use this function in a hot path.
@@ -58,6 +70,28 @@ func ParseGraphqlDocumentBytes(input []byte) (ast.Document, operationreport.Repo
 	return *doc, report
 }
 
+// ParseGraphqlDocumentBytesWithOptions takes a raw GraphQL document in byte slice format and parses it into an AST
+// while collecting parser stats and optionally enforcing parser limits.
+func ParseGraphqlDocumentBytesWithOptions(input []byte, options ParseOptions) (ast.Document, ParseStats, operationreport.Report) {
+	parser := getParser()
+	defer releaseParser(parser)
+	doc := ast.NewSmallDocument()
+	doc.Input.ResetInputBytes(input)
+	report := operationreport.Report{}
+	stats := parser.ParseWithOptions(doc, &report, options)
+	return *doc, stats, report
+}
+
+type ParseOptions struct {
+	MaxDepth      int
+	MaxFieldCount int
+}
+
+type ParseStats struct {
+	Depth      int
+	FieldCount int
+}
+
 // Parser takes a raw input and turns it into an AST
 // use NewParser() to create a parser
 // Don't create new parsers in the hot path, re-use them.
@@ -67,6 +101,10 @@ type Parser struct {
 	tokenizer            *Tokenizer
 	shouldIndex          bool
 	reportInternalErrors bool
+	options              ParseOptions
+	stats                ParseStats
+	currentDepth         int
+	currentRootMaxDepth  int
 }
 
 // NewParser returns a new parser with all values properly initialized
@@ -87,16 +125,88 @@ func (p *Parser) PrepareImport(document *ast.Document, report *operationreport.R
 
 // Parse parses all input in a Document.Input into the Document
 func (p *Parser) Parse(document *ast.Document, report *operationreport.Report) {
+	_ = p.ParseWithOptions(document, report, ParseOptions{})
+}
+
+// ParseWithOptions parses all input in a Document.Input into the Document while collecting parser stats
+// and enforcing optional parser limits.
+func (p *Parser) ParseWithOptions(document *ast.Document, report *operationreport.Report, options ParseOptions) ParseStats {
 	p.document = document
 	p.report = report
+	p.options = options
+	p.stats = ParseStats{}
+	p.currentDepth = 0
+	p.currentRootMaxDepth = 0
 	p.tokenize()
-	p.parse()
+	if !p.report.HasErrors() {
+		p.parse()
+	}
+	stats := p.stats
+	if p.report.HasErrors() && p.currentRootMaxDepth > 0 {
+		stats.Depth += p.currentRootMaxDepth
+	}
+	p.options = ParseOptions{}
+	return stats
 }
 
 func (p *Parser) tokenize() {
 	p.tokenizer.Tokenize(&p.document.Input)
 }
 
+func (p *Parser) beginExecutableDefinition() {
+	p.currentDepth = 0
+	p.currentRootMaxDepth = 0
+}
+
+func (p *Parser) finishExecutableDefinition() {
+	p.stats.Depth += p.currentRootMaxDepth
+	p.currentDepth = 0
+	p.currentRootMaxDepth = 0
+}
+
+func (p *Parser) abortExecutableDefinition() {
+	p.currentDepth = 0
+	p.currentRootMaxDepth = 0
+}
+
+func (p *Parser) enterSelectionSet(pos position.Position) {
+	p.currentDepth++
+	if p.currentDepth <= p.currentRootMaxDepth {
+		return
+	}
+	p.currentRootMaxDepth = p.currentDepth
+	if p.options.MaxDepth > 0 && p.stats.Depth+p.currentRootMaxDepth > p.options.MaxDepth {
+		p.report.AddExternalError(operationreport.ExternalError{
+			Message: fmt.Sprintf(
+				"parser depth limit exceeded: depth %d exceeds max depth %d",
+				p.stats.Depth+p.currentRootMaxDepth,
+				p.options.MaxDepth,
+			),
+			Locations: operationreport.LocationsFromPosition(pos),
+		})
+	}
+}
+
+func (p *Parser) leaveSelectionSet() {
+	if p.currentDepth > 0 {
+		p.currentDepth--
+	}
+}
+
+func (p *Parser) countField(pos position.Position) {
+	p.stats.FieldCount++
+	if p.options.MaxFieldCount > 0 && p.stats.FieldCount > p.options.MaxFieldCount {
+		p.report.AddExternalError(operationreport.ExternalError{
+			Message: fmt.Sprintf(
+				"parser field limit exceeded: field count %d exceeds max field count %d",
+				p.stats.FieldCount,
+				p.options.MaxFieldCount,
+			),
+			Locations: operationreport.LocationsFromPosition(pos),
+		})
+	}
+}
+
 func (p *Parser) parse() {
 	for {
 		key, literalReference := p.peekLiteral()
@@ -1303,6 +1413,8 @@ func (p *Parser) parseSelectionSet() (int, bool) {
 	set.SelectionRefs = p.document.Refs[p.document.NextRefIndex()][:0]
 	lbraceToken := p.mustRead(keyword.LBRACE)
 	set.LBrace = lbraceToken.TextPosition
+	p.enterSelectionSet(lbraceToken.TextPosition)
+	defer p.leaveSelectionSet()
 
 	for {
 		switch p.peek() {
@@ -1401,6 +1513,10 @@ func (p *Parser) parseField() int {
 		field.Name = firstToken.Literal
 	}
 	field.Position = firstToken.TextPosition
+	p.countField(field.Position)
+	if p.report.HasErrors() {
+		return ast.InvalidRef
+	}
 
 	if p.peekEquals(keyword.LPAREN) {
 		field.Arguments = p.parseArgumentList()
@@ -1481,7 +1597,12 @@ func (p *Parser) parseOperationDefinition() {
 		}
 	case keyword.LBRACE:
 		operationDefinition.OperationType = ast.OperationTypeQuery
+		p.beginExecutableDefinition()
 		operationDefinition.SelectionSet, operationDefinition.HasSelections = p.parseSelectionSet()
+		if p.report.HasErrors() {
+			return
+		}
+		p.finishExecutableDefinition()
 		p.document.OperationDefinitions = append(p.document.OperationDefinitions, operationDefinition)
 		ref := len(p.document.OperationDefinitions) - 1
 		rootNode := ast.Node{
@@ -1507,7 +1628,12 @@ func (p *Parser) parseOperationDefinition() {
 		operationDefinition.HasDirectives = len(operationDefinition.Directives.Refs) > 0
 	}
 
+	p.beginExecutableDefinition()
 	operationDefinition.SelectionSet, operationDefinition.HasSelections = p.parseSelectionSet()
+	if p.report.HasErrors() {
+		return
+	}
+	p.finishExecutableDefinition()
 
 	p.document.OperationDefinitions = append(p.document.OperationDefinitions, operationDefinition)
 	ref := len(p.document.OperationDefinitions) - 1
@@ -1587,7 +1713,12 @@ func (p *Parser) parseFragmentDefinition() {
 		fragmentDefinition.Directives = p.parseDirectiveList()
 		fragmentDefinition.HasDirectives = len(fragmentDefinition.Directives.Refs) > 0
 	}
+	p.beginExecutableDefinition()
 	fragmentDefinition.SelectionSet, fragmentDefinition.HasSelections = p.parseSelectionSet()
+	if p.report.HasErrors() {
+		return
+	}
+	p.finishExecutableDefinition()
 	p.document.FragmentDefinitions = append(p.document.FragmentDefinitions, fragmentDefinition)
 
 	ref := len(p.document.FragmentDefinitions) - 1
diff --git a/tmp/agent-patch-flux-pr-1241.1-of-1.2026-02-28__13-38-10__gpt-5-4/app/v2/pkg/astparser/parser_test.go b/app/v2/pkg/astparser/parser_test.go
index d651adb..c52e850 100644
--- a/tmp/agent-patch-flux-pr-1241.1-of-1.2026-02-28__13-38-10__gpt-5-4/app/v2/pkg/astparser/parser_test.go
+++ b/app/v2/pkg/astparser/parser_test.go
@@ -2601,6 +2601,68 @@ func TestErrorReport(t *testing.T) {
 	})
 }
 
+func TestParseGraphqlDocumentWithOptionsStats(t *testing.T) {
+	doc, stats, report := ParseGraphqlDocumentStringWithOptions(`
+		query One {
+			user {
+				id
+			}
+			...Frag
+		}
+
+		fragment Frag on Query {
+			account {
+				name
+			}
+		}
+
+		query Two {
+			version
+		}
+	`, ParseOptions{})
+
+	require.False(t, report.HasErrors(), report.Error())
+	require.Equal(t, 5, stats.Depth)
+	require.Equal(t, 5, stats.FieldCount)
+	require.Len(t, doc.OperationDefinitions, 2)
+	require.Len(t, doc.FragmentDefinitions, 1)
+}
+
+func TestParseGraphqlDocumentWithOptionsDepthLimit(t *testing.T) {
+	_, stats, report := ParseGraphqlDocumentStringWithOptions(`
+		query {
+			user {
+				id
+			}
+		}
+	`, ParseOptions{MaxDepth: 1})
+
+	require.True(t, report.HasErrors())
+	require.Equal(t, 2, stats.Depth)
+	require.Equal(t, 2, stats.FieldCount)
+	require.Len(t, report.ExternalErrors, 1)
+	assert.Contains(t, report.ExternalErrors[0].Message, "depth limit exceeded")
+	assert.Contains(t, report.ExternalErrors[0].Message, "max depth 1")
+}
+
+func TestParseGraphqlDocumentWithOptionsFieldLimit(t *testing.T) {
+	_, stats, report := ParseGraphqlDocumentStringWithOptions(`
+		query {
+			me {
+				id
+			}
+			version
+		}
+	`, ParseOptions{MaxFieldCount: 2})
+
+	require.True(t, report.HasErrors())
+	require.Equal(t, 2, stats.Depth)
+	require.Equal(t, 3, stats.FieldCount)
+	require.Len(t, report.ExternalErrors, 1)
+	assert.Contains(t, report.ExternalErrors[0].Message, "field limit exceeded")
+	assert.Contains(t, report.ExternalErrors[0].Message, "max field count 2")
+}
+
 func TestParseStarwars(t *testing.T) {
 
 	starWarsSchema, err := os.ReadFile("./testdata/starwars.schema.graphql")