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
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")