Skip to content

fix(templates): refuse an uncompilable tool call ID pattern - #465

Open
Alpovka wants to merge 1 commit into
vxcontrol:mainfrom
Alpovka:fix/tool-call-id-template-panic
Open

Alpovka wants to merge 1 commit into
vxcontrol:mainfrom
Alpovka:fix/tool-call-id-template-panic

Conversation

@Alpovka

@Alpovka Alpovka commented Oct 20, 2026

Copy link
Copy Markdown

Description of the Change

Problem

templates.ValidatePattern builds a regexp from the tool-call-ID template and compiles it with regexp.MustCompile. The {r:N:cs} length is read with strconv.Atoi and the error is discarded. Go's regexp refuses a repeat count above 1000, so a template such as call_{r:1001:x}, or one whose count overflows int, panics.

The template is taken verbatim from the model's submit_pattern tool call in detectPatternWithAI (backend/pkg/providers/provider/agents.go). The panic happens inside go fc.prepareFlow(...), where no recover runs, so one such answer stops the whole process and every running flow with it. A user with settings.prompts.edit can cause this by editing the tool_call_id_detector prompt, and an OpenAI-compatible endpoint can cause it by answering with that template or with IDs longer than 1000 characters.

The heuristic fallback has the same gap: it derives {r:N:cs} from the sample IDs and stores it unvalidated. The template is written to flows.tool_call_id_template, and on restart restoreChain → NormalizeToolCallIDs → ValidatePattern panics again for every tool call in the chain.

Solution

  • parsePattern returns an error when the length does not parse or exceeds the regexp limit (1000).
  • ValidatePattern returns that error and compiles with regexp.Compile instead of MustCompile.
  • GenerateFromPattern keeps its no-error contract and falls back to DefaultToolCallIDTemplate for a pattern that does not compile. cast.ToolCallIDTemplate now points at that constant.
  • fallbackHeuristicDetection yields "" for a pattern it cannot compile, so the caller takes the default.
  • toolCallIDTemplateOrDefault replaces a stored template that no longer compiles with the default, which also repairs rows written before this change.
  • templates.ValidatePatternSyntax is the one exported check the last two use.

Reproduction of the panic before the fix (Go 1.24.7):

{r:1000:x} -> ok
{r:1001:x} -> PANIC: regexp: Compile(`^call_[a-zA-Z0-9]{1001}$`): invalid repeat count
{r:99999999999999999999:x} -> PANIC (Atoi overflow yields 9223372036854775807)

Type of Change

  • 🐛 Bug fix (non-breaking change which fixes an issue)
  • 🛡️ Security update

Areas Affected

  • Core Services (Frontend UI/Backend API)
  • AI Agents (Researcher/Developer/Executor)
  • External Integrations (LLM/Search APIs)

Testing and Verification

Test Steps

  1. cd backend && go test -race -count=1 ./pkg/templates/ ./pkg/cast/ ./pkg/providers/provider/ ./pkg/providers/
  2. go vet, gofmt -l and golangci-lint run (v2.12.2, the CI pin) on the same packages.
  3. Mutation proof in a throwaway copy: with the length cap removed, the Atoi error ignored, the heuristic syntax check removed, or the stored-template check removed, the corresponding new test fails.

Test Results

New tests: two rows in TestTemplates_ValidatePattern_NamesWhereASampleDiverges, TestTemplates_ValidatePatternSyntax_StopsAtTheLargestRepeatTheRegexpCompiles, TestTemplates_GenerateFromPattern_FallsBackToTheDefaultTemplateWhenThePatternCannotCompile, a row in TestAgents_FallbackHeuristicDetection_BuildsThePatternTheSamplesShare and a row in TestHelpers_ToolCallIDTemplateOrDefault_NeverYieldsAnEmptyTemplate.

pkg/templates, pkg/cast, pkg/providers/provider and pkg/providers pass under -race; golangci-lint reports 0 issues. The one exception: TestPerformer_AiResponsePart_ReplaysATextlessThinkingTurnOnTheClaudeDoors/bedrock/* fails on unmodified main in my environment too (unable to add custom RootCAs HTTPClient, an AWS SDK config error caused by the sandbox's HTTP proxy) and was skipped. No dependency changed, so go mod tidy leaves go.mod and go.sum as they are.

Security Considerations

Removes a remote denial of service: a prompt edit by a low-privilege user, or a hostile LLM endpoint, could crash the server and leave a flow that crashes it again on every restart. No new dependencies, no permission changes.

Performance Impact

None. One extra parse of the template on flow and assistant creation.

Deployment Notes

None. A flow whose stored template no longer compiles falls back to call_{r:24:x} on load and its chain IDs are normalised to it.

Additional Notes

A recover in the flow prepare and worker goroutines would turn any future panic into a failed flow instead of a dead process. It is left out of this PR to keep the change to the bug.

ValidatePattern built its regexp with MustCompile from a {r:N:cs} length
it never checked. Go's regexp refuses a repeat count above 1000, so a
template such as call_{r:1001:x} panicked. The template comes verbatim
from the model's submit_pattern call, and the panic happens in the flow
prepare goroutine, where no recover runs, so one such answer stopped the
whole process. A heuristic fallback built from IDs longer than 1000
characters was stored in flows.tool_call_id_template and panicked again
on every restart that resumed the flow.

parsePattern now rejects a length that does not parse or exceeds the
regexp limit, ValidatePattern returns that error instead of panicking,
GenerateFromPattern falls back to the default template, the heuristic
fallback yields nothing for a pattern it cannot compile, and
toolCallIDTemplateOrDefault replaces a stored template that no longer
compiles with the default.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QCjnWfCbd1hMpiS7wwkQPc
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant