Skip to content

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 into
feat/skilled-caller-credentialsfrom
feat/skilled-execute-action
Nov 4, 2026
Merged

frontegg-david merged 5 commits into
feat/skilled-caller-credentialsfrom
feat/skilled-execute-action

Conversation

@frontegg-david

@frontegg-david frontegg-david commented Nov 4, 2026 •

Copy link
Copy Markdown
Contributor

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-vm sandbox 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_workflow call 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? }

  • It resolves actionId across the loaded skills, as run_workflow does. An unknown action gets the same error: unknown action "x" — run search_skill then load_skill to discover available actions.
  • It runs the action by calling the action's internal operation tool, <bundleId>.<operationId>, through the SDK's tools:call-tool flow, with the caller's auth. So every tools:call-tool hook runs for the operation's own call. A test checks that a hook sees both execute_action and acme:billing.getInvoice.
  • On success, structuredContent is the action's data. A JSON object is passed as it is; any other value goes under value, 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.
  • On failure it answers isError: true:
    • the text is the message a script's callTool would throw: authority denied, input validation, output schema, timeout, unknown action, or action "x" failed with status 422: <body>;
    • structuredContent is { ok: false, status, data?, error? }, the same shape callTool(..., { throwOnError: false }) returns. data is the API's error body, redacted and capped, or left out with outbound.errorBodies: 'status-only'.
  • MCP clients see a failed call, and the generated CLI's execute-action command exits 1.
  • A caller without a connected credential (from feat(plugins): a Skilled OpenAPI binding can send each caller's own connected credential #1096) gets ToolCredentialsRequiredError (-32001, data.authUrl), not a failed result. The error passes through as the operation tool raised it, so data.tool names that tool (e2e:billing.getAccountPlan), as it does for a server's own this.callTool.
  • It doesn't import @enclave-vm at all. Without the sandbox, run_workflow answers "workflow execution unavailable", and execute_action still works.
  • Annotations are 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.
  • With exposeOperationsAsInternalTools: false there is no operation tool to call. In that case execute_action runs 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 turn execute_action off. tools:call-tool hooks still run for the execute_action call itself.

One shared path for an action

  • A new runSkillAction in executor/run-skill-action.ts wraps the existing executeSkillAction and actionOutcome. 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 per outbound.errorBodies.
  • The operation tool's executor is now one call to runSkillAction. It replaces a 100-line copy that had drifted:
    • it had no output-schema check;
    • it wrote no audit records;
    • on an HTTP failure it returned the API's whole error body, ignoring outbound.errorBodies.
  • run_workflow builds its dependencies through the same skillActionDepsOf helper.

Server code calling an operation tool. this.callTool('<bundleId>.<operationId>', input) keeps its contract on purpose: structured content { ok, status, data?, contentType?, error? }, no isError, and no throw for an action failure. What changes:

  • an answer that fails the output schema is now ok: false with an error;
  • each call writes audit records;
  • an error body is redacted, capped at 2 KB, and left out with 'status-only'.

Discovery text. These now say: one action → execute_action; several chained → run_workflow:

  • search_skill, load_skill and run_workflow descriptions;
  • the SKILL.md "## Actions" hint;
  • the initialize instructions line.

All four tool descriptions are within the 1024-character budget, and a test checks it:

Tool Before After
search_skill (static part) 1189 1011
load_skill 974 1009
execute_action (new) 831
run_workflow 1008 1015

Known limit, documented, not fixed here. Operation tools are built on the server's providers. So when the plugin is installed in an @App, a SkilledOpenApiCredentialResolver declared in @App({ providers }) reaches run_workflow but not the operation tools, and so not execute_action. A resolver in init({ 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

  • What changed:
    • New meta-tool execute_action { actionId, input? }. tools/list has four tools: search_skill, load_skill, execute_action, run_workflow.
    • The generated CLI gets an execute-action --action-id <id> --input '<json>' command: exit 0 with the data, exit 1 on a failed action.
    • Operation tools (this.callTool('<bundleId>.<operationId>')) check the output schema, write audit records, and redact, cap or drop the error body per outbound.errorBodies.
    • New wording in the search_skill, load_skill and run_workflow descriptions, the SKILL.md actions hint, and the initialize instructions line.
  • Before → after:
    • tools/list: load_skill, run_workflow, search_skill → execute_action, load_skill, run_workflow, search_skill.
    • Running one action needed run_workflow { script: "return await callTool('getInvoice', { id: 'inv_1' })" } and @enclave-vm → execute_action { actionId: 'getInvoice', input: { id: 'inv_1' } }, with no sandbox.
    • An operation tool's answer that failed the output schema came back as { ok: true, data } → { ok: false, status, contentType, error: 'upstream response failed output schema: …' }.
    • An operation tool call wrote no audit records → it writes authority-check-pass|fail and http-call-success|failure, like a run_workflow call.
    • An operation tool's data on an HTTP failure was the whole body → redacted and capped at 2 KB, or left out with errorBodies: 'status-only'.
    • Instructions line: 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.md actions hint: Run these with \run_workflow`: …→Run one with `execute_action` ({ actionId, input }), or chain several with `run_workflow`: …`
  • frontmcp.dev pages affected (if known):
    • https://frontmcp.dev/reference/plugins/skilled-openapi: the tools, exposeOperationsAsInternalTools, operation tools, credentials, audit;
    • the CLI build page: the execute-action command and its exit codes;
    • the skills-only / Cloudflare managed-mode page: the meta-tool list.
  • Skills catalog entries updated:
    • frontmcp-deployment/references/deploy-to-cloudflare-skills-only.md: four meta-tools, execute_action's result and failure, the SKILL.md actions line.
    • frontmcp-deployment/references/build-for-cli.md: execute-action and a troubleshooting row.
    • frontmcp-config/references/configure-skills-http.md: the new instructions line.
    • frontmcp-extensibility/references/skill-audit-log.md: execute_action and operation tool calls are audited.
    • frontmcp-development/references/create-plugin.md: "four meta-tools".
    • The plugin README, its package.json description, and the demo app's README.

Tests

Added:

  • execute-action.integration.spec.ts (22 tests, through a real server):
    • the four tools, the description budget and pointers, and the annotations;
    • success, through the operation tool with its hook; an array and a 204;
    • an unknown action, authority denied, and an operation a public and an admin-only skill both list (fix(plugins): an operation several skills list runs for a caller any of them admits #1087);
    • invalid input, a 422 with its body and with 'status-only', a per-operation timeout, an output-schema failure;
    • audit records, and no credential in any result;
    • exposeOperationsAsInternalTools: false;
    • operation-tool parity for server code: output schema, cap, 'status-only' and audit, with the { ok, status, data, contentType } contract.
  • execute-action.no-enclave.integration.spec.ts (2): with @enclave-vm/core/worker failing to import, run_workflow answers "unavailable" and execute_action runs.
  • caller-credentials.integration.spec.ts (+3): the caller's own connected credential through execute_action, and ToolCredentialsRequiredError with and without operation tools.
  • plugin-registration.integration.spec.ts: execute_action uses the resolver from @FrontMcp({ providers }) and from init({ providers }).
  • bundle-loading.integration.spec.ts: execute_action answers why no bundle is loaded.
  • E2E, demo-e2e-skilled-openapi:
    • tools/list has four tools;
    • parity over streamable HTTP and stdio for six execute_action calls (two succeed, four fail);
    • execute_action against the HTTPS mock billing API: success with the credential, a 404 body, authority and unprotectedOps denials, output schema, timeout, unknown action;
    • the shared-operation check through execute_action;
    • a tampered bundle;
    • over HTTP, a caller without a connected credential gets -32001 with data.authUrl.
  • E2E, demo-e2e-cli-exec: a CLI built with the plugin runs execute-action against an API on loopback. It exits 0 with the data, and 1 for a 404 and for an unknown action. A new runCliBundleAsync helper 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 jest in plugins/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-authorities skips 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 bare operationId, so execute_action could 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.

… 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
frontegg-david marked this pull request as ready for review November 4, 2026 20:04
@frontegg-david

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Nov 4, 2026 •

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

Your organization has reached its usage spending cap. Adjust your spending cap in the billing tab.

Next included review available in 13 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used all 2 included reviews currently available.

Learn how review limits work.

Review configuration:

Run configuration
Commits

Reviewing files that changed from the base of the PR and between 391c541 and a567e51.

Files selected for processing: 39
  • apps/demo/src/skilled-openapi-fixtures/README.md
  • apps/demo/src/skilled-openapi-fixtures/skilled-openapi-demo.app.ts
  • apps/e2e/demo-e2e-cli-exec/e2e/cli-skilled-openapi.e2e.spec.ts
  • apps/e2e/demo-e2e-cli-exec/e2e/helpers/exec-cli.ts
  • apps/e2e/demo-e2e-cli-exec/e2e/helpers/skilled-cli-project.ts
  • apps/e2e/demo-e2e-skilled-openapi/e2e/bundle-signing.e2e.spec.ts
  • apps/e2e/demo-e2e-skilled-openapi/e2e/caller-credentials.e2e.spec.ts
  • apps/e2e/demo-e2e-skilled-openapi/e2e/discovery.e2e.spec.ts
  • apps/e2e/demo-e2e-skilled-openapi/e2e/execution.e2e.spec.ts
  • apps/e2e/demo-e2e-skilled-openapi/e2e/helpers/skilled-client.ts
  • apps/e2e/demo-e2e-skilled-openapi/e2e/parity.e2e.spec.ts
  • libs/skills/catalog/frontmcp-config/references/configure-skills-http.md
  • libs/skills/catalog/frontmcp-deployment/references/build-for-cli.md
  • libs/skills/catalog/frontmcp-deployment/references/deploy-to-cloudflare-skills-only.md
  • libs/skills/catalog/frontmcp-development/references/create-plugin.md
  • libs/skills/catalog/frontmcp-extensibility/references/skill-audit-log.md
  • plugins/plugin-skilled-openapi/README.md
  • plugins/plugin-skilled-openapi/package.json
  • plugins/plugin-skilled-openapi/src/__tests__/authorization.integration.spec.ts
  • plugins/plugin-skilled-openapi/src/__tests__/bundle-loading.integration.spec.ts
  • plugins/plugin-skilled-openapi/src/__tests__/caller-credentials.integration.spec.ts
  • plugins/plugin-skilled-openapi/src/__tests__/execute-action.integration.spec.ts
  • plugins/plugin-skilled-openapi/src/__tests__/execute-action.no-enclave.integration.spec.ts
  • plugins/plugin-skilled-openapi/src/__tests__/operation-tool.factory.spec.ts
  • plugins/plugin-skilled-openapi/src/__tests__/plugin-registration.integration.spec.ts
  • plugins/plugin-skilled-openapi/src/__tests__/skill-catalog-pointers.spec.ts
  • plugins/plugin-skilled-openapi/src/executor/action-outcome.ts
  • plugins/plugin-skilled-openapi/src/executor/run-skill-action.ts
  • plugins/plugin-skilled-openapi/src/registry/hidden-op.registry.ts
  • plugins/plugin-skilled-openapi/src/skilled-openapi.plugin.ts
  • plugins/plugin-skilled-openapi/src/skilled-openapi.types.ts
  • plugins/plugin-skilled-openapi/src/sync/bundle-sync.service.ts
  • plugins/plugin-skilled-openapi/src/tools/execute-action.schema.ts
  • plugins/plugin-skilled-openapi/src/tools/execute-action.tool.ts
  • plugins/plugin-skilled-openapi/src/tools/load-skill.schema.ts
  • plugins/plugin-skilled-openapi/src/tools/operation-tool.factory.ts
  • plugins/plugin-skilled-openapi/src/tools/run-workflow.schema.ts
  • plugins/plugin-skilled-openapi/src/tools/run-workflow.tool.ts
  • plugins/plugin-skilled-openapi/src/tools/search-skill.schema.ts
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

@coderabbitai

coderabbitai Bot commented Nov 4, 2026 •

Copy link
Copy Markdown
Contributor
Action not completed

Review rate limited.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

… 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.
@github-actions

github-actions Bot commented Nov 4, 2026

Copy link
Copy Markdown
Contributor

Performance Test Results

Status: ✅ All tests passed

Summary

Project Tests Passed Warnings Failed Leaks
✅ demo-e2e-agents 4 4 0 0 0
✅ demo-e2e-cache 11 11 0 0 0
✅ demo-e2e-codecall 4 4 0 0 0
✅ demo-e2e-config 4 4 0 0 0
✅ demo-e2e-direct 3 3 0 0 0
✅ demo-e2e-elicitation 1 1 0 0 0
✅ demo-e2e-errors 4 4 0 0 0
✅ demo-e2e-hooks 3 3 0 0 0
✅ demo-e2e-multiapp 4 4 0 0 0
✅ demo-e2e-notifications 3 3 0 0 0
✅ demo-e2e-openapi 2 2 0 0 0
✅ demo-e2e-providers 4 4 0 0 0
✅ demo-e2e-public 4 4 0 0 0
✅ demo-e2e-redis 15 15 0 0 0
✅ demo-e2e-remember 4 4 0 0 0
✅ demo-e2e-remote 5 5 0 0 0
✅ demo-e2e-serverless 2 2 0 0 0
✅ demo-e2e-skills 15 15 0 0 0
✅ demo-e2e-standalone 2 2 0 0 0
✅ demo-e2e-transport-recreation 3 3 0 0 0
✅ demo-e2e-ui 4 4 0 0 0

Total: 101 tests across 21 projects

📊 View full report in workflow run


Generated at: 2026-11-04T20:23:20.487Z
Commit: 15869578

@frontegg-david
frontegg-david merged commit fe6d542 into release/1.11.x Nov 4, 2026
46 checks passed
@frontegg-david
frontegg-david deleted the feat/skilled-execute-action branch November 4, 2026 23:23
frontegg-david added a commit that referenced this pull request Nov 4, 2026
…s operation tool, and operation tools run run_workflow's checks (#1097)

Cherry-picked from #1097 (merged to release/1.11.x)
Original commit: fe6d542
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.

2 participants