diff --git a/ai-code-mcp-debug-tools.el b/ai-code-mcp-debug-tools.el index 271c31b..22b9a60 100644 --- a/ai-code-mcp-debug-tools.el +++ b/ai-code-mcp-debug-tools.el @@ -107,14 +107,42 @@ :optional t))) "Optional MCP eval tool specification.") +(defun ai-code-mcp-debug-tools--register-base-tools () + "Register the standard MCP debugging tools." + (dolist (tool ai-code-mcp-debug-tools--specs) + (apply #'ai-code-mcp-make-tool tool))) + +(defun ai-code-mcp-debug-tools--register-eval-tool () + "Register the optional `eval_elisp' MCP tool." + (apply #'ai-code-mcp-make-tool ai-code-mcp-debug-tools--eval-spec)) + +(defun ai-code-mcp-debug-tools--enabled-p () + "Return non-nil when debug tools are enabled globally." + ai-code-mcp-debug-tools-enabled) + +(defun ai-code-mcp-debug-tools--eval-enabled-p () + "Return non-nil when `eval_elisp' is enabled globally." + ai-code-mcp-debug-tools-enable-eval-elisp) + +(defun ai-code-mcp-debug-tools--require-enabled () + "Signal an error unless debug inspection tools are enabled." + (unless (ai-code-mcp-debug-tools--enabled-p) + (error + "Enable ai-code-mcp-debug-tools-enabled to use the global Emacs debug MCP tools"))) + +(defun ai-code-mcp-debug-tools--require-eval-enabled () + "Signal an error unless `eval_elisp' is enabled." + (unless (ai-code-mcp-debug-tools--eval-enabled-p) + (error + "Enable ai-code-mcp-debug-tools-enable-eval-elisp to use the global eval_elisp MCP tool"))) + (defun ai-code-mcp-debug-tools-setup () "Register optional MCP debugging tools when enabled." (when ai-code-mcp-debug-tools-enabled (ai-code-mcp--ensure-error-capture) - (dolist (tool ai-code-mcp-debug-tools--specs) - (apply #'ai-code-mcp-make-tool tool)) + (ai-code-mcp-debug-tools--register-base-tools) (when ai-code-mcp-debug-tools-enable-eval-elisp - (apply #'ai-code-mcp-make-tool ai-code-mcp-debug-tools--eval-spec)))) + (ai-code-mcp-debug-tools--register-eval-tool)))) (defun ai-code-mcp--documentation-summary (documentation) "Return a trimmed summary line for DOCUMENTATION." @@ -343,6 +371,7 @@ keeps the backtrace on failures." (defun ai-code-mcp-get-variable-binding-info (variable-name &optional buffer-name) "Return JSON binding details for VARIABLE-NAME in BUFFER-NAME." + (ai-code-mcp-debug-tools--require-enabled) (let ((symbol (ai-code-mcp--find-existing-variable-symbol variable-name))) (if (not symbol) (json-encode @@ -380,6 +409,7 @@ keeps the backtrace on failures." "Return the printed representation of VARIABLE-NAME. Return a friendly error string when VARIABLE-NAME does not name an existing bound variable." + (ai-code-mcp-debug-tools--require-enabled) (let ((symbol (ai-code-mcp--find-existing-variable-symbol variable-name))) (cond ((not symbol) @@ -429,6 +459,7 @@ existing bound variable." (defun ai-code-mcp-get-function-info (function-name) "Return JSON metadata describing FUNCTION-NAME." + (ai-code-mcp-debug-tools--require-enabled) (let ((symbol (ai-code-mcp--find-existing-function-symbol function-name))) (if (not (and symbol (fboundp symbol))) (json-encode @@ -480,6 +511,7 @@ existing bound variable." (defun ai-code-mcp-get-last-error-backtrace () "Return a JSON snapshot of the most recently recorded Emacs error." + (ai-code-mcp-debug-tools--require-enabled) (json-encode (if ai-code-mcp--last-error-record (ai-code-mcp--last-error-json-payload ai-code-mcp--last-error-record) @@ -520,6 +552,7 @@ existing bound variable." (defun ai-code-mcp-get-feature-load-state (feature-name) "Return JSON load-state details for FEATURE-NAME." + (ai-code-mcp-debug-tools--require-enabled) (if (not (ai-code-mcp--valid-feature-name-p feature-name)) (json-encode (ai-code-mcp--invalid-feature-load-state-payload feature-name)) @@ -539,6 +572,7 @@ existing bound variable." (defun ai-code-mcp-get-recent-messages (&optional limit) "Return a JSON payload for recent messages using LIMIT." + (ai-code-mcp-debug-tools--require-enabled) (let* ((limit (or limit 50)) (messages (ai-code-mcp--message-lines))) (unless (and (integerp limit) (> limit 0)) @@ -556,6 +590,8 @@ existing bound variable." Return a JSON payload. BUFFER-NAME or FILE-PATH select the evaluation context. CAPTURE-MESSAGES, INCLUDE-BACKTRACE, and TIMEOUT-MS control diagnostics." + (ai-code-mcp-debug-tools--require-enabled) + (ai-code-mcp-debug-tools--require-eval-enabled) (let* ((capture-messages (ai-code-mcp-debug-tools--bool-arg capture-messages t)) diff --git a/ai-code.el b/ai-code.el index 669487b..3c02a9d 100644 --- a/ai-code.el +++ b/ai-code.el @@ -480,6 +480,59 @@ ARG is the prefix argument." clipboard-context))))) (ai-code--insert-prompt final-prompt))))) +(defun ai-code--emacs-runtime-debug-prompt (description eval-available-p + request-eval-elisp) + "Return an Emacs runtime debugging prompt from DESCRIPTION. +EVAL-AVAILABLE-P reports whether `eval_elisp' is globally enabled. +REQUEST-EVAL-ELISP reports whether this debug run may use it." + (format + (concat + "Use the Emacs MCP tools available in this session to debug my Emacs runtime.\n" + "The issue may involve an interactive function or a key binding.\n" + "%s\n\n" + "Inspect the relevant runtime state first: keymaps, command metadata,\n" + "variables, recent messages, load state, and the last backtrace when useful.\n" + "Explain what you find, then recommend the smallest fix or next step.\n\n" + "Runtime issue description:\n" + "%s") + (cond + ((and eval-available-p request-eval-elisp) + "eval_elisp is enabled in your Emacs MCP config and is allowed for this debugging run.") + (eval-available-p + "eval_elisp is enabled in your Emacs MCP config, but it was not requested for this debugging run.") + (t + "eval_elisp is disabled in your Emacs MCP config, so rely on non-eval inspection tools unless you first enable ai-code-mcp-debug-tools-enable-eval-elisp.")) + description)) + +;;;###autoload +(defun ai-code-debug-emacs-runtime () + "Assemble and send an Emacs runtime debugging prompt for the current AI session." + (interactive) + (unless (bound-and-true-p ai-code-mcp-debug-tools-enabled) + (user-error + "Enable ai-code-mcp-debug-tools-enabled before using Emacs runtime debugging")) + (let* ((description + (ai-code-read-string + "Describe the Emacs runtime issue (it can be an interactive function or a key binding): ")) + (eval-available-p + (bound-and-true-p ai-code-mcp-debug-tools-enable-eval-elisp)) + (request-eval-elisp + (y-or-n-p + "Allow AI to eval Emacs Lisp while debugging this Emacs runtime issue? "))) + (when description + (when (and request-eval-elisp + (not eval-available-p)) + (user-error + "Enable ai-code-mcp-debug-tools-enable-eval-elisp before requesting eval_elisp debugging")) + (when-let* ((prompt + (ai-code-read-string + "Confirm and edit Emacs runtime debug prompt: " + (ai-code--emacs-runtime-debug-prompt + description + eval-available-p + request-eval-elisp)))) + (ai-code--insert-prompt prompt))))) + ;;;###autoload (defun ai-code-cli-switch-to-buffer-or-hide () "Hide the current buffer when its name both begins and ends with '*'. @@ -639,6 +692,7 @@ Shows the current backend label to the right." ("p" "Open prompt history file" ai-code-open-prompt-file) ("m" "Debug python MCP server" ai-code-debug-mcp) ("N" "Toggle notifications" ai-code-notifications-toggle) + ("d" "Debug Emacs runtime" ai-code-debug-emacs-runtime) ("h" "Help / Quick Start" ai-code-onboarding-open-quickstart)) (transient-define-prefix ai-code-menu-default () diff --git a/test/test_ai-code-mcp-debug-tools.el b/test/test_ai-code-mcp-debug-tools.el index c6617af..a2aeab6 100644 --- a/test/test_ai-code-mcp-debug-tools.el +++ b/test/test_ai-code-mcp-debug-tools.el @@ -121,6 +121,30 @@ (alist-get 'tools tools-result)))) (should (member "eval_elisp" tool-names))))) +(ert-deftest ai-code-test-mcp-debug-tools-disabled-globally-blocks-access () + "Global disable should block direct use of the optional debug tools." + (let ((ai-code-mcp-server-tools nil) + (ai-code-mcp-debug-tools-enabled nil) + (ai-code-mcp-debug-tools-enable-eval-elisp nil)) + (should-error + (ai-code-mcp-get-recent-messages)) + (should-error + (ai-code-mcp-eval-elisp "(+ 1 2)")))) + +(ert-deftest ai-code-test-mcp-debug-tools-errors-name-global-flags () + "Global gating errors should point to the relevant defcustom names." + (let ((ai-code-mcp-debug-tools-enabled nil) + (ai-code-mcp-debug-tools-enable-eval-elisp nil)) + (should (string-match-p + "ai-code-mcp-debug-tools-enabled" + (error-message-string + (should-error (ai-code-mcp-get-recent-messages))))) + (let ((ai-code-mcp-debug-tools-enabled t)) + (should (string-match-p + "ai-code-mcp-debug-tools-enable-eval-elisp" + (error-message-string + (should-error (ai-code-mcp-eval-elisp "(+ 1 2)")))))))) + (ert-deftest ai-code-test-mcp-tools-list-warns-eval-elisp-is-unrestricted () "Eval tool metadata should warn about unrestricted side effects." (let ((ai-code-mcp-server-tools nil) diff --git a/test/test_ai-code.el b/test/test_ai-code.el index 99c7e8a..41dca8c 100644 --- a/test/test_ai-code.el +++ b/test/test_ai-code.el @@ -565,6 +565,94 @@ (should (eq ai-code-backends-infra-terminal-backend 'ghostel)) (should sync-called)))) +(ert-deftest ai-code-test-debug-emacs-runtime-uses-global-eval-flag-in-prompt () + "Debug Emacs runtime should describe the global eval flag state." + (let (description-prompt + confirm-read-args + sent-prompt) + (let ((ai-code-mcp-debug-tools-enabled t) + (ai-code-mcp-debug-tools-enable-eval-elisp t)) + (cl-letf (((symbol-function 'y-or-n-p) + (lambda (prompt) + (should (string-match-p "eval Emacs Lisp" prompt)) + t)) + ((symbol-function 'ai-code-read-string) + (lambda (prompt &optional initial-input _candidate-list) + (cond + ((string-match-p "Describe the Emacs runtime issue" prompt) + (setq description-prompt prompt) + "C-c x runs the wrong interactive command") + ((string-match-p "Confirm and edit Emacs runtime debug prompt" prompt) + (setq confirm-read-args (list prompt initial-input)) + initial-input) + (t + (ert-fail (format "Unexpected prompt: %s" prompt)))))) + ((symbol-function 'ai-code--insert-prompt) + (lambda (prompt) + (setq sent-prompt prompt)))) + (ai-code-debug-emacs-runtime))) + (should (string-match-p "interactive function or a key binding" + description-prompt)) + (should (equal (car confirm-read-args) + "Confirm and edit Emacs runtime debug prompt: ")) + (should (string-match-p "Use the Emacs MCP tools available in this session" + (cadr confirm-read-args))) + (should (string-match-p "eval_elisp is enabled in your Emacs MCP config" + (cadr confirm-read-args))) + (should (string-match-p "C-c x runs the wrong interactive command" + sent-prompt)))) + +(ert-deftest ai-code-test-debug-emacs-runtime-errors-when-global-eval-flag-is-off () + "Debug Emacs runtime should tell the user to enable the global eval flag." + (let ((ai-code-mcp-debug-tools-enabled t) + (ai-code-mcp-debug-tools-enable-eval-elisp nil) + description-prompt) + (cl-letf (((symbol-function 'y-or-n-p) + (lambda (_prompt) t)) + ((symbol-function 'ai-code-read-string) + (lambda (prompt &optional _initial-input _candidate-list) + (setq description-prompt prompt) + "M-x foo fails")) + ((symbol-function 'ai-code--insert-prompt) + (lambda (&rest _args) + (ert-fail "Should not send a prompt when eval_elisp is disabled globally.")))) + (should-error + (ai-code-debug-emacs-runtime) + :type 'user-error)) + (should (string-match-p "Describe the Emacs runtime issue" + description-prompt)))) + +(ert-deftest ai-code-test-debug-emacs-runtime-distinguishes-config-from-run-consent () + "Debug Emacs runtime should separate global eval availability from per-run consent." + (let (confirm-read-args) + (let ((ai-code-mcp-debug-tools-enabled t) + (ai-code-mcp-debug-tools-enable-eval-elisp t)) + (cl-letf (((symbol-function 'y-or-n-p) + (lambda (_prompt) nil)) + ((symbol-function 'ai-code-read-string) + (lambda (prompt &optional initial-input _candidate-list) + (if (string-match-p "Confirm and edit Emacs runtime debug prompt" prompt) + (progn + (setq confirm-read-args (list prompt initial-input)) + initial-input) + "C-c x runs the wrong interactive command"))) + ((symbol-function 'ai-code--insert-prompt) + (lambda (&rest _args) nil))) + (ai-code-debug-emacs-runtime))) + (should (string-match-p + "eval_elisp is enabled in your Emacs MCP config" + (cadr confirm-read-args))) + (should (string-match-p + "not requested for this debugging run" + (cadr confirm-read-args))))) + +(ert-deftest ai-code-test-debug-emacs-runtime-removes-stale-done-comment () + "The source should not keep the stale DONE note for the runtime debug menu item." + (with-temp-buffer + (insert-file-contents (expand-file-name "ai-code.el" default-directory)) + (should-not + (search-forward ";; DONE: add a menu item: Debug your emacs runtime." nil t)))) + (ert-deftest ai-code-test-menu-ai-cli-session-includes-select-terminal-entry () "Test that the AI CLI session menu exposes terminal backend selection." (let ((suffix (transient-get-suffix 'ai-code--menu-ai-cli-session "l"))) @@ -572,6 +660,15 @@ (should (eq (plist-get (cdr suffix) :command) 'ai-code-select-terminal)))) +(ert-deftest ai-code-test-menu-other-tools-includes-debug-emacs-runtime-entry () + "Test that the Other Tools menu exposes Emacs runtime debugging." + (let ((suffix (transient-get-suffix 'ai-code--menu-other-tools "d"))) + (should suffix) + (should (eq (plist-get (cdr suffix) :command) + 'ai-code-debug-emacs-runtime)) + (should (equal (plist-get (cdr suffix) :description) + "Debug Emacs runtime")))) + (ert-deftest ai-code-test-menu-prefix-command-default-layout () "Test that the default menu layout uses the original transient." (let ((ai-code-menu-layout 'default))