Skip to content

feat(mcp): step-up auth gate for destructive ops — Phase 1 (KLA-408)#23

Merged
jklaassenjc merged 2 commits into
mainfrom
juergen/kla-408-stepup-auth
Apr 28, 2026
Merged

feat(mcp): step-up auth gate for destructive ops — Phase 1 (KLA-408)#23
jklaassenjc merged 2 commits into
mainfrom
juergen/kla-408-stepup-auth

Conversation

@jklaassenjc
Copy link
Copy Markdown
Collaborator

@jklaassenjc jklaassenjc commented Apr 28, 2026

Summary

Phase 1 of KLA-408. Adds an opt-in chokepoint that gates every destructive MCP tool invocation (any tool argument carrying Execute: true) behind a fresh proof of operator presence. Ships the framework and a TTY-based authenticator; Touch ID and out-of-band approval are slated for follow-up slices.

Why

Today the execute: true bit on the 30+ destructive MCP tools is agent-controlled: any connected MCP client can flip it without operator awareness. The audit log records "the credential called users_delete," not "the operator at the keyboard called it." This adds a real human-in-the-loop pause point for the operators who want one.

What's wired

  • internal/mcp/stepup.gostepUpAuthenticator interface, noopStepUp (zero-cost default), ttyStepUp (mutex-serialized prompt for last-6 of API key), newStepUp factory. Reflection helpers isExecutingDestructive and destructiveTarget so a single chokepoint covers all 30+ destructive tool input types without per-handler hooks.
  • internal/mcp/tools.go — chokepoint in addTypedTool's wrappedHandler. On denial, returns an error result and records a step-up entry in the audit log.
  • internal/mcp/server.goOptions.RequireStepUp + StepUpAPIKey wire config through. Unexported Options.stepUp lets tests inject a recording fake.
  • internal/cmd/mcp.go--require-step-up flag (defaults to mcp.require_step_up_for_destructive). When set with transport=stdio, prints a startup warning that TTY prompts can't reach the operator.
  • internal/config/config.gomcp.require_step_up_for_destructive config key (default false), MCPRequireStepUp() getter, ValidConfigKeys + coerceValue handling.

Trade-offs (called out so reviewers can push back)

  • Last-6-of-API-key is a weak challenge — anyone holding the key answers it. Real value is in (a) catching agent-flipped Execute and (b) forcing in-the-loop pause on destructive calls. Stronger authenticators (Touch ID, push approval) plug into the same interface as Phase 2.
  • Stdio transport can't host a TTY prompt--require-step-up is only effective when stdin is a real terminal (e.g. operator-launched --transport http session). Documented via startup warning. For the Claude Desktop / stdio flow, an out-of-band authenticator (Slice 3) is the right answer.
  • Default is off. Backwards-compat with all existing deployments.

Behavior matrix

Tool input Execute: true? RequireStepUp? Authenticator says Result
destructive yes true allow API call fires
destructive yes true deny error result; audit-logged
destructive yes true unavailable (no TTY) error result; audit-logged
destructive yes false (skipped) API call fires
destructive no (plan) true (skipped) plan returned
non-destructive (no Execute field) n/a true (skipped) normal call

Test plan

  • go build ./... clean
  • go test ./... — full suite passing
  • 9 stepup unit tests — noopStepUp allows; factory dispatch; ttyStepUp accept / wrong-fragment-deny / explicit-no-deny / empty-key / too-short-key / target-clause-omitted-when-empty
  • 2 reflection helper testsisExecutingDestructive and destructiveTarget table-driven across multiple tool input types incl. pointers and nil
  • 4 chokepoint integration tests — destructive Execute: true triggers step-up; denial blocks the underlying API call; plan mode (Execute: false) bypasses; non-destructive tools (no Execute field) bypass
  • CLI flag-defaults test (--require-step-up defaults to false)

Follow-ups

🤖 Generated with Claude Code


Note

Medium Risk
Adds a new authorization chokepoint that can block destructive MCP tool executions based on an interactive prompt, affecting runtime behavior when enabled and introducing new failure modes (no TTY/no API key). Default is off, limiting impact to opt-in deployments.

Overview
Adds an opt-in step-up authentication gate for destructive MCP tool calls (any tool input with Execute: true), prompting the operator on a controlling TTY and failing closed when denied/unavailable.

Wires the feature end-to-end via new config mcp.require_step_up_for_destructive (default false), server options (RequireStepUp, StepUpAPIKey), and a new jc mcp serve --require-step-up flag (including startup warnings for stdio transport and fail-fast if no API key is available).

Implements the gate in addTypedTool so all destructive tools share a single chokepoint, and adds comprehensive unit/integration tests for the authenticator, reflection-based destructiveness detection, and enforcement behavior.

Reviewed by Cursor Bugbot for commit 3037565. Bugbot is set up for automated code reviews on this repo. Configure here.

Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Fix All in Cursor

Reviewed by Cursor Bugbot for commit 66d37fb. Configure here.

Comment thread internal/cmd/mcp.go Outdated
Comment thread internal/cmd/mcp.go
jklaassenjc pushed a commit that referenced this pull request Apr 28, 2026
Address two Cursor Bugbot findings on PR #23:

1. (low) StepUpAPIKey was wired unconditionally — read config.APIKey()
   only when --require-step-up is on, matching the --require-auth
   pattern in the same file.
2. (medium) Missing startup validation when --require-step-up is set
   but no credential exists. The TTY authenticator would otherwise
   reject every destructive call at runtime with "no challenge channel
   available." Now fails fast at startup with a clear message and a
   pointer to fix it.

Both fixes are one block, preceding the mcp.NewServer call. No
behavioral change when --require-step-up is off (which is the default).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
jrennichjc
jrennichjc previously approved these changes Apr 28, 2026
juergen-kc and others added 2 commits April 28, 2026 13:17
Adds an opt-in chokepoint that gates every destructive MCP tool
invocation (any tool argument carrying Execute: true) behind a fresh
proof of operator presence. Phase 1 ships the framework and a TTY-based
authenticator; Touch ID and out-of-band approval are slated for follow-
up slices.

What's wired:

- internal/mcp/stepup.go — stepUpAuthenticator interface, noopStepUp
  (zero-cost default), ttyStepUp (prompts for last-6 of API key,
  mutex-serialized so concurrent destructive calls don't interleave),
  newStepUp factory.
- internal/mcp/tools.go — single chokepoint inside addTypedTool's
  wrappedHandler. Reflection-based check for Execute: true on the
  generic In type covers all 30+ destructive tool inputs (destructive
  Input, userUpdateInput, membershipInput, commandRunInput, etc.)
  without per-handler hooks. On denial, returns an error result and
  records a step-up entry in the audit log.
- internal/mcp/server.go — Options.RequireStepUp + StepUpAPIKey wire
  config through to the authenticator. Unexported Options.stepUp lets
  tests inject a recording fake.
- internal/cmd/mcp.go — --require-step-up flag (defaults to
  mcp.require_step_up_for_destructive in config). When set with
  transport=stdio, prints a startup warning that TTY prompts can't
  reach the operator and destructive calls will fail closed.
- internal/config/config.go — mcp.require_step_up_for_destructive
  config key (default false), MCPRequireStepUp() getter, ValidConfig
  Keys + coerceValue handling.

Trade-offs:

- Last-6-of-API-key is a weak challenge — anyone holding the key
  answers it. Real value is in (a) catching agent-flipped Execute and
  (b) forcing in-the-loop pause on destructive calls. Stronger
  authenticators (Touch ID, push approval) plug into the same
  interface as Phase 2.
- Stdio transport can't host a TTY prompt; --require-step-up is only
  effective when stdin is a real terminal (e.g. operator-launched
  --transport http session). Documented via startup warning.

Tests:

- 9 stepup unit tests (noop, factory, ttyStepUp accept/deny/missing-
  key/short-key paths)
- 2 reflection helper tests (table-driven, multiple tool input types)
- 4 chokepoint integration tests (destructive triggers step-up,
  denial blocks API call, plan mode bypasses, non-destructive tools
  bypass)
- CLI flag-defaults test
- Full suite green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Address two Cursor Bugbot findings on PR #23:

1. (low) StepUpAPIKey was wired unconditionally — read config.APIKey()
   only when --require-step-up is on, matching the --require-auth
   pattern in the same file.
2. (medium) Missing startup validation when --require-step-up is set
   but no credential exists. The TTY authenticator would otherwise
   reject every destructive call at runtime with "no challenge channel
   available." Now fails fast at startup with a clear message and a
   pointer to fix it.

Both fixes are one block, preceding the mcp.NewServer call. No
behavioral change when --require-step-up is off (which is the default).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@jklaassenjc jklaassenjc merged commit f561eb6 into main Apr 28, 2026
6 of 7 checks passed
@jklaassenjc jklaassenjc deleted the juergen/kla-408-stepup-auth branch April 28, 2026 19:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

3 participants