Skip to content

fix(plugins,edge): managed mode takes confirm, and the audit log says whether the user confirmed a call - #1139

Merged
alexmercerpo merged 4 commits into
fix/skilled-push-token-body-bindingfrom
fix/skilled-confirm-edge-audit
Nov 5, 2026
Merged

alexmercerpo merged 4 commits into
fix/skilled-push-token-body-bindingfrom
fix/skilled-confirm-edge-audit

Conversation

@frontegg-david

Copy link
Copy Markdown
Contributor

Stacks on #1138.

What and why

The Skilled OpenAPI plugin's confirm option names actions that ask the user before their request. Review found three gaps around it:

  1. Edge managed mode dropped confirm. createEdgeMcp({ managed }) builds the plugin's options from managed, and confirm wasn't one of them. A managed Worker had no way to stop a destructive action, so every action ran without asking.
  2. The audit log didn't say whether a call was confirmed. A confirmed call wrote the same records as a call no rule names. An auditor couldn't tell "the user said yes" from "nobody asked" or "it ran unasked because the client can't ask".
  3. load_skill didn't flag the actions that ask. The model learned an action asks only when it called it.

managed.confirm. ManagedEdgeOptions has a confirm field with the plugin's input type, passed to the plugin as it is. The plugin's schema checks it, so an invalid value refuses to start like any other plugin option: 500 CONFIG_INVALID, and the log names the field (confirm.methods.0). The plugin's rules for a client that can't ask hold on the edge:

  • Without elicitation: { enabled: true }, an action confirm names is refused: the user's confirmation is needed for voidInvoice, and this server can't ask for it: elicitation is disabled; nothing was sent. With whenUnsupported: 'allow' it runs unasked.
  • A client that sends no protocol version (a 2025 client without elicitation) is refused with this client can't ask for it: it doesn't support elicitation.
  • Where elicitation can be enabled, a 2026-07-28 client that declares elicitation gets an input_required result with the question, and its retry with confirmed: true sends the request.

On a Cloudflare Worker, elicitation: { enabled: true } doesn't start today: the SDK refuses an in-memory elicitation store on an edge runtime (Elicitation requires distributed storage when running on Edge runtime), and elicitation.redis takes Redis only. So a managed Worker refuses the actions confirm names, even for a client that declares elicitation, or runs them unasked with 'allow'. The READMEs and the catalog say so. Letting a Worker ask needs an SDK change (an elicitation store a Worker can use, or none for 2026-07-28 input requests), left for a follow-up.

confirmation on the audit record. SkillAuditRecord has a new optional confirmation field, typed SkillAuditConfirmation, named after what the code tells apart:

Value When
not_required No confirm rule names the action
confirmed The user said yes
skipped The client can't ask, and whenUnsupported: 'allow' sent the request unasked
declined The user said no, or cancelled
unanswered No answer came: the client can't ask ('refuse'), the question timed out, or a script reached the action by an id it didn't name
  • It is set on a call's http-call-success or http-call-failure once its input passed the schema check. The authority-check-* records and an input-schema failure come before the confirmation and leave it out.
  • A run_workflow's action calls carry the workflow's answer: confirmed (or skipped) for the actions its question named, not_required for the others. A declined workflow runs no action, so it still writes no records.
  • The CLI's --yes answers the question on the client, so the server records confirmed. It can't tell --yes apart from a user's yes.
  • The field is optional and signed with the rest of the record, so older records stay valid and an older verifyChain verifies new ones.
  • In the plugin, a confirmation now returns a decision ({ confirmation } or { confirmation, refusal }) in place of a refusal string, and the executor puts it on the records written after it.

load_skill marks the actions that ask. Each action a confirm rule names has asksForConfirmation: true. The tool's description stays as it was: a line about the field would take it past the 1,024-character budget.

Scenario: E7 (asking before a destructive action), on the managed edge, and in the audit log.

User-facing change

  • What changed:
    • createEdgeMcp({ managed: { confirm } }) takes the plugin's confirm option and passes it through.
    • The skill audit record has an optional confirmation: not_required, confirmed, skipped, declined or unanswered. @frontmcp/adapters/skills exports its type, SkillAuditConfirmation, and SkillAuditWriteContext takes an optional confirmation.
    • load_skill adds asksForConfirmation: true to each action that asks the user first.
  • Before → after:
    • A managed Worker with a DELETE action: always sent → with confirm: { methods: ['DELETE'] }, refused with the user's confirmation is needed for voidInvoice, and this server can't ask for it: elicitation is disabled; nothing was sent.
    • A confirmed voidInvoice call's http-call-success record: { phase, status, ... } → { phase, status, ..., confirmation: 'confirmed' }. A declined one's http-call-failure has confirmation: 'declined'.
    • load_skill's action: { actionId: 'voidInvoice', summary, ... } → { actionId: 'voidInvoice', summary, ..., asksForConfirmation: true }.
  • frontmcp.dev pages affected (if known):
  • Skills catalog entries updated:
    • frontmcp-extensibility/references/skill-audit-log.md
    • frontmcp-deployment/references/deploy-to-cloudflare-skills-only.md
    • frontmcp-config/references/configure-elicitation.md

The plugin's and the edge's READMEs are updated too.

Tests

Added:

  • @frontmcp/adapters, audit-writer.spec.ts: the writer records the confirmation a call passes, leaves it out of a record without one, and an edited confirmation breaks the chain.
  • Plugin, action-confirmation.spec.ts: each decision (confirmed, declined for a decline, a cancel or confirmed: false, unanswered for a timeout or a client that can't ask, skipped with 'allow'); a workflow's answer carried to its asked actions, skipped included; unanswered for an action a workflow didn't ask about.
  • Plugin, execute-skill-action.spec.ts: which records carry the field on each path (not_required, confirmed, skipped, a failed call after a yes, unanswered, and none before the input check).
  • Plugin, confirm-actions.integration.spec.ts (a real server and client): the field on each path for execute_action and run_workflow, and load_skill's marker with and without confirm.
  • Edge:
    • managed.spec.ts: confirm mapped as it is, with the plugin's default whenUnsupported, and left out when unset.
    • index.spec.ts (a real scope): a managed edge refuses for a client that can't ask, runs unasked with 'allow', refuses with elicitation disabled, asks a 2026-07-28 client through an input request and sends once confirmed, marks the action in load_skill, and refuses to start with an invalid confirm, naming the field.
  • demo-e2e-cloudflare, the managed Worker in workerd, now with confirm: { methods: ['DELETE'] } and a voidInvoice action:
    • execute_action and run_workflow refuse it, sending nothing;
    • a 2026-07-28 client that declares elicitation is refused too, without a question;
    • an action the rule doesn't name runs;
    • load_skill marks it.
  • demo-e2e-skilled-openapi, confirm-destructive.e2e.spec.ts: load_skill marks the DELETE action and no other.

Run:

  • npx nx run-many -t build -p sdk,adapters,plugin-skilled-openapi,edge,testing: passed.
  • @frontmcp/adapters: 63 suites, 1136 tests, passed.
  • @frontmcp/plugin-skilled-openapi: 53 suites, 844 tests, passed.
  • @frontmcp/edge: 10 suites, 118 tests, passed.
  • @frontmcp/skills: 3 suites, 249 tests, passed.
  • npx nx test demo-e2e-cloudflare: 3 suites, 37 tests, passed.
  • npx nx test demo-e2e-skilled-openapi: 11 suites, 99 tests, passed.
  • npx nx run <project>:typecheck for adapters, plugin-skilled-openapi, edge and demo-e2e-skilled-openapi, and npx tsc --noEmit -p <lib>/tsconfig.lib.json for adapters, plugin-skilled-openapi and edge: no errors.
  • Prettier and ESLint on the changed files: clean.

The SDK is unchanged, so the bundle-size e2e doesn't apply.

… whether the user confirmed a call

createEdgeMcp({ managed }) built the Skilled OpenAPI plugin without the
confirm option, so a managed Worker couldn't stop a destructive action.
managed.confirm now goes to the plugin, whose schema checks it. On a
Cloudflare Worker, where elicitation can't be enabled today, the actions it
names are refused (or run unasked with whenUnsupported: 'allow').

The audit records of a call didn't say whether the user confirmed it. Its
http-call-* records now carry an optional confirmation: not_required,
confirmed, skipped, declined or unanswered. A workflow's action calls carry
the workflow's answer. load_skill marks the actions that ask with
asksForConfirmation: true.
@coderabbitai

coderabbitai Bot commented Nov 5, 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 28 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 9665de5 and 17dcc39.

Files selected for processing: 28

  • apps/e2e/demo-e2e-cloudflare/e2e/helpers/managed-bundles.ts
  • apps/e2e/demo-e2e-cloudflare/e2e/helpers/managed-internet.ts
  • apps/e2e/demo-e2e-cloudflare/e2e/helpers/managed-worker.ts
  • apps/e2e/demo-e2e-cloudflare/e2e/managed-edge-autoupdate.e2e.spec.ts
  • apps/e2e/demo-e2e-cloudflare/fixture-managed/worker.ts
  • apps/e2e/demo-e2e-skilled-openapi/e2e/confirm-destructive.e2e.spec.ts
  • apps/e2e/demo-e2e-skilled-openapi/e2e/helpers/skilled-client.ts
  • libs/adapters/src/skills/audit/__tests__/audit-writer.spec.ts
  • libs/adapters/src/skills/audit/audit-record.types.ts
  • libs/adapters/src/skills/audit/audit-writer.ts
  • libs/adapters/src/skills/audit/index.ts
  • libs/adapters/src/skills/index.ts
  • libs/edge/README.md
  • libs/edge/src/__tests__/managed.spec.ts
  • libs/edge/src/index.spec.ts
  • libs/edge/src/managed.ts
  • libs/skills/catalog/frontmcp-config/references/configure-elicitation.md
  • libs/skills/catalog/frontmcp-deployment/references/deploy-to-cloudflare-skills-only.md
  • libs/skills/catalog/frontmcp-extensibility/references/skill-audit-log.md
  • plugins/plugin-skilled-openapi/README.md
  • plugins/plugin-skilled-openapi/src/__tests__/audit-and-startup.spec.ts
  • plugins/plugin-skilled-openapi/src/__tests__/confirm-actions.integration.spec.ts
  • plugins/plugin-skilled-openapi/src/executor/__tests__/action-confirmation.spec.ts
  • plugins/plugin-skilled-openapi/src/executor/__tests__/execute-skill-action.spec.ts
  • plugins/plugin-skilled-openapi/src/executor/action-confirmation.ts
  • plugins/plugin-skilled-openapi/src/executor/execute-skill-action.ts
  • plugins/plugin-skilled-openapi/src/tools/load-skill.schema.ts
  • plugins/plugin-skilled-openapi/src/tools/load-skill.tool.ts
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

@github-actions

github-actions Bot commented Nov 5, 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-05T17:46:02.232Z
Commit: 76a369ee

@alexmercerpo
alexmercerpo merged commit 588289f into release/1.11.x Nov 5, 2026
46 checks passed
@alexmercerpo
alexmercerpo deleted the fix/skilled-confirm-edge-audit branch November 5, 2026 17:54
@github-actions

github-actions Bot commented Nov 5, 2026

Copy link
Copy Markdown
Contributor

Cherry-picked with its stack

This PR is part of stack #1121. Its cherry-pick to main is made as one PR for the whole stack when the stack's top PR, #1142, merges.

If #1142 does not merge, run the Cherry-pick to main workflow by hand with stack: 1121.

frontegg-david added a commit that referenced this pull request Nov 5, 2026
* Cherry-pick: fix(adapters,plugins,edge): an older saas bundle, pulled or pushed, doesn't roll the server back

Cherry-picked from #1119 (merged to release/1.11.x)
Original commit: d88f422

Co-Authored-By: frontegg-david <69419539+frontegg-david@users.noreply.github.com>

* Cherry-pick: fix(sdk,plugins): an @app's Skilled OpenAPI credential resolver reaches execute_action and this.callTool() of an operation

Cherry-picked from #1120 (merged to release/1.11.x)
Original commit: 00a3b00

Co-Authored-By: frontegg-david <69419539+frontegg-david@users.noreply.github.com>

* Cherry-pick: fix(adapters,plugins,edge): a push token replayed to another replica or Worker isolate is refused

Cherry-picked from #1122 (merged to release/1.11.x)
Original commit: 1341026

Co-Authored-By: frontegg-david <69419539+frontegg-david@users.noreply.github.com>

* Cherry-pick: fix(plugins): a skill a skills:filter hook hides can't be run by naming its actions

Cherry-picked from #1123 (merged to release/1.11.x)
Original commit: a081fbf

Co-Authored-By: frontegg-david <69419539+frontegg-david@users.noreply.github.com>

* Cherry-pick: fix(sdk): a plugin listed twice in one plugins list is refused at startup instead of keeping only its last entry

Cherry-picked from #1127 (merged to release/1.11.x)
Original commit: 3dbadd3

Co-Authored-By: frontegg-david <69419539+frontegg-david@users.noreply.github.com>

* Cherry-pick: fix(plugins,edge): a Cron run that doesn't apply its bundle fails, and /readyz says the bundle served is stale

Cherry-picked from #1128 (merged to release/1.11.x)
Original commit: 4938d21

Co-Authored-By: frontegg-david <69419539+frontegg-david@users.noreply.github.com>

* Cherry-pick: fix(sdk): connect() with an authToken connects an anonymous caller again on a server without auth

Cherry-picked from #1129 (merged to release/1.11.x)
Original commit: 3d238da

Co-Authored-By: frontegg-david <69419539+frontegg-david@users.noreply.github.com>

* Cherry-pick: fix(auth,adapters,plugins,edge): operators can tell why a pull or push token was refused, while callers still get the generic answer

Cherry-picked from #1130 (merged to release/1.11.x)
Original commit: 913f4f5

Co-Authored-By: frontegg-david <69419539+frontegg-david@users.noreply.github.com>

* Cherry-pick: fix(sdk,auth,plugins): this.authProviders works: the SDK wires the AuthProviders vault from a new authProvidersVault option

Cherry-picked from #1131 (merged to release/1.11.x)
Original commit: e14c30c

Co-Authored-By: frontegg-david <69419539+frontegg-david@users.noreply.github.com>

* Cherry-pick: fix(sdk,auth): /oauth/connect connects a verified caller who has no vault yet, and asks an anonymous one to sign in

Cherry-picked from #1132 (merged to release/1.11.x)
Original commit: f46a87a

Co-Authored-By: frontegg-david <69419539+frontegg-david@users.noreply.github.com>

* Cherry-pick: fix(sdk,plugins): the SDK skill surfaces say a plugin's skills haven't loaded yet, retryable, and carry bundleVersion

Cherry-picked from #1133 (merged to release/1.11.x)
Original commit: 0d1dc94

Co-Authored-By: frontegg-david <69419539+frontegg-david@users.noreply.github.com>

* Cherry-pick: fix(sdk,auth,cli,plugins): the credentials-required error keeps its connect link out of the message, and asks through URL elicitation where the client supports it

Cherry-picked from #1134 (merged to release/1.11.x)
Original commit: 6d8ea24

Co-Authored-By: frontegg-david <69419539+frontegg-david@users.noreply.github.com>

* Cherry-pick: fix(sdk): a health check registered after startup runs in /readyz, and registerHealthProbe returns a remover

Cherry-picked from #1135 (merged to release/1.11.x)
Original commit: 7f2f48f

Co-Authored-By: frontegg-david <69419539+frontegg-david@users.noreply.github.com>

* Cherry-pick: fix(sdk,plugins): a bundle swap sends one list_changed per list it changed, not one per entry

Cherry-picked from #1136 (merged to release/1.11.x)
Original commit: 0296291

Co-Authored-By: frontegg-david <69419539+frontegg-david@users.noreply.github.com>

* Cherry-pick: fix(adapters,plugins,edge): a restarted server serves its cached bundle in place of an older or refused pull, and doesn't write it back

Cherry-picked from #1137 (merged to release/1.11.x)
Original commit: 358d114

Co-Authored-By: frontegg-david <69419539+frontegg-david@users.noreply.github.com>

* Cherry-pick: fix(sdk,adapters,plugins,edge): a push token with a body_sha256 claim can't carry another bundle

Cherry-picked from #1138 (merged to release/1.11.x)
Original commit: b870325

Co-Authored-By: frontegg-david <69419539+frontegg-david@users.noreply.github.com>

* Cherry-pick: fix(plugins,edge): managed mode takes confirm, and the audit log says whether the user confirmed a call

Cherry-picked from #1139 (merged to release/1.11.x)
Original commit: 588289f

Co-Authored-By: frontegg-david <69419539+frontegg-david@users.noreply.github.com>

* Cherry-pick: fix(cli): the generated CLI says unknown command, prints an MCP error prefix once, and offers load_skill only the run lines it has

Cherry-picked from #1140 (merged to release/1.11.x)
Original commit: 4cfe9a2

Co-Authored-By: frontegg-david <69419539+frontegg-david@users.noreply.github.com>

* Cherry-pick: fix(plugins): load_skill's outputSchema describes a loaded skill once

Cherry-picked from #1141 (merged to release/1.11.x)
Original commit: 470010e

Co-Authored-By: frontegg-david <69419539+frontegg-david@users.noreply.github.com>

* Cherry-pick: fix(sdk,cli): skills/load's default answer sends formattedContent again, as 1.11.0 did; format: 'markdown' is the opt-in for the smaller one

Cherry-picked from #1142 (merged to release/1.11.x)
Original commit: a680fe7

Co-Authored-By: frontegg-david <69419539+frontegg-david@users.noreply.github.com>

---------

Co-authored-by: agentfront[bot] <agentfront[bot]@users.noreply.github.com>
Co-authored-by: frontegg-david <69419539+frontegg-david@users.noreply.github.com>
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