Skip to content

feat(core): add execution validators#1219

Open
omeraplak wants to merge 1 commit intomainfrom
feat/execution-validators
Open

feat(core): add execution validators#1219
omeraplak wants to merge 1 commit intomainfrom
feat/execution-validators

Conversation

@omeraplak
Copy link
Copy Markdown
Member

@omeraplak omeraplak commented Apr 22, 2026

PR Checklist

Please check if your PR fulfills the following requirements:

Bugs / Features

What is the current behavior?

VoltAgent supports guardrails, hooks, approval, and schema validation, but there is no reusable execution-boundary validation primitive that can deterministically inspect the final tool/workflow payload and context immediately before execution.

Tool and workflow execution policies must be implemented ad hoc inside individual hooks, tools, or workflow steps. Direct server handlers also do not preserve client HTTP error details for validation-style failures.

What is the new behavior?

  • Adds executionValidators for agents and workflows.
  • Runs tool execution validators before tool start hooks and before tool execution, including provider tool execution.
  • Runs workflow execution validators before workflow steps begin.
  • Adds ExecutionValidationError with custom message, code, HTTP status, and metadata support.
  • Preserves ClientHTTPError details in server-core direct tool and workflow handlers.
  • Adds docs for tool and workflow pre-execution validators.
  • Adds focused tests for core agent, core workflow, server-core tool handler, and server-core workflow handler behavior.

fixes #1213

Notes for reviewers

Validation run:

  • pnpm --filter @voltagent/core typecheck
  • pnpm --filter @voltagent/server-core typecheck
  • pnpm --filter @voltagent/core exec vitest run src/agent/agent.spec.ts src/workflow/core.spec.ts --reporter=default
  • pnpm --filter @voltagent/server-core test -- src/handlers/tool.handlers.spec.ts src/handlers/workflow.handlers.spec.ts
  • git diff --check

Summary by cubic

Adds pre-execution validators for tools and workflows to enforce deterministic policies right before execution. Server handlers return structured validation errors (name, code, httpStatus) instead of generic 500s.

  • New Features
    • executionValidators for tools and workflows; run before tool start/execution (including provider tools) and before workflow steps.
    • Validators can be set at the agent/workflow level or per request; return false or { pass: false } to block.
    • New ExecutionValidationError with message, code, httpStatus, and metadata.
    • @voltagent/server-core direct tool and workflow handlers preserve ClientHTTPError details (name, code, httpStatus) in responses.
    • Docs and focused tests added across @voltagent/core and @voltagent/server-core.

Written for commit ec3bab9. Summary will update on new commits.

Summary by CodeRabbit

  • New Features

    • Execution validators for agents and workflows that run before tools/steps and can block execution (returning false or { pass: false }) with custom message/code/httpStatus.
    • New execution validation error type; server handlers now preserve validator-provided error details in responses.
  • Tests

    • Added tests covering validator blocking behavior for agents, tools, and workflows.
  • Documentation

    • Added docs and examples for pre-execution tool and workflow validators.

@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Apr 22, 2026

🦋 Changeset detected

Latest commit: ec3bab9

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 2 packages
Name Type
@voltagent/core Minor
@voltagent/server-core Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 22, 2026

📝 Walkthrough

Walkthrough

Adds pre-execution validators for tools and workflows. Validators run (sync/async) before tool hooks or workflow steps and can block execution by returning false or { pass: false }, causing an ExecutionValidationError that preserves validator-provided message, code, httpStatus, and metadata in server responses.

Changes

Cohort / File(s) Summary
Validation Framework
packages/core/src/execution-validation.ts
New typed validator primitives, contexts, and runExecutionValidators runner that throws ExecutionValidationError on failure.
Agent Core & Types
packages/core/src/agent/agent.ts, packages/core/src/agent/types.ts
Added executionValidators to agent/options/operation context, merged per-call validators, introduced Agent.validateToolExecution(), and wired validation into tool execution paths (before hooks and execution).
Errors Export
packages/core/src/agent/errors/client-http-errors.ts, packages/core/src/agent/errors/index.ts, packages/core/src/index.ts
New ExecutionValidationError class, ExecutionValidationErrorCode type, isExecutionValidationError guard, and re-exports added to public surface.
Workflow Integration
packages/core/src/workflow/core.ts, packages/core/src/workflow/types.ts
Added executionValidators to workflow config/run options and run validators before any step execution (gating resume/start).
Server Handlers
packages/server-core/src/handlers/tool.handlers.ts, packages/server-core/src/handlers/workflow.handlers.ts
Added optional agent.validateToolExecution hook, unified toolCallId handling, and updated handlers to map ClientHTTPError (including ExecutionValidationError) into responses preserving message, code, name, and httpStatus.
Tests
packages/core/src/agent/agent.spec.ts, packages/core/src/workflow/core.spec.ts, packages/server-core/src/handlers/tool.handlers.spec.ts, packages/server-core/src/handlers/workflow.handlers.spec.ts
Added tests asserting validators run before hooks/execute, block execution, and that server handlers propagate client error details.
Docs & Changeset
website/docs/agents/tools.md, website/docs/workflows/overview.md, .changeset/execution-validators.md
Documentation for pre-execution validators with examples; changeset bumping @voltagent/core (minor) and @voltagent/server-core (patch).

Sequence Diagram(s)

sequenceDiagram
    rect rgba(220,230,241,0.5)
    participant App as Application
    participant Agent as Agent
    participant Validator as Execution Validator
    participant Tool as Tool
    end
    App->>Agent: request tool execution (args, executionValidators)
    Agent->>Validator: validateToolExecution(context)
    alt Validation Passes
        Validator-->>Agent: void / pass
        Agent->>Tool: run tool hooks → execute(args)
        Tool-->>Agent: result
        Agent-->>App: success response
    else Validation Fails
        Validator-->>Agent: { pass: false } / throws ExecutionValidationError
        Agent-->>App: error response (message, code, httpStatus)
    end
Loading
sequenceDiagram
    rect rgba(241,230,220,0.5)
    participant App as Application
    participant Workflow as Workflow
    participant Validator as Execution Validator
    participant Step as Workflow Step
    end
    App->>Workflow: run(input, { executionValidators })
    Workflow->>Validator: runExecutionValidators(context)
    alt Validation Passes
        Validator-->>Workflow: void / pass
        Workflow->>Step: execute step(s)
        Step-->>Workflow: results
        Workflow-->>App: workflow output
    else Validation Fails
        Validator-->>Workflow: { pass: false } / throws ExecutionValidationError
        Workflow-->>App: error response (message, code, httpStatus)
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • lzj960515

Poem

🐰 I sniff the code where validators hide,

I hop in front of tools with careful pride.
"Pass or stop!" I loudly cry,
Guarding steps beneath the sky—
Safe executions now hop on by.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 22.22% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat(core): add execution validators' clearly and concisely describes the main feature addition, following the repository's commit convention for feature changes.
Description check ✅ Passed The PR description comprehensively addresses the template requirements with current behavior, new behavior, linked issue, tests, docs, changesets, and validation commands documented.
Linked Issues check ✅ Passed All coding requirements from issue #1213 are fully addressed: execution validators for tools/workflows, pre-execution validation blocking, structured error support, and uniform enforcement across execution paths.
Out of Scope Changes check ✅ Passed All changes are directly scoped to the linked issue #1213: execution validators for tools, execution validators for workflows, validation error handling, server handler error preservation, and supporting documentation.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/execution-validators

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@joggrbot

This comment has been minimized.

@omeraplak omeraplak force-pushed the feat/execution-validators branch from f5dbf49 to ec3bab9 Compare April 22, 2026 01:59
@cloudflare-workers-and-pages
Copy link
Copy Markdown

cloudflare-workers-and-pages Bot commented Apr 22, 2026

Deploying voltagent with  Cloudflare Pages  Cloudflare Pages

Latest commit: ec3bab9
Status: ✅  Deploy successful!
Preview URL: https://fad555f1.voltagent.pages.dev
Branch Preview URL: https://feat-execution-validators.voltagent.pages.dev

View logs

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/core/src/agent/agent.ts`:
- Around line 925-927: The aiSDKOptions object being passed into the provider
calls mistakenly includes the VoltAgent-specific executionValidators field, so
update the option destructuring in the places that call generateText,
streamText, generateObject, and streamObject (including the aiSDKOptions
construction around functions/methods where aiSDKOptions is built) to explicitly
exclude executionValidators (e.g., const { executionValidators, ...aiSDKOptions
} = options) before forwarding; apply this same destructuring pattern at the
other listed sites (around the calls referenced by symbols generateText,
streamText, generateObject, streamObject) so validator functions are stripped
out of the options passed to the AI SDK.
- Around line 1183-1186: The method currently resolves validators from options
or this.executionValidators but ignores operationContext.executionValidators;
update the resolver to prefer operationContext?.executionValidators first, then
options?.executionValidators, then this.executionValidators so validators merged
by createOperationContext are honored; locate the code around the validators
assignment (the const validators = ... line) and change the lookup order to
check operationContext.executionValidators before falling back to options and
agent-level executionValidators, and keep the existing empty-array/length check
behavior.

In `@packages/core/src/workflow/core.ts`:
- Around line 1222-1242: Validators are currently run inside executeInternal
after startAsync has already set the workflow state to "running", causing
validation failures to appear only as async background errors; extract the
validator merge and run logic (the executionValidators array construction and
the runExecutionValidators call) into a new helper (e.g., runWorkflowValidators
or validateBeforeStart) and call that helper from startAsync before it calls
setWorkflowState/runs the "running" commit; then have executeInternal accept a
flag/param (or remove its own validation call) to skip re-running the same
validators when startAsync already validated, ensuring pre-start validation
blocks creation of a running execution record.

In `@packages/server-core/src/handlers/workflow.handlers.ts`:
- Around line 562-566: The consumeWorkflowStream path drops ClientHTTPError
details when the async validation fails later (via streamExecution.result) —
update consumeWorkflowStream/wherever the workflow.stream result is consumed to
await or attach a .catch handler on streamExecution.result and, if it rejects
with a ClientHTTPError, preserve and propagate its properties (code, name,
httpStatus) into the broadcast payload instead of only broadcasting the message;
use the existing mapClientHTTPError function or copy its mapped fields so the
downstream consumer receives the same structured error as the synchronous catch
that checks "if (error instanceof ClientHTTPError)" and calls
mapClientHTTPError, ensuring consistency between initial try/catch and
post-stream rejection handling.

In `@website/docs/workflows/overview.md`:
- Around line 598-603: The example calls await workflow.run with tenant-a which
will be rejected because tenant-a is not in the allowedTenants map; update the
docs example to explicitly handle the expected validation rejection by wrapping
the call to workflow.run (the workflow.run invocation using context/new Map with
allowedTenants) in a try/catch and logging/returning the error in the catch so
the documented behavior is explicit rather than causing an unhandled rejection.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e4be4d40-39bd-430b-af54-f6dcf54f7f18

📥 Commits

Reviewing files that changed from the base of the PR and between 8b8c238 and f5dbf49.

📒 Files selected for processing (17)
  • .changeset/execution-validators.md
  • packages/core/src/agent/agent.spec.ts
  • packages/core/src/agent/agent.ts
  • packages/core/src/agent/errors/client-http-errors.ts
  • packages/core/src/agent/errors/index.ts
  • packages/core/src/agent/types.ts
  • packages/core/src/execution-validation.ts
  • packages/core/src/index.ts
  • packages/core/src/workflow/core.spec.ts
  • packages/core/src/workflow/core.ts
  • packages/core/src/workflow/types.ts
  • packages/server-core/src/handlers/tool.handlers.spec.ts
  • packages/server-core/src/handlers/tool.handlers.ts
  • packages/server-core/src/handlers/workflow.handlers.spec.ts
  • packages/server-core/src/handlers/workflow.handlers.ts
  • website/docs/agents/tools.md
  • website/docs/workflows/overview.md

Comment on lines +925 to +927
// Execution validators (can add per-call validators)
executionValidators?: AgentExecutionValidators;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Strip executionValidators before forwarding options to the AI SDK.

executionValidators is a VoltAgent-only option, but it remains in aiSDKOptions and is forwarded into generateText, streamText, generateObject, and streamObject. That can leak validator functions into provider/AI SDK call options and create hard-to-debug behavior.

Proposed fix
               parentOperationContext,
               hooks,
+              executionValidators: _executionValidators,
               feedback: _feedback,
               maxSteps: userMaxSteps,

Apply the same destructuring exclusion in the streamText, generateObject, and streamObject option destructures.

Also applies to: 1337-1356, 1956-1976, 2860-2879, 3234-3254

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/core/src/agent/agent.ts` around lines 925 - 927, The aiSDKOptions
object being passed into the provider calls mistakenly includes the
VoltAgent-specific executionValidators field, so update the option destructuring
in the places that call generateText, streamText, generateObject, and
streamObject (including the aiSDKOptions construction around functions/methods
where aiSDKOptions is built) to explicitly exclude executionValidators (e.g.,
const { executionValidators, ...aiSDKOptions } = options) before forwarding;
apply this same destructuring pattern at the other listed sites (around the
calls referenced by symbols generateText, streamText, generateObject,
streamObject) so validator functions are stripped out of the options passed to
the AI SDK.

Comment on lines +1183 to +1186
const validators = options?.executionValidators?.tools ?? this.executionValidators?.tools;
if (!validators || validators.length === 0) {
return;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Resolve validators from the operation context first.

createOperationContext already merges parent, agent, and per-call validators into OperationContext.executionValidators, but this method ignores operationContext?.executionValidators. Direct calls that pass an operation context can silently skip operation-scoped validators; direct calls with options.executionValidators also skip agent-level validators.

Proposed fix
-    const validators = options?.executionValidators?.tools ?? this.executionValidators?.tools;
+    const validators =
+      operationContext?.executionValidators?.tools ??
+      mergeAgentExecutionValidators(this.executionValidators, options?.executionValidators)?.tools;
     if (!validators || validators.length === 0) {
       return;
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/core/src/agent/agent.ts` around lines 1183 - 1186, The method
currently resolves validators from options or this.executionValidators but
ignores operationContext.executionValidators; update the resolver to prefer
operationContext?.executionValidators first, then options?.executionValidators,
then this.executionValidators so validators merged by createOperationContext are
honored; locate the code around the validators assignment (the const validators
= ... line) and change the lookup order to check
operationContext.executionValidators before falling back to options and
agent-level executionValidators, and keep the existing empty-array/length check
behavior.

Comment on lines +1222 to +1242
const executionValidators = [
...(workflowExecutionValidators ?? []),
...(options?.executionValidators ?? []),
];
await runExecutionValidators(
executionValidators,
{
type: "workflow",
workflowId: id,
workflowName: name,
input,
options,
executionId,
context: contextMap,
workflowState: workflowStateStore,
timestamp: new Date(),
logger: runLogger,
},
`Workflow ${id} execution blocked by validation.`,
"WORKFLOW_VALIDATION_FAILED",
);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Run validators before startAsync commits workflow state.

startAsync writes a running workflow state before calling executeInternal, so a validation denial still creates an execution record and only becomes an async background failure. That weakens the “pre-execution” boundary for async starts.

Consider extracting this validator merge/run into a helper that startAsync can call before setWorkflowState, then skip the duplicate validation when it delegates to executeInternal.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/core/src/workflow/core.ts` around lines 1222 - 1242, Validators are
currently run inside executeInternal after startAsync has already set the
workflow state to "running", causing validation failures to appear only as async
background errors; extract the validator merge and run logic (the
executionValidators array construction and the runExecutionValidators call) into
a new helper (e.g., runWorkflowValidators or validateBeforeStart) and call that
helper from startAsync before it calls setWorkflowState/runs the "running"
commit; then have executeInternal accept a flag/param (or remove its own
validation call) to skip re-running the same validators when startAsync already
validated, ensuring pre-start validation blocks creation of a running execution
record.

Comment on lines 562 to +566
} catch (error) {
logger.error("Failed to initiate workflow stream", { error });
if (error instanceof ClientHTTPError) {
return mapClientHTTPError(error);
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Preserve ClientHTTPError details for asynchronous workflow streams too.

This catch only handles errors thrown while initiating the stream. For workflow.stream(...), validator failures can reject later via streamExecution.result, so consumeWorkflowStream currently broadcasts only the message and drops code, name, and httpStatus.

Proposed fix
   } catch (error) {
     logger.error("Failed during workflow stream:", { error });

@@
-    broadcastWorkflowStreamEvent(session, {
-      type: "error",
-      error: error instanceof Error ? error.message : "Stream failed",
-    });
+    broadcastWorkflowStreamEvent(
+      session,
+      error instanceof ClientHTTPError
+        ? {
+            type: "error",
+            error: error.message,
+            code: error.code,
+            name: error.name,
+            httpStatus: error.httpStatus,
+          }
+        : {
+            type: "error",
+            error: error instanceof Error ? error.message : "Stream failed",
+          },
+    );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/server-core/src/handlers/workflow.handlers.ts` around lines 562 -
566, The consumeWorkflowStream path drops ClientHTTPError details when the async
validation fails later (via streamExecution.result) — update
consumeWorkflowStream/wherever the workflow.stream result is consumed to await
or attach a .catch handler on streamExecution.result and, if it rejects with a
ClientHTTPError, preserve and propagate its properties (code, name, httpStatus)
into the broadcast payload instead of only broadcasting the message; use the
existing mapClientHTTPError function or copy its mapped fields so the downstream
consumer receives the same structured error as the synchronous catch that checks
"if (error instanceof ClientHTTPError)" and calls mapClientHTTPError, ensuring
consistency between initial try/catch and post-stream rejection handling.

Comment on lines +598 to +603
await workflow.run(
{ tenantId: "tenant-a" },
{
context: new Map([["allowedTenants", ["tenant-b"]]]),
}
);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Handle the expected validation rejection in the docs example.

This run is denied because tenant-a is not in allowedTenants; wrapping it makes the documented behavior explicit instead of ending in an unhandled rejection.

📝 Proposed docs adjustment
-await workflow.run(
-  { tenantId: "tenant-a" },
-  {
-    context: new Map([["allowedTenants", ["tenant-b"]]]),
-  }
-);
+try {
+  await workflow.run(
+    { tenantId: "tenant-a" },
+    {
+      context: new Map([["allowedTenants", ["tenant-b"]]]),
+    }
+  );
+} catch (error) {
+  console.error("Workflow blocked by execution validator:", error);
+}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
await workflow.run(
{ tenantId: "tenant-a" },
{
context: new Map([["allowedTenants", ["tenant-b"]]]),
}
);
try {
await workflow.run(
{ tenantId: "tenant-a" },
{
context: new Map([["allowedTenants", ["tenant-b"]]]),
}
);
} catch (error) {
console.error("Workflow blocked by execution validator:", error);
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@website/docs/workflows/overview.md` around lines 598 - 603, The example calls
await workflow.run with tenant-a which will be rejected because tenant-a is not
in the allowedTenants map; update the docs example to explicitly handle the
expected validation rejection by wrapping the call to workflow.run (the
workflow.run invocation using context/new Map with allowedTenants) in a
try/catch and logging/returning the error in the catch so the documented
behavior is explicit rather than causing an unhandled rejection.

Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 17 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="packages/core/src/workflow/core.ts">

<violation number="1" location="packages/core/src/workflow/core.ts:1226">
P1: Validation failures can leave pre-created `startAsync` workflow state stuck as `running` because validators run before the `skipStateInit` state-update path.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment on lines +1226 to +1242
await runExecutionValidators(
executionValidators,
{
type: "workflow",
workflowId: id,
workflowName: name,
input,
options,
executionId,
context: contextMap,
workflowState: workflowStateStore,
timestamp: new Date(),
logger: runLogger,
},
`Workflow ${id} execution blocked by validation.`,
"WORKFLOW_VALIDATION_FAILED",
);
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot Apr 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Validation failures can leave pre-created startAsync workflow state stuck as running because validators run before the skipStateInit state-update path.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/core/src/workflow/core.ts, line 1226:

<comment>Validation failures can leave pre-created `startAsync` workflow state stuck as `running` because validators run before the `skipStateInit` state-update path.</comment>

<file context>
@@ -1217,6 +1219,28 @@ export function createWorkflow<
+        ...(workflowExecutionValidators ?? []),
+        ...(options?.executionValidators ?? []),
+      ];
+      await runExecutionValidators(
+        executionValidators,
+        {
</file context>
Suggested change
await runExecutionValidators(
executionValidators,
{
type: "workflow",
workflowId: id,
workflowName: name,
input,
options,
executionId,
context: contextMap,
workflowState: workflowStateStore,
timestamp: new Date(),
logger: runLogger,
},
`Workflow ${id} execution blocked by validation.`,
"WORKFLOW_VALIDATION_FAILED",
);
try {
await runExecutionValidators(
executionValidators,
{
type: "workflow",
workflowId: id,
workflowName: name,
input,
options,
executionId,
context: contextMap,
workflowState: workflowStateStore,
timestamp: new Date(),
logger: runLogger,
},
`Workflow ${id} execution blocked by validation.`,
"WORKFLOW_VALIDATION_FAILED",
);
} catch (error) {
if (options?.skipStateInit) {
await executionMemory.updateWorkflowState(executionId, {
status: "error",
error,
updatedAt: new Date(),
});
}
throw error;
}
Fix with Cubic

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
packages/core/src/workflow/core.ts (1)

1113-1114: ⚠️ Potential issue | 🟠 Major

Validate against the resumed workflow state.

For resumed runs, validators receive workflowState: options?.workflowState ?? {}, but the actual runtime state is later replaced from options.resumeFrom.checkpoint.workflowState. Validators that enforce tenant/permission state can inspect the wrong state on resume.

Proposed fix
-    const workflowStateStore = options?.workflowState ?? {};
+    const workflowStateStore =
+      options?.resumeFrom?.checkpoint?.workflowState ?? options?.workflowState ?? {};

Also applies to: 1226-1237, 1455-1466

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/core/src/workflow/core.ts` around lines 1113 - 1114, The validators
are using workflowStateStore from const workflowStateStore =
options?.workflowState ?? {}, which is wrong for resumed runs because the
runtime replaces state from options.resumeFrom.checkpoint.workflowState; update
places where validators are passed workflowStateStore (e.g., validator
invocation sites around core.ts lines where validators are called near the const
workflowStateStore declaration and the similar blocks around the other noted
ranges) to instead derive the state from
options.resumeFrom?.checkpoint?.workflowState when options.resumeFrom exists,
falling back to options?.workflowState ?? {} otherwise—ensure all validator
calls (the same symbol usages referenced) receive the resumed checkpoint state
so tenant/permission checks validate against the actual runtime state.
packages/core/src/agent/agent.ts (1)

6392-6406: ⚠️ Potential issue | 🟠 Major

Avoid firing the tool end hook twice on failures.

Validation failures now flow through handleToolError, so this duplicated tool.hooks?.onEnd call can double-run side effects such as audit logging, cleanup, or metrics.

Proposed fix
         await tool.hooks?.onEnd?.({
           tool,
           args,
           output: undefined,
           error: voltAgentError,
           options: executionOptions,
         });
-
-        await tool.hooks?.onEnd?.({
-          tool,
-          args,
-          output: undefined,
-          error: voltAgentError,
-          options: executionOptions,
-        });
 
         const onToolErrorResult = await hooks.onToolError?.({
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/core/src/agent/agent.ts` around lines 6392 - 6406, The duplicate
invocation of tool.hooks?.onEnd with the same parameters causes side effects to
run twice on failures; remove the redundant call so onEnd is awaited only once
(keep a single await tool.hooks?.onEnd? invocation), or centralize the call to
happen after handleToolError returns/throws to ensure validation failures
handled by handleToolError do not cause a second onEnd invocation; update the
block around tool.hooks?.onEnd, voltAgentError, args and executionOptions so
only one onEnd is executed for a given failure path.
♻️ Duplicate comments (4)
packages/server-core/src/handlers/workflow.handlers.ts (1)

248-251: ⚠️ Potential issue | 🟠 Major

Preserve ClientHTTPError fields in streamed error events.

handleStreamWorkflow now maps ClientHTTPError only during stream creation, but validator failures can reject later through session.streamExecution.result. This catch still broadcasts only message, dropping code, name, and httpStatus.

Proposed fix
-    broadcastWorkflowStreamEvent(session, {
-      type: "error",
-      error: error instanceof Error ? error.message : "Stream failed",
-    });
+    broadcastWorkflowStreamEvent(
+      session,
+      error instanceof ClientHTTPError
+        ? {
+            type: "error",
+            error: error.message,
+            code: error.code,
+            name: error.name,
+            httpStatus: error.httpStatus,
+          }
+        : {
+            type: "error",
+            error: error instanceof Error ? error.message : "Stream failed",
+          },
+    );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/server-core/src/handlers/workflow.handlers.ts` around lines 248 -
251, The catch block in handleStreamWorkflow currently calls
broadcastWorkflowStreamEvent with only error.message, losing ClientHTTPError
fields; update the catch to detect ClientHTTPError (or an error object present
on session.streamExecution.result) and include its code, name, and httpStatus
when calling broadcastWorkflowStreamEvent so the emitted event preserves all
ClientHTTPError properties rather than only message; reference
broadcastWorkflowStreamEvent, handleStreamWorkflow, ClientHTTPError, and
session.streamExecution.result to locate and change the broadcast call
accordingly.
packages/core/src/workflow/core.ts (1)

1222-1242: ⚠️ Potential issue | 🟠 Major

Run validators before startAsync commits a running state.

startAsync persists a running execution before delegating to executeInternal, where validation runs. A denied async start therefore returns success and leaves an execution record that later flips to error, instead of blocking pre-execution.

Proposed direction
+      await validateWorkflowExecution(input, {
+        ...options,
+        executionId,
+      });
+
       await executionMemory.setWorkflowState(executionId, {
         id: executionId,
         workflowId: id,
         workflowName: name,
         status: "running",

Then have executeInternal skip the duplicate validation when startAsync already validated.

Also applies to: 3124-3147

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/core/src/workflow/core.ts` around lines 1222 - 1242, Run the
execution validators before persisting the running state in startAsync: move the
executionValidators construction and the await runExecutionValidators(...) call
to occur prior to the code that commits a running execution (i.e., before the
logic that sets the execution record to "running" inside startAsync). Then
update executeInternal to accept a flag (e.g., alreadyValidated or
skipValidation) and short-circuit/skip calling runExecutionValidators when that
flag is true to avoid duplicate validation; reference the existing symbols
workflowExecutionValidators, options?.executionValidators,
runExecutionValidators, startAsync, and executeInternal when making these
changes.
packages/core/src/agent/agent.ts (2)

1183-1186: ⚠️ Potential issue | 🟠 Major

Resolve validators from the effective operation context.

createOperationContext() already builds the merged validator set on OperationContext, but this resolver can skip it when callers pass operationContext separately. That makes direct validation paths miss parent/agent/per-call validators.

Proposed fix
-    const validators = options?.executionValidators?.tools ?? this.executionValidators?.tools;
+    const validators =
+      operationContext?.executionValidators?.tools ??
+      mergeAgentExecutionValidators(this.executionValidators, options?.executionValidators)?.tools;
     if (!validators || validators.length === 0) {
       return;
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/core/src/agent/agent.ts` around lines 1183 - 1186, The validator
lookup is bypassing the merged validators on OperationContext; instead of using
options?.executionValidators?.tools ?? this.executionValidators?.tools, fetch
the effective validators from the resolved OperationContext created by
createOperationContext() (or use operationContext if passed in) so
parent/agent/per-call validators are honored — modify the code that sets the
validators variable (the block referencing options?.executionValidators?.tools
and this.executionValidators?.tools) to read from
operationContext.executionValidators.tools (falling back to options and then
this.executionValidators) so the merged set on OperationContext is used.

1337-1356: ⚠️ Potential issue | 🟠 Major

Strip executionValidators before forwarding AI SDK options.

executionValidators is a VoltAgent-only option, but it still lands in aiSDKOptions and is forwarded to generateText, streamText, generateObject, and streamObject.

Proposed fix pattern
               parentAgentId,
               parentOperationContext,
               hooks,
+              executionValidators: _executionValidators,
               feedback: _feedback,
               maxSteps: userMaxSteps,

Apply the same exclusion in all four option destructures.

Also applies to: 1956-1976, 2860-2879, 3234-3254

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/core/src/agent/agent.ts` around lines 1337 - 1356, The options
destructuring in the agent methods is leaking the VoltAgent-only option
executionValidators into aiSDKOptions so it gets forwarded to AI SDK calls;
update the destructuring that builds aiSDKOptions (the blocks that currently
collect ...aiSDKOptions in the methods handling generateText, streamText,
generateObject, and streamObject) to explicitly extract and exclude
executionValidators (e.g., add executionValidators to the left-hand list
alongside context, _requestHeaders, _feedback, etc.) so executionValidators is
not present in aiSDKOptions before calling
generateText/streamText/generateObject/streamObject.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/server-core/src/handlers/tool.handlers.ts`:
- Around line 207-229: The current call to agent.validateToolExecution passes a
hard-coded empty messages array which prevents validators from falling back to
options.toolContext.messages; update the call so it forwards the actual
toolContext messages (e.g., use executionOptions.toolContext.messages) or omit
the messages property so the validator can use its fallback. Modify the
validateToolExecution invocation near executionOptions and toolContext (symbols:
executionOptions, toolContext, agent.validateToolExecution,
validateToolExecution) to pass the real messages buffer instead of [].

---

Outside diff comments:
In `@packages/core/src/agent/agent.ts`:
- Around line 6392-6406: The duplicate invocation of tool.hooks?.onEnd with the
same parameters causes side effects to run twice on failures; remove the
redundant call so onEnd is awaited only once (keep a single await
tool.hooks?.onEnd? invocation), or centralize the call to happen after
handleToolError returns/throws to ensure validation failures handled by
handleToolError do not cause a second onEnd invocation; update the block around
tool.hooks?.onEnd, voltAgentError, args and executionOptions so only one onEnd
is executed for a given failure path.

In `@packages/core/src/workflow/core.ts`:
- Around line 1113-1114: The validators are using workflowStateStore from const
workflowStateStore = options?.workflowState ?? {}, which is wrong for resumed
runs because the runtime replaces state from
options.resumeFrom.checkpoint.workflowState; update places where validators are
passed workflowStateStore (e.g., validator invocation sites around core.ts lines
where validators are called near the const workflowStateStore declaration and
the similar blocks around the other noted ranges) to instead derive the state
from options.resumeFrom?.checkpoint?.workflowState when options.resumeFrom
exists, falling back to options?.workflowState ?? {} otherwise—ensure all
validator calls (the same symbol usages referenced) receive the resumed
checkpoint state so tenant/permission checks validate against the actual runtime
state.

---

Duplicate comments:
In `@packages/core/src/agent/agent.ts`:
- Around line 1183-1186: The validator lookup is bypassing the merged validators
on OperationContext; instead of using options?.executionValidators?.tools ??
this.executionValidators?.tools, fetch the effective validators from the
resolved OperationContext created by createOperationContext() (or use
operationContext if passed in) so parent/agent/per-call validators are honored —
modify the code that sets the validators variable (the block referencing
options?.executionValidators?.tools and this.executionValidators?.tools) to read
from operationContext.executionValidators.tools (falling back to options and
then this.executionValidators) so the merged set on OperationContext is used.
- Around line 1337-1356: The options destructuring in the agent methods is
leaking the VoltAgent-only option executionValidators into aiSDKOptions so it
gets forwarded to AI SDK calls; update the destructuring that builds
aiSDKOptions (the blocks that currently collect ...aiSDKOptions in the methods
handling generateText, streamText, generateObject, and streamObject) to
explicitly extract and exclude executionValidators (e.g., add
executionValidators to the left-hand list alongside context, _requestHeaders,
_feedback, etc.) so executionValidators is not present in aiSDKOptions before
calling generateText/streamText/generateObject/streamObject.

In `@packages/core/src/workflow/core.ts`:
- Around line 1222-1242: Run the execution validators before persisting the
running state in startAsync: move the executionValidators construction and the
await runExecutionValidators(...) call to occur prior to the code that commits a
running execution (i.e., before the logic that sets the execution record to
"running" inside startAsync). Then update executeInternal to accept a flag
(e.g., alreadyValidated or skipValidation) and short-circuit/skip calling
runExecutionValidators when that flag is true to avoid duplicate validation;
reference the existing symbols workflowExecutionValidators,
options?.executionValidators, runExecutionValidators, startAsync, and
executeInternal when making these changes.

In `@packages/server-core/src/handlers/workflow.handlers.ts`:
- Around line 248-251: The catch block in handleStreamWorkflow currently calls
broadcastWorkflowStreamEvent with only error.message, losing ClientHTTPError
fields; update the catch to detect ClientHTTPError (or an error object present
on session.streamExecution.result) and include its code, name, and httpStatus
when calling broadcastWorkflowStreamEvent so the emitted event preserves all
ClientHTTPError properties rather than only message; reference
broadcastWorkflowStreamEvent, handleStreamWorkflow, ClientHTTPError, and
session.streamExecution.result to locate and change the broadcast call
accordingly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: bd3ffdbb-ddad-467e-b7fa-269c60855e23

📥 Commits

Reviewing files that changed from the base of the PR and between f5dbf49 and ec3bab9.

📒 Files selected for processing (17)
  • .changeset/execution-validators.md
  • packages/core/src/agent/agent.spec.ts
  • packages/core/src/agent/agent.ts
  • packages/core/src/agent/errors/client-http-errors.ts
  • packages/core/src/agent/errors/index.ts
  • packages/core/src/agent/types.ts
  • packages/core/src/execution-validation.ts
  • packages/core/src/index.ts
  • packages/core/src/workflow/core.spec.ts
  • packages/core/src/workflow/core.ts
  • packages/core/src/workflow/types.ts
  • packages/server-core/src/handlers/tool.handlers.spec.ts
  • packages/server-core/src/handlers/tool.handlers.ts
  • packages/server-core/src/handlers/workflow.handlers.spec.ts
  • packages/server-core/src/handlers/workflow.handlers.ts
  • website/docs/agents/tools.md
  • website/docs/workflows/overview.md
✅ Files skipped from review due to trivial changes (7)
  • .changeset/execution-validators.md
  • website/docs/agents/tools.md
  • packages/core/src/workflow/types.ts
  • packages/server-core/src/handlers/workflow.handlers.spec.ts
  • website/docs/workflows/overview.md
  • packages/core/src/agent/errors/client-http-errors.ts
  • packages/core/src/agent/agent.spec.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • packages/core/src/agent/types.ts
  • packages/server-core/src/handlers/tool.handlers.spec.ts
  • packages/core/src/index.ts

Comment on lines 207 to 229
// Build a minimal execution context for tools
const result = await tool.execute(parsedInput, {
const executionOptions = {
userId,
conversationId,
context: contextMap,
systemContext: new Map(),
abortController,
toolContext: {
name: tool.name,
callId: generateId(),
callId: toolCallId,
messages: [],
abortSignal: abortController.signal,
},
logger,
};

await agent.validateToolExecution?.({
tool,
args: parsedInput,
options: executionOptions,
toolCallId,
messages: [],
});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Don’t hard-code empty validator messages.

Passing messages: [] bypasses Agent.validateToolExecution’s fallback to options.toolContext.messages, so direct tool validators cannot inspect conversation/request history even when it is available.

Proposed fix
+    const requestMessages =
+      Array.isArray(body?.messages)
+        ? body.messages
+        : Array.isArray(contextMap.get("messages"))
+          ? (contextMap.get("messages") as unknown[])
+          : [];
+
     // Build a minimal execution context for tools
     const executionOptions = {
       userId,
       conversationId,
       context: contextMap,
@@
       toolContext: {
         name: tool.name,
         callId: toolCallId,
-        messages: [],
+        messages: requestMessages,
         abortSignal: abortController.signal,
       },
       logger,
     };
@@
       args: parsedInput,
       options: executionOptions,
       toolCallId,
-      messages: [],
     });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Build a minimal execution context for tools
const result = await tool.execute(parsedInput, {
const executionOptions = {
userId,
conversationId,
context: contextMap,
systemContext: new Map(),
abortController,
toolContext: {
name: tool.name,
callId: generateId(),
callId: toolCallId,
messages: [],
abortSignal: abortController.signal,
},
logger,
};
await agent.validateToolExecution?.({
tool,
args: parsedInput,
options: executionOptions,
toolCallId,
messages: [],
});
// Build a minimal execution context for tools
const requestMessages =
Array.isArray(body?.messages)
? body.messages
: Array.isArray(contextMap.get("messages"))
? (contextMap.get("messages") as unknown[])
: [];
const executionOptions = {
userId,
conversationId,
context: contextMap,
systemContext: new Map(),
abortController,
toolContext: {
name: tool.name,
callId: toolCallId,
messages: requestMessages,
abortSignal: abortController.signal,
},
logger,
};
await agent.validateToolExecution?.({
tool,
args: parsedInput,
options: executionOptions,
toolCallId,
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/server-core/src/handlers/tool.handlers.ts` around lines 207 - 229,
The current call to agent.validateToolExecution passes a hard-coded empty
messages array which prevents validators from falling back to
options.toolContext.messages; update the call so it forwards the actual
toolContext messages (e.g., use executionOptions.toolContext.messages) or omit
the messages property so the validator can use its fallback. Modify the
validateToolExecution invocation near executionOptions and toolContext (symbols:
executionOptions, toolContext, agent.validateToolExecution,
validateToolExecution) to pass the real messages buffer instead of [].

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.

Add pre-execution validation for tool and workflow execution

1 participant