Repository navigation
feat(plugins): execute_action runs one action through its operation tool, and operation tools run run_workflow's checks - #1097
Merged
frontegg-david merged 5 commits intoNov 4, 2026
Conversation
… and operation tools run run_workflow's checks
A model that needed one action had to write an AgentScript program and needed
the @enclave-vm sandbox installed. Now:
- execute_action takes { actionId, input } and runs the action through its
internal operation tool, with the caller's auth, so the tools:call-tool
hooks run for it. Its structured content is the action's data; a failure
(authority denied, invalid input, timeout, output schema, or an error
status with the API's error body) is an isError result whose structured
content is { ok: false, status, data?, error? }. An unknown action gets
run_workflow's message. It needs no sandbox. With
exposeOperationsAsInternalTools: false it runs the same checks itself.
- An operation tool, which server code reaches with this.callTool, now runs
the shared path a run_workflow call takes: the output schema check, the
audit records, and the error body redacted and capped per
outbound.errorBodies. Its answer keeps the { ok, status, data, contentType }
shape with no isError.
- search_skill, load_skill and run_workflow, the SKILL.md actions hint and
the initialize instructions point one action to execute_action and a chain
to run_workflow, each description within 1024 characters.
…s execute-action exits 0 or 1 The production-settings app lists four tools, compares execute_action's results and errors across streamable HTTP and stdio, and runs it against the mock billing API. A CLI built with the plugin runs execute-action against an API on loopback: exit 0 with the data, exit 1 for a 404 and an unknown action.
… into feat/skilled-execute-action # Conflicts: # plugins/plugin-skilled-openapi/README.md # plugins/plugin-skilled-openapi/src/tools/operation-tool.factory.ts # plugins/plugin-skilled-openapi/src/tools/run-workflow.tool.ts
…and raises ToolCredentialsRequiredError without one A binding the authProviders option maps sends the credential the caller connected through execute_action too. A caller who hasn't connected it gets the operation tool's ToolCredentialsRequiredError (-32001, data.authUrl), not a failed result, and nothing is sent; without operation tools the error names execute_action. The README says so.
frontegg-david
marked this pull request as ready for review
November 4, 2026 20:04
Contributor
Author
|
@coderabbitai review |
Contributor
|
Warning Review limit reachedYour organization has reached its usage spending cap. Adjust your spending cap in the billing tab. Next included review available in 13 minutes. View limit details
|
Contributor
Action not completed
|
alexmercerpo
approved these changes
Nov 4, 2026
… name, never the bare operationId When <bundleId>.<operationId> was longer than 64 characters, the operation's tool fell back to the bare operationId, so execute_action, which calls the tool by that name, could reach a server tool named like the operation. The name now keeps its start and ends with a hash of the full name: unique per operation, and never another tool's name.
Contributor
Performance Test ResultsStatus: ✅ All tests passed Summary
Total: 101 tests across 21 projects 📊 View full report in workflow run Generated at: 2026-11-04T20:23:20.487Z |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What and why
A model that needed one action from a skill had to write an AgentScript program for
run_workflow. That meant one more thing to get right, and it needed the optional@enclave-vmsandbox installed. This PR adds a fourth tool,execute_action, that runs one action directly.It also fixes the hidden per-operation tools. They skipped checks that a
run_workflowcall runs, so an action could pass through one path and fail through the other.This PR stacks on #1096 (per-caller credentials), which stacks on #1095.
execute_action { actionId, input? }actionIdacross the loaded skills, asrun_workflowdoes. An unknown action gets the same error:unknown action "x" — run search_skill then load_skill to discover available actions.<bundleId>.<operationId>, through the SDK'stools:call-toolflow, with the caller's auth. So everytools:call-toolhook runs for the operation's own call. A test checks that a hook sees bothexecute_actionandacme:billing.getInvoice.structuredContentis the action's data. A JSON object is passed as it is; any other value goes undervalue, as the SDK does for a tool without an output schema. A bodyless answer (204) has no structured content, and its text names the status.isError: true:callToolwould throw: authority denied, input validation, output schema, timeout, unknown action, oraction "x" failed with status 422: <body>;structuredContentis{ ok: false, status, data?, error? }, the same shapecallTool(..., { throwOnError: false })returns.datais the API's error body, redacted and capped, or left out withoutbound.errorBodies: 'status-only'.execute-actioncommand exits 1.ToolCredentialsRequiredError(-32001,data.authUrl), not a failed result. The error passes through as the operation tool raised it, sodata.toolnames that tool (e2e:billing.getAccountPlan), as it does for a server's ownthis.callTool.@enclave-vmat all. Without the sandbox,run_workflowanswers "workflow execution unavailable", andexecute_actionstill works.run_workflow's:readOnlyHint: false,destructiveHint: true,openWorldHint: true. Tool annotations are static, and one tool runs every action, so a per-method hint (GET is read-only) would not be honest.exposeOperationsAsInternalTools: falsethere is no operation tool to call. In that caseexecute_actionruns the same shared path itself, with the same checks, audit records and error body, rather than refusing. That option exists for very large bundles, and turning it off shouldn't turnexecute_actionoff.tools:call-toolhooks still run for theexecute_actioncall itself.One shared path for an action
runSkillActioninexecutor/run-skill-action.tswraps the existingexecuteSkillActionandactionOutcome. It covers authorization across all listing skills, the input schema, the outbound checks and request, the output schema, the audit records, and the error body peroutbound.errorBodies.runSkillAction. It replaces a 100-line copy that had drifted:outbound.errorBodies.run_workflowbuilds its dependencies through the sameskillActionDepsOfhelper.Server code calling an operation tool.
this.callTool('<bundleId>.<operationId>', input)keeps its contract on purpose: structured content{ ok, status, data?, contentType?, error? }, noisError, and no throw for an action failure. What changes:ok: falsewith anerror;'status-only'.Discovery text. These now say: one action →
execute_action; several chained →run_workflow:search_skill,load_skillandrun_workflowdescriptions;SKILL.md"## Actions" hint;initializeinstructions line.All four tool descriptions are within the 1024-character budget, and a test checks it:
search_skill(static part)load_skillexecute_actionrun_workflowKnown limit, documented, not fixed here. Operation tools are built on the server's providers. So when the plugin is installed in an
@App, aSkilledOpenApiCredentialResolverdeclared in@App({ providers })reachesrun_workflowbut not the operation tools, and so notexecute_action. A resolver ininit({ providers })or@FrontMcp({ providers })reaches all three, and tests check both. Fixing it means building the operation tools on the plugin's own provider registry, which needs SDK support.User-facing change
execute_action { actionId, input? }.tools/listhas four tools:search_skill,load_skill,execute_action,run_workflow.execute-action --action-id <id> --input '<json>'command: exit 0 with the data, exit 1 on a failed action.this.callTool('<bundleId>.<operationId>')) check the output schema, write audit records, and redact, cap or drop the error body peroutbound.errorBodies.search_skill,load_skillandrun_workflowdescriptions, theSKILL.mdactions hint, and theinitializeinstructions line.tools/list:load_skill, run_workflow, search_skill→execute_action, load_skill, run_workflow, search_skill.run_workflow { script: "return await callTool('getInvoice', { id: 'inv_1' })" }and@enclave-vm→execute_action { actionId: 'getInvoice', input: { id: 'inv_1' } }, with no sandbox.{ ok: true, data }→{ ok: false, status, contentType, error: 'upstream response failed output schema: …' }.authority-check-pass|failandhttp-call-success|failure, like arun_workflowcall.dataon an HTTP failure was the whole body → redacted and capped at 2 KB, or left out witherrorBodies: 'status-only'.This server has 2 skills: load one by name with \load_skill`, or find one with `search_skill`.→… ; then run an action with `execute_action`, or chain several with `run_workflow`.`SKILL.mdactions hint:Run these with \run_workflow`: …→Run one with `execute_action` ({ actionId, input }), or chain several with `run_workflow`: …`exposeOperationsAsInternalTools, operation tools, credentials, audit;execute-actioncommand and its exit codes;frontmcp-deployment/references/deploy-to-cloudflare-skills-only.md: four meta-tools,execute_action's result and failure, theSKILL.mdactions line.frontmcp-deployment/references/build-for-cli.md:execute-actionand a troubleshooting row.frontmcp-config/references/configure-skills-http.md: the new instructions line.frontmcp-extensibility/references/skill-audit-log.md:execute_actionand operation tool calls are audited.frontmcp-development/references/create-plugin.md: "four meta-tools".package.jsondescription, and the demo app's README.Tests
Added:
execute-action.integration.spec.ts(22 tests, through a real server):'status-only', a per-operation timeout, an output-schema failure;exposeOperationsAsInternalTools: false;'status-only'and audit, with the{ ok, status, data, contentType }contract.execute-action.no-enclave.integration.spec.ts(2): with@enclave-vm/core/workerfailing to import,run_workflowanswers "unavailable" andexecute_actionruns.caller-credentials.integration.spec.ts(+3): the caller's own connected credential throughexecute_action, andToolCredentialsRequiredErrorwith and without operation tools.plugin-registration.integration.spec.ts:execute_actionuses the resolver from@FrontMcp({ providers })and frominit({ providers }).bundle-loading.integration.spec.ts:execute_actionanswers why no bundle is loaded.demo-e2e-skilled-openapi:tools/listhas four tools;execute_actioncalls (two succeed, four fail);execute_actionagainst the HTTPS mock billing API: success with the credential, a 404 body, authority andunprotectedOpsdenials, output schema, timeout, unknown action;execute_action;data.authUrl.demo-e2e-cli-exec: a CLI built with the plugin runsexecute-actionagainst an API on loopback. It exits 0 with the data, and 1 for a 404 and for an unknown action. A newrunCliBundleAsynchelper runs the CLI without blocking the test process that serves the API.Run:
npx nx run-many -t build -p sdk,adapters,plugin-skilled-openapi,testing,cli: passed.Plugin suite (
npx jestinplugins/plugin-skilled-openapi): 44 suites, 636 tests passed.demo-e2e-skilled-openapi(all specs): 6 suites, 60 tests passed.demo-e2e-cli-exec,cli-skilled-openapi.e2e.spec.ts: 14 tests passed (cli-login-authoritiesskips on macOS).npx nx test skills: 3 suites, 249 tests passed.npx nx run-many -t typecheck -p plugin-skilled-openapi,demo-e2e-skilled-openapi,demo-e2e-cli-exec,demo: passed.npx tsc --noEmit -p plugins/plugin-skilled-openapi/tsconfig.lib.json: passed.node scripts/fix-unused-imports.mjs, prettier and eslint on the changed files: clean.No change to
libs/sdk, so the bundle-size e2e doesn't apply.A long operation tool name (
<bundleId>.<operationId>over 64 characters) used to fall back to the bareoperationId, soexecute_actioncould reach a server tool named like the operation. It now keeps its start and ends with a 16-character hash of the full name. A test checks that it is unique per operation and never the bare id.