Skip to content

[plan] Fix non-deterministic map iteration in EngineRegistry #21473

@github-actions

Description

@github-actions

Context

From security audit discussion #21454 (Sergo Run 24, 2026-03-17).

Problem

GetSupportedEngines() and GetEngineByPrefix() in pkg/workflow/agentic_engine.go iterate the r.engines map[string]CodingAgentEngine directly using for range, producing non-deterministic results due to Go's randomized map iteration order.

GetEngineByPrefix() specifically returns the first matching engine whose ID is a prefix of the query string — since iteration order is random, the same prefix string can resolve to different engines across runs.

// Lines 459-465 — non-deterministic order
func (r *EngineRegistry) GetSupportedEngines() []string {
    var engines []string
    for id := range r.engines {
        engines = append(engines, id)
    }
    return engines
}

// Lines 480-487 — first match wins, order is random
func (r *EngineRegistry) GetEngineByPrefix(prefix string) (CodingAgentEngine, error) {
    for id, engine := range r.engines {
        if strings.HasPrefix(prefix, id) {
            return engine, nil
        }
    }
    ...
}

Severity: HIGH — Non-deterministic engine selection, potential intermittent test failures, inconsistent --engine flag behavior.
Confirmed unfixed since: Run 2 (2026-02-22) — 23 consecutive runs.

Files to Modify

  • pkg/workflow/agentic_engine.go — fix GetSupportedEngines() and GetEngineByPrefix()

Approach

GetSupportedEngines(): Sort the result before returning:

func (r *EngineRegistry) GetSupportedEngines() []string {
    var engines []string
    for id := range r.engines {
        engines = append(engines, id)
    }
    sort.Strings(engines)
    return engines
}

GetEngineByPrefix(): Collect all matching candidates, sort by ID, return the first (longest or alphabetically first):

func (r *EngineRegistry) GetEngineByPrefix(prefix string) (CodingAgentEngine, error) {
    type candidate struct{ id string; engine CodingAgentEngine }
    var candidates []candidate
    for id, engine := range r.engines {
        if strings.HasPrefix(prefix, id) {
            candidates = append(candidates, candidate{id, engine})
        }
    }
    if len(candidates) == 0 {
        return nil, fmt.Errorf("no engine found matching prefix: %s", prefix)
    }
    sort.Slice(candidates, func(i, j int) bool { return candidates[i].id < candidates[j].id })
    return candidates[0].engine, nil
}

Also check if GetAllEngines() or similar functions in the same file have the same issue and fix them as well.

Acceptance Criteria

  • GetSupportedEngines() returns engines in consistent (sorted) order
  • GetEngineByPrefix() deterministically resolves the same prefix to the same engine
  • Tests in agentic_engine_test.go and related engine tests pass
  • make agent-finish passes with no errors

Generated by Plan Command for issue #discussion #21454 ·

  • expires on Mar 19, 2026, 11:44 PM UTC

Metadata

Metadata

Labels

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions