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
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 {