STET

agent_patch

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

18964 bytes

Back to adjudication
diff --git a/app/pkg/astparser/parse_options.go b/app/pkg/astparser/parse_options.go
new file mode 100644
index 0000000..2b6bfb7
--- /dev/null
+++ b/app/pkg/astparser/parse_options.go
@@ -0,0 +1,14 @@
+package astparser
+
+// ParseOptions configures optional parse-time limits.
+// A zero value means "no limit" for the corresponding field.
+type ParseOptions struct {
+	MaxDepth  int
+	MaxFields int
+}
+
+// ParseStats contains executable-document complexity metrics gathered during parsing.
+type ParseStats struct {
+	Depth      int
+	FieldCount int
+}
diff --git a/app/pkg/astparser/parse_options_test.go b/app/pkg/astparser/parse_options_test.go
new file mode 100644
index 0000000..3a57daf
--- /dev/null
+++ b/app/pkg/astparser/parse_options_test.go
@@ -0,0 +1,79 @@
+package astparser
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+)
+
+func TestParseGraphqlDocumentStringWithOptions_Stats(t *testing.T) {
+	input := `
+		query Q {
+			user {
+				id
+				...UserFields
+			}
+		}
+
+		fragment UserFields on User {
+			friends {
+				name
+			}
+		}
+	`
+
+	_, report, stats := ParseGraphqlDocumentStringWithOptions(input, ParseOptions{})
+	require.False(t, report.HasErrors(), report.Error())
+
+	assert.Equal(t, 4, stats.Depth)
+	assert.Equal(t, 4, stats.FieldCount)
+}
+
+func TestParseGraphqlDocumentStringWithOptions_MaxFields(t *testing.T) {
+	input := `
+		query Q {
+			user {
+				id
+				...UserFields
+			}
+		}
+
+		fragment UserFields on User {
+			friends {
+				name
+			}
+		}
+	`
+
+	_, report, _ := ParseGraphqlDocumentStringWithOptions(input, ParseOptions{
+		MaxFields: 3,
+	})
+	require.True(t, report.HasErrors())
+	require.NotEmpty(t, report.ExternalErrors)
+	assert.Contains(t, report.ExternalErrors[0].Message, "field limit exceeded")
+}
+
+func TestParseGraphqlDocumentStringWithOptions_MaxDepth(t *testing.T) {
+	input := `
+		query Q {
+			user {
+				id
+				...UserFields
+			}
+		}
+
+		fragment UserFields on User {
+			friends {
+				name
+			}
+		}
+	`
+
+	_, report, _ := ParseGraphqlDocumentStringWithOptions(input, ParseOptions{
+		MaxDepth: 3,
+	})
+	require.True(t, report.HasErrors())
+	require.NotEmpty(t, report.ExternalErrors)
+	assert.Contains(t, report.ExternalErrors[0].Message, "depth limit exceeded")
+}
diff --git a/tmp/agent-patch-flux-pr-1241.1-of-1.2026-02-28__13-38-10__gpt-5-3-codex/app/pkg/astparser/parser.go b/app/pkg/astparser/parser.go
index 0847a97..d087106 100644
--- a/tmp/agent-patch-flux-pr-1241.1-of-1.2026-02-28__13-38-10__gpt-5-3-codex/app/pkg/astparser/parser.go
+++ b/app/pkg/astparser/parser.go
@@ -36,6 +36,23 @@ func ParseGraphqlDocumentBytes(input []byte) (ast.Document, operationreport.Repo
 	return doc, report
 }
 
+// ParseGraphqlDocumentStringWithOptions takes a raw GraphQL document in string format and parses it into an AST.
+// In addition to the parse result, this function returns executable document statistics.
+func ParseGraphqlDocumentStringWithOptions(input string, options ParseOptions) (ast.Document, operationreport.Report, ParseStats) {
+	return ParseGraphqlDocumentBytesWithOptions([]byte(input), options)
+}
+
+// ParseGraphqlDocumentBytesWithOptions takes a raw GraphQL document in byte slice format and parses it into an AST.
+// In addition to the parse result, this function returns executable document statistics.
+func ParseGraphqlDocumentBytesWithOptions(input []byte, options ParseOptions) (ast.Document, operationreport.Report, ParseStats) {
+	parser := NewParser()
+	doc := *ast.NewDocument()
+	doc.Input.ResetInputBytes(input)
+	report := operationreport.Report{}
+	stats := parser.ParseWithOptions(&doc, &report, options)
+	return doc, report, stats
+}
+
 // 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 +62,11 @@ type Parser struct {
 	tokenizer            *Tokenizer
 	shouldIndex          bool
 	reportInternalErrors bool
+	options              ParseOptions
+	stats                ParseStats
+	currentDepth         int
+	currentMaxDepth      int
+	inExecutable         bool
 }
 
 // NewParser returns a new parser with all values properly initialized
@@ -65,10 +87,90 @@ 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.resetRuntimeState(ParseOptions{})
+	p.document = document
+	p.report = report
+	p.tokenize()
+	p.parse()
+}
+
+// ParseWithOptions parses all input in a Document.Input into the Document and enforces optional limits.
+// It returns executable-document statistics regardless of whether limits are configured.
+func (p *Parser) ParseWithOptions(document *ast.Document, report *operationreport.Report, options ParseOptions) ParseStats {
+	p.resetRuntimeState(options)
 	p.document = document
 	p.report = report
 	p.tokenize()
 	p.parse()
+	return p.stats
+}
+
+func (p *Parser) resetRuntimeState(options ParseOptions) {
+	p.options = options
+	p.stats = ParseStats{}
+	p.currentDepth = 0
+	p.currentMaxDepth = 0
+	p.inExecutable = false
+}
+
+func (p *Parser) beginExecutable() {
+	p.inExecutable = true
+	p.currentDepth = 0
+	p.currentMaxDepth = 0
+}
+
+func (p *Parser) endExecutable() {
+	if !p.inExecutable {
+		return
+	}
+	p.stats.Depth += p.currentMaxDepth
+	p.currentDepth = 0
+	p.currentMaxDepth = 0
+	p.inExecutable = false
+}
+
+func (p *Parser) enterSelectionSet(pos position.Position) {
+	if !p.inExecutable || p.report.HasErrors() {
+		return
+	}
+	p.currentDepth++
+	if p.currentDepth > p.currentMaxDepth {
+		p.currentMaxDepth = p.currentDepth
+	}
+	if p.options.MaxDepth > 0 && p.stats.Depth+p.currentMaxDepth > p.options.MaxDepth {
+		p.report.AddExternalError(operationreport.ExternalError{
+			Message: fmt.Sprintf(
+				"parser depth limit exceeded: max depth is %d, got at least %d",
+				p.options.MaxDepth,
+				p.stats.Depth+p.currentMaxDepth,
+			),
+			Locations: operationreport.LocationsFromPosition(pos),
+		})
+	}
+}
+
+func (p *Parser) leaveSelectionSet() {
+	if !p.inExecutable || p.currentDepth == 0 {
+		return
+	}
+	p.currentDepth--
+}
+
+func (p *Parser) registerField(pos position.Position) {
+	if !p.inExecutable || p.report.HasErrors() {
+		return
+	}
+	p.stats.FieldCount++
+	if p.options.MaxFields > 0 && p.stats.FieldCount > p.options.MaxFields {
+		p.report.AddExternalError(operationreport.ExternalError{
+			Message: fmt.Sprintf(
+				"parser field limit exceeded: max fields is %d, got at least %d",
+				p.options.MaxFields,
+				p.stats.FieldCount,
+			),
+			Locations: operationreport.LocationsFromPosition(pos),
+		})
+	}
 }
 
 func (p *Parser) tokenize() {
@@ -1277,6 +1379,11 @@ 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)
+	if p.report.HasErrors() {
+		return ast.InvalidRef, false
+	}
+	defer p.leaveSelectionSet()
 
 	for {
 		switch p.peek() {
@@ -1362,6 +1469,10 @@ func (p *Parser) parseField() int {
 	if firstToken.Keyword != keyword.IDENT {
 		p.errUnexpectedToken(firstToken, keyword.IDENT)
 	}
+	p.registerField(firstToken.TextPosition)
+	if p.report.HasErrors() {
+		return ast.InvalidRef
+	}
 
 	if p.peek() == keyword.COLON {
 		field.Alias.IsDefined = true
@@ -1431,6 +1542,8 @@ func (p *Parser) parseTypeCondition() (typeCondition ast.TypeCondition) {
 }
 
 func (p *Parser) parseOperationDefinition() {
+	p.beginExecutable()
+	defer p.endExecutable()
 
 	var operationDefinition ast.OperationDefinition
 
@@ -1552,6 +1665,9 @@ func (p *Parser) parseDefaultValue() ast.DefaultValue {
 }
 
 func (p *Parser) parseFragmentDefinition() {
+	p.beginExecutable()
+	defer p.endExecutable()
+
 	var fragmentDefinition ast.FragmentDefinition
 	fragmentDefinition.FragmentLiteral = p.mustReadIdentKey(identkeyword.FRAGMENT).TextPosition
 	fragmentDefinition.Name = p.mustRead(keyword.IDENT).Literal
diff --git a/tmp/agent-patch-flux-pr-1241.1-of-1.2026-02-28__13-38-10__gpt-5-3-codex/app/pkg/internal/unsafeparser/unsafeparser.go b/app/pkg/internal/unsafeparser/unsafeparser.go
index a1b0fa3..9572797 100644
--- a/tmp/agent-patch-flux-pr-1241.1-of-1.2026-02-28__13-38-10__gpt-5-3-codex/app/pkg/internal/unsafeparser/unsafeparser.go
+++ b/app/pkg/internal/unsafeparser/unsafeparser.go
@@ -24,6 +24,22 @@ func ParseGraphqlDocumentBytes(input []byte) ast.Document {
 	return doc
 }
 
+func ParseGraphqlDocumentStringWithOptions(input string, options astparser.ParseOptions) (ast.Document, astparser.ParseStats) {
+	doc, report, stats := astparser.ParseGraphqlDocumentStringWithOptions(input, options)
+	if report.HasErrors() {
+		panic(report.Error())
+	}
+	return doc, stats
+}
+
+func ParseGraphqlDocumentBytesWithOptions(input []byte, options astparser.ParseOptions) (ast.Document, astparser.ParseStats) {
+	doc, report, stats := astparser.ParseGraphqlDocumentBytesWithOptions(input, options)
+	if report.HasErrors() {
+		panic(report.Error())
+	}
+	return doc, stats
+}
+
 func ParseGraphqlDocumentFile(filePath string) ast.Document {
 	fileBytes, err := os.ReadFile(filePath)
 	if err != nil {
diff --git a/app/v2/pkg/astparser/parse_options.go b/app/v2/pkg/astparser/parse_options.go
new file mode 100644
index 0000000..2b6bfb7
--- /dev/null
+++ b/app/v2/pkg/astparser/parse_options.go
@@ -0,0 +1,14 @@
+package astparser
+
+// ParseOptions configures optional parse-time limits.
+// A zero value means "no limit" for the corresponding field.
+type ParseOptions struct {
+	MaxDepth  int
+	MaxFields int
+}
+
+// ParseStats contains executable-document complexity metrics gathered during parsing.
+type ParseStats struct {
+	Depth      int
+	FieldCount int
+}
diff --git a/app/v2/pkg/astparser/parse_options_test.go b/app/v2/pkg/astparser/parse_options_test.go
new file mode 100644
index 0000000..3a57daf
--- /dev/null
+++ b/app/v2/pkg/astparser/parse_options_test.go
@@ -0,0 +1,79 @@
+package astparser
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+)
+
+func TestParseGraphqlDocumentStringWithOptions_Stats(t *testing.T) {
+	input := `
+		query Q {
+			user {
+				id
+				...UserFields
+			}
+		}
+
+		fragment UserFields on User {
+			friends {
+				name
+			}
+		}
+	`
+
+	_, report, stats := ParseGraphqlDocumentStringWithOptions(input, ParseOptions{})
+	require.False(t, report.HasErrors(), report.Error())
+
+	assert.Equal(t, 4, stats.Depth)
+	assert.Equal(t, 4, stats.FieldCount)
+}
+
+func TestParseGraphqlDocumentStringWithOptions_MaxFields(t *testing.T) {
+	input := `
+		query Q {
+			user {
+				id
+				...UserFields
+			}
+		}
+
+		fragment UserFields on User {
+			friends {
+				name
+			}
+		}
+	`
+
+	_, report, _ := ParseGraphqlDocumentStringWithOptions(input, ParseOptions{
+		MaxFields: 3,
+	})
+	require.True(t, report.HasErrors())
+	require.NotEmpty(t, report.ExternalErrors)
+	assert.Contains(t, report.ExternalErrors[0].Message, "field limit exceeded")
+}
+
+func TestParseGraphqlDocumentStringWithOptions_MaxDepth(t *testing.T) {
+	input := `
+		query Q {
+			user {
+				id
+				...UserFields
+			}
+		}
+
+		fragment UserFields on User {
+			friends {
+				name
+			}
+		}
+	`
+
+	_, report, _ := ParseGraphqlDocumentStringWithOptions(input, ParseOptions{
+		MaxDepth: 3,
+	})
+	require.True(t, report.HasErrors())
+	require.NotEmpty(t, report.ExternalErrors)
+	assert.Contains(t, report.ExternalErrors[0].Message, "depth limit exceeded")
+}
diff --git a/tmp/agent-patch-flux-pr-1241.1-of-1.2026-02-28__13-38-10__gpt-5-3-codex/app/v2/pkg/astparser/parser.go b/app/v2/pkg/astparser/parser.go
index b950b4a..8c2629c 100644
--- a/tmp/agent-patch-flux-pr-1241.1-of-1.2026-02-28__13-38-10__gpt-5-3-codex/app/v2/pkg/astparser/parser.go
+++ b/app/v2/pkg/astparser/parser.go
@@ -58,6 +58,30 @@ func ParseGraphqlDocumentBytes(input []byte) (ast.Document, operationreport.Repo
 	return *doc, report
 }
 
+// ParseGraphqlDocumentStringWithOptions takes a raw GraphQL document in string format and parses it into an AST.
+// In addition to the parse result, this function returns executable document statistics.
+func ParseGraphqlDocumentStringWithOptions(input string, options ParseOptions) (ast.Document, operationreport.Report, ParseStats) {
+	parser := getParser()
+	defer releaseParser(parser)
+	doc := ast.NewSmallDocument()
+	doc.Input.ResetInputString(input)
+	report := operationreport.Report{}
+	stats := parser.ParseWithOptions(doc, &report, options)
+	return *doc, report, stats
+}
+
+// ParseGraphqlDocumentBytesWithOptions takes a raw GraphQL document in byte slice format and parses it into an AST.
+// In addition to the parse result, this function returns executable document statistics.
+func ParseGraphqlDocumentBytesWithOptions(input []byte, options ParseOptions) (ast.Document, operationreport.Report, ParseStats) {
+	parser := getParser()
+	defer releaseParser(parser)
+	doc := ast.NewSmallDocument()
+	doc.Input.ResetInputBytes(input)
+	report := operationreport.Report{}
+	stats := parser.ParseWithOptions(doc, &report, options)
+	return *doc, report, stats
+}
+
 // 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 +91,11 @@ type Parser struct {
 	tokenizer            *Tokenizer
 	shouldIndex          bool
 	reportInternalErrors bool
+	options              ParseOptions
+	stats                ParseStats
+	currentDepth         int
+	currentMaxDepth      int
+	inExecutable         bool
 }
 
 // NewParser returns a new parser with all values properly initialized
@@ -87,10 +116,90 @@ 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.resetRuntimeState(ParseOptions{})
+	p.document = document
+	p.report = report
+	p.tokenize()
+	p.parse()
+}
+
+// ParseWithOptions parses all input in a Document.Input into the Document and enforces optional limits.
+// It returns executable-document statistics regardless of whether limits are configured.
+func (p *Parser) ParseWithOptions(document *ast.Document, report *operationreport.Report, options ParseOptions) ParseStats {
+	p.resetRuntimeState(options)
 	p.document = document
 	p.report = report
 	p.tokenize()
 	p.parse()
+	return p.stats
+}
+
+func (p *Parser) resetRuntimeState(options ParseOptions) {
+	p.options = options
+	p.stats = ParseStats{}
+	p.currentDepth = 0
+	p.currentMaxDepth = 0
+	p.inExecutable = false
+}
+
+func (p *Parser) beginExecutable() {
+	p.inExecutable = true
+	p.currentDepth = 0
+	p.currentMaxDepth = 0
+}
+
+func (p *Parser) endExecutable() {
+	if !p.inExecutable {
+		return
+	}
+	p.stats.Depth += p.currentMaxDepth
+	p.currentDepth = 0
+	p.currentMaxDepth = 0
+	p.inExecutable = false
+}
+
+func (p *Parser) enterSelectionSet(pos position.Position) {
+	if !p.inExecutable || p.report.HasErrors() {
+		return
+	}
+	p.currentDepth++
+	if p.currentDepth > p.currentMaxDepth {
+		p.currentMaxDepth = p.currentDepth
+	}
+	if p.options.MaxDepth > 0 && p.stats.Depth+p.currentMaxDepth > p.options.MaxDepth {
+		p.report.AddExternalError(operationreport.ExternalError{
+			Message: fmt.Sprintf(
+				"parser depth limit exceeded: max depth is %d, got at least %d",
+				p.options.MaxDepth,
+				p.stats.Depth+p.currentMaxDepth,
+			),
+			Locations: operationreport.LocationsFromPosition(pos),
+		})
+	}
+}
+
+func (p *Parser) leaveSelectionSet() {
+	if !p.inExecutable || p.currentDepth == 0 {
+		return
+	}
+	p.currentDepth--
+}
+
+func (p *Parser) registerField(pos position.Position) {
+	if !p.inExecutable || p.report.HasErrors() {
+		return
+	}
+	p.stats.FieldCount++
+	if p.options.MaxFields > 0 && p.stats.FieldCount > p.options.MaxFields {
+		p.report.AddExternalError(operationreport.ExternalError{
+			Message: fmt.Sprintf(
+				"parser field limit exceeded: max fields is %d, got at least %d",
+				p.options.MaxFields,
+				p.stats.FieldCount,
+			),
+			Locations: operationreport.LocationsFromPosition(pos),
+		})
+	}
 }
 
 func (p *Parser) tokenize() {
@@ -1303,6 +1412,11 @@ 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)
+	if p.report.HasErrors() {
+		return ast.InvalidRef, false
+	}
+	defer p.leaveSelectionSet()
 
 	for {
 		switch p.peek() {
@@ -1389,6 +1503,10 @@ func (p *Parser) parseField() int {
 	if firstToken.Keyword != keyword.IDENT {
 		p.errUnexpectedToken(firstToken, keyword.IDENT)
 	}
+	p.registerField(firstToken.TextPosition)
+	if p.report.HasErrors() {
+		return ast.InvalidRef
+	}
 
 	if p.peek() == keyword.COLON {
 		field.Alias.IsDefined = true
@@ -1458,6 +1576,8 @@ func (p *Parser) parseTypeCondition() (typeCondition ast.TypeCondition) {
 }
 
 func (p *Parser) parseOperationDefinition() {
+	p.beginExecutable()
+	defer p.endExecutable()
 
 	var operationDefinition ast.OperationDefinition
 
@@ -1579,6 +1699,9 @@ func (p *Parser) parseDefaultValue() ast.DefaultValue {
 }
 
 func (p *Parser) parseFragmentDefinition() {
+	p.beginExecutable()
+	defer p.endExecutable()
+
 	var fragmentDefinition ast.FragmentDefinition
 	fragmentDefinition.FragmentLiteral = p.mustReadIdentKey(identkeyword.FRAGMENT).TextPosition
 	fragmentDefinition.Name = p.mustRead(keyword.IDENT).Literal
diff --git a/tmp/agent-patch-flux-pr-1241.1-of-1.2026-02-28__13-38-10__gpt-5-3-codex/app/v2/pkg/internal/unsafeparser/unsafeparser.go b/app/v2/pkg/internal/unsafeparser/unsafeparser.go
index b115856..394dd1a 100644
--- a/tmp/agent-patch-flux-pr-1241.1-of-1.2026-02-28__13-38-10__gpt-5-3-codex/app/v2/pkg/internal/unsafeparser/unsafeparser.go
+++ b/app/v2/pkg/internal/unsafeparser/unsafeparser.go
@@ -25,6 +25,22 @@ func ParseGraphqlDocumentBytes(input []byte) ast.Document {
 	return doc
 }
 
+func ParseGraphqlDocumentStringWithOptions(input string, options astparser.ParseOptions) (ast.Document, astparser.ParseStats) {
+	doc, report, stats := astparser.ParseGraphqlDocumentStringWithOptions(input, options)
+	if report.HasErrors() {
+		panic(report.Error())
+	}
+	return doc, stats
+}
+
+func ParseGraphqlDocumentBytesWithOptions(input []byte, options astparser.ParseOptions) (ast.Document, astparser.ParseStats) {
+	doc, report, stats := astparser.ParseGraphqlDocumentBytesWithOptions(input, options)
+	if report.HasErrors() {
+		panic(report.Error())
+	}
+	return doc, stats
+}
+
 func ParseGraphqlDocumentFile(filePath string) ast.Document {
 	fileBytes, err := os.ReadFile(filePath)
 	if err != nil {