Skip to content

MCP run_action never enforces ai.requiresConfirmation — it is surfaced in list_actions only, while @objectstack/spec guidance tells authors it stops execution #15942

Description

@os-steve

Summary

action.ai.requiresConfirmation is a safety-shaped flag. @objectstack/spec's own authoring guidance tells authors it gates execution — that it "forces a human-in-the-loop gate on the AI call", and, in the ADR-0049 ledger entry for the retired tool.requiresConfirmation, that action.ai.requiresConfirmation "is the flag the HITL approval queue actually reads … and the only path that actually stops execution".

Measured against the shipped @objectstack/runtime 17.3.0, no such queue exists. The flag is read in exactly one place, to fill in one field of the MCP list_actions summary. run_action never consults it. An author who follows the spec's prescription gets a JSON field, not a gate.

Measured

@objectstack/runtime 17.3.0, dist/index.jsrequiresConfirmation occurs at exactly two sites in the entire bundle:

// site 1 — the read
function actionLooksDestructive(_deps, action) {
  if (action?.ai?.requiresConfirmation !== void 0) return Boolean(action.ai.requiresConfirmation);
  return Boolean(action?.mode === "delete" || action?.variant === "danger");
}

// site 2 — the only consumer, projecting into the MCP summary
function summarizeAction(deps, action, obj, objectName) {
  return {
    name: action.name,
    // …
    requiresConfirmation: actionLooksDestructive(deps, action),
  };
}

summarizeAction has exactly one caller — the listActions loop. The runAction sibling on the same object goes to invokeBusinessAction, whose pre-dispatch checks are, in order: AI exposure, action permission, disabled-activation refusal, and param enforcement. There is no confirmation check, and nothing awaits an approval. For type: 'flow' it proceeds straight to dispatchFlowAction.

Corroborating greps on the same bundle:

  • approvalQueue — 0 occurrences
  • destructiveHint — 0 occurrences
  • HITL — 1 occurrence, in a comment describing the flag as a hint: /** True when an action is destructive by author signal/heuristic (HITL hint). */

That comment is the accurate description. The published spec guidance is not.

Why this is worth a card rather than a docs tweak

This is the exact failure class ADR-0049 was written for, and the project has already paid for it once: tool.requiresConfirmation was retired in the 16.x line precisely because "a SAFETY flag that is merely accepted is false compliance — authors set it on destructive tools believing the call would pause, and nothing ever did" (#3715). The retirement's own prescription points authors at action.ai.requiresConfirmation as the real gate.

If the runtime does not stop execution on that key either, then the replacement carries the same defect as the key it replaced, and the ledger entry directing authors to it is actively misleading. The near-miss is worse here than for an ordinary dead property: the author reads a spec sentence promising a pause, sets the flag, sees it accepted, sees it echoed truthfully in list_actions, and ships believing a human is in the loop.

Repro

  1. Author an action with ai: { exposed: true, requiresConfirmation: true } on a flow-typed target.
  2. list_actions over MCP → the entry correctly reports "requiresConfirmation": true.
  3. run_action the same action with an API key → the flow dispatches immediately. Nothing pauses, nothing is queued, no approval is requested, and the response carries the flow result.

What this card is NOT claiming

The decision this needs

Which of the three is intended, since the surfaces currently disagree:

  1. Enforce itrun_action refuses an unconfirmed destructive call, with a resume/approve path. This is what the spec text currently promises.
  2. Rescope the docs — keep surfacing-only, and rewrite the spec guidance and the ADR-0049 ledger entry so neither promises a pause. The runtime's own "HITL hint" comment is the honest wording.
  3. Something between — e.g. an opt-in server-side enforcement mode, so trusted agent principals can be held while interactive ones are not.

Filed from downstream while fixing hotcrm#1578, where convert_lead had to declare this key so list_actions would stop contradicting the action's own description. That fix is correct and lands regardless; this card is the platform half, filed at destination rather than worked around downstream. Reporting the honest scope of the flag is why the fix's comment says "surfaced, not enforced" — if this card resolves as option 1, that comment gets simpler.

Measured on @objectstack/runtime 17.3.0 and @objectstack/spec 17.3.0 as installed from the registry.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions