From ad23fea03f03640bc751bfb4748250648821a382 Mon Sep 17 00:00:00 2001 From: Clay Sheaff <10719781+csheaff@users.noreply.github.com> Date: Sat, 17 Jan 2026 09:43:13 -0800 Subject: [PATCH 1/7] Add TRAMP support for remote agent execution - Add agent-shell--tramp-command-runner to run agents on remote hosts via SSH - Add agent-shell--resolve-tramp-path to convert paths between TRAMP and remote-local format - Add agent-shell-enable-tramp-support and agent-shell-disable-tramp-support commands - Fix temporary-file-directory returning remote path when default-directory is TRAMP path (icon caching bug) Closes #122 --- agent-shell.el | 73 +++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 72 insertions(+), 1 deletion(-) diff --git a/agent-shell.el b/agent-shell.el index 20d0cd0..8920530 100644 --- a/agent-shell.el +++ b/agent-shell.el @@ -1175,6 +1175,72 @@ If the buffer's file has changed, prompt the user to reload it." "Resolve PATH using `agent-shell-path-resolver-function'." (funcall (or agent-shell-path-resolver-function #'identity) path)) +;;; TRAMP Support (experimental) + +(declare-function tramp-tramp-file-p "tramp") +(declare-function tramp-dissect-file-name "tramp") +(declare-function tramp-file-name-method "tramp") +(declare-function tramp-file-name-user "tramp") +(declare-function tramp-file-name-host "tramp") +(declare-function tramp-file-name-port "tramp") +(declare-function tramp-file-name-localname "tramp") +(declare-function tramp-make-tramp-file-name "tramp") + +(defun agent-shell--tramp-command-runner (buffer) + "Return command prefix for running commands on TRAMP remote host. +BUFFER is the agent-shell buffer. +Returns nil for non-TRAMP buffers, allowing local execution." + (require 'tramp) + (with-current-buffer buffer + (let ((cwd (agent-shell-cwd))) + (when (tramp-tramp-file-p cwd) + (let* ((vec (tramp-dissect-file-name cwd)) + (method (tramp-file-name-method vec)) + (user (tramp-file-name-user vec)) + (host (tramp-file-name-host vec)) + (port (tramp-file-name-port vec))) + (unless (member method '("ssh" "scp" nil)) + (error "TRAMP method '%s' not supported; only SSH is supported" method)) + (append + (list "ssh") + (when port (list "-p" port)) + (list (if user (format "%s@%s" user host) host)) + (list "--"))))))) + +(defun agent-shell--resolve-tramp-path (path) + "Resolve PATH between TRAMP format and remote-local format. + +For example: +- /ssh:host:/project/README.md => /project/README.md +- /project/README.md => /ssh:host:/project/README.md" + (require 'tramp) + (let* ((cwd (agent-shell-cwd)) + (tramp-vec (and (tramp-tramp-file-p cwd) + (tramp-dissect-file-name cwd)))) + (cond + ;; Path is already a TRAMP path - strip the prefix for the agent + ((tramp-tramp-file-p path) + (tramp-file-name-localname (tramp-dissect-file-name path))) + ;; Path is a remote-local path - add TRAMP prefix for Emacs + (tramp-vec + (tramp-make-tramp-file-name tramp-vec path)) + ;; Not in a TRAMP context + (t path)))) + +(defun agent-shell-enable-tramp-support () + "Enable TRAMP support for agent-shell (experimental)." + (interactive) + (setq agent-shell-container-command-runner #'agent-shell--tramp-command-runner) + (setq agent-shell-path-resolver-function #'agent-shell--resolve-tramp-path) + (message "TRAMP support enabled for agent-shell")) + +(defun agent-shell-disable-tramp-support () + "Disable TRAMP support for agent-shell." + (interactive) + (setq agent-shell-container-command-runner nil) + (setq agent-shell-path-resolver-function nil) + (message "TRAMP support disabled for agent-shell")) + (defun agent-shell--get-devcontainer-workspace-path (cwd) "Return devcontainer workspaceFolder for CWD, or default value if none found. @@ -2190,7 +2256,12 @@ Icon names starting with https:// are downloaded directly from that location." url)) ;; For lobe-icons names, use the original filename (file-name-nondirectory url))) - (cache-dir (file-name-concat (temporary-file-directory) "agent-shell" mode)) + ;; Always use local temp directory, even when default-directory is remote + (local-temp-dir (if (and (fboundp 'tramp-tramp-file-p) + (tramp-tramp-file-p default-directory)) + (or (getenv "TMPDIR") "/tmp") + (temporary-file-directory))) + (cache-dir (file-name-concat local-temp-dir "agent-shell" mode)) (cache-path (expand-file-name filename cache-dir))) (unless (file-exists-p cache-path) (make-directory cache-dir t) From 377f2e3f32647965243aed9a8c1032d89c3f7225 Mon Sep 17 00:00:00 2001 From: Clay Sheaff <10719781+csheaff@users.noreply.github.com> Date: Sat, 17 Jan 2026 10:28:10 -0800 Subject: [PATCH 2/7] Save transcripts locally for TRAMP sessions For remote TRAMP sessions, save transcripts to ~/.agent-shell/transcripts/// instead of the remote filesystem. This avoids slow TRAMP file operations on every append. --- agent-shell.el | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/agent-shell.el b/agent-shell.el index 8920530..a0e2284 100644 --- a/agent-shell.el +++ b/agent-shell.el @@ -4229,8 +4229,22 @@ When nil, transcript saving is disabled." For example: - project/.agent-shell/transcripts/." - (let* ((dir (expand-file-name ".agent-shell/transcripts" (agent-shell-cwd))) + project/.agent-shell/transcripts/. + +For TRAMP paths, saves locally in ~/.agent-shell/transcripts///." + (let* ((cwd (agent-shell-cwd)) + (dir (if (and (fboundp 'tramp-tramp-file-p) + (tramp-tramp-file-p cwd)) + ;; For TRAMP paths, save transcripts locally + (let* ((vec (tramp-dissect-file-name cwd)) + (host (tramp-file-name-host vec)) + (localname (tramp-file-name-localname vec)) + ;; Sanitize path for use as directory name + (safe-path (replace-regexp-in-string "/" "_" (string-trim localname "/")))) + (expand-file-name (format ".agent-shell/transcripts/%s/%s" host safe-path) + (expand-file-name "~"))) + ;; Local paths use project root as before + (expand-file-name ".agent-shell/transcripts" cwd))) (filename (format-time-string "%F-%H-%M-%S.md")) (filepath (expand-file-name filename dir))) filepath)) From 7c4a4cbadc4baaba1aa7ebbf271c700c51a9117f Mon Sep 17 00:00:00 2001 From: Clay Sheaff <10719781+csheaff@users.noreply.github.com> Date: Sat, 17 Jan 2026 13:16:35 -0800 Subject: [PATCH 3/7] Polish TRAMP support for PR readiness - Add agent-shell--local-temp-directory helper for cross-platform temp paths - Fix fallback icon caching to use local temp directory - Use ~/tmp instead of /tmp for Windows compatibility - Add ERT tests for TRAMP functions - Add README.org documentation for TRAMP support --- README.org | 53 +++++++++++++++++++++++++++++++++++ agent-shell.el | 19 +++++++++---- tests/agent-shell-tests.el | 57 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 124 insertions(+), 5 deletions(-) diff --git a/README.org b/README.org index 525f391..b2391a7 100644 --- a/README.org +++ b/README.org @@ -450,6 +450,59 @@ Optional: to prevent the agent running inside the container to access your local All of the above settings can be applied on a per-project basis using [[https://www.gnu.org/software/emacs/manual/html_node/emacs/Directory-Variables.html][directory-local variables]]. +** Running agents on remote hosts via TRAMP (Experimental) + +=agent-shell= supports running agents on remote hosts accessed via TRAMP. When you open a project on a remote machine (e.g., =/ssh:user@host:/path/to/project=), the agent can run on that remote machine. + +*** Enabling TRAMP support + +#+begin_src emacs-lisp +;; Add to your init.el, or call interactively with M-x +(agent-shell-enable-tramp-support) +#+end_src + +Or add to your =use-package= configuration: + +#+begin_src emacs-lisp +(use-package agent-shell + :config + (agent-shell-enable-tramp-support)) +#+end_src + +*** How it works + +When TRAMP support is enabled: +- The agent runs on the remote host via SSH +- File paths are automatically converted between TRAMP format and remote-local format +- Transcripts are saved locally in =~/.agent-shell/transcripts///= + +*** Requirements + +1. *Agent must be installed on the remote host* - e.g., =copilot=, =claude-code-acp= +2. *SSH key-based authentication recommended* - interactive password prompts may not work +3. *Remote shell must have correct PATH* - ensure your remote =.bashrc= sets up PATH for non-interactive shells + +*** Disabling TRAMP support + +#+begin_src emacs-lisp +(agent-shell-disable-tramp-support) +#+end_src + +*** Known limitations + +- *SSH-only*: Only =ssh= and =scp= TRAMP methods are supported. Docker, sudo, and other methods will error. +- *Multi-hop not supported*: Paths like =/ssh:gateway|ssh:target:/path= are not currently supported. +- *Environment variables*: Must be configured on the remote machine, not passed from Emacs. + +*** Per-project configuration + +You can enable TRAMP support on a per-project basis using [[https://www.gnu.org/software/emacs/manual/html_node/emacs/Directory-Variables.html][directory-local variables]]: + +#+begin_src emacs-lisp +;; .dir-locals.el in your remote project +((nil . ((eval . (agent-shell-enable-tramp-support))))) +#+end_src + ** Keybindings - =C-c C-c= - Interrupt current agent operation diff --git a/agent-shell.el b/agent-shell.el index a0e2284..37fdbd4 100644 --- a/agent-shell.el +++ b/agent-shell.el @@ -1241,6 +1241,16 @@ For example: (setq agent-shell-path-resolver-function nil) (message "TRAMP support disabled for agent-shell")) +(defun agent-shell--local-temp-directory () + "Return a local temporary directory, even when `default-directory' is remote. +This ensures temp files (like cached icons) are always stored locally." + (if (and (fboundp 'tramp-tramp-file-p) + (tramp-tramp-file-p default-directory)) + ;; When in a TRAMP buffer, use local temp dir + ;; Prefer user's home for cross-platform compatibility (Windows has no /tmp) + (expand-file-name "tmp" (expand-file-name "~")) + (temporary-file-directory))) + (defun agent-shell--get-devcontainer-workspace-path (cwd) "Return devcontainer workspaceFolder for CWD, or default value if none found. @@ -2257,10 +2267,7 @@ Icon names starting with https:// are downloaded directly from that location." ;; For lobe-icons names, use the original filename (file-name-nondirectory url))) ;; Always use local temp directory, even when default-directory is remote - (local-temp-dir (if (and (fboundp 'tramp-tramp-file-p) - (tramp-tramp-file-p default-directory)) - (or (getenv "TMPDIR") "/tmp") - (temporary-file-directory))) + (local-temp-dir (agent-shell--local-temp-directory)) (cache-dir (file-name-concat local-temp-dir "agent-shell" mode)) (cache-path (expand-file-name filename cache-dir))) (unless (file-exists-p cache-path) @@ -2286,7 +2293,9 @@ Return file path of the generated SVG." (let* ((icon-text (char-to-string (string-to-char icon-name))) (mode (if (eq (frame-parameter nil 'background-mode) 'dark) "dark" "light")) (filename (format "%s-%s.svg" icon-name width)) - (cache-dir (file-name-concat (temporary-file-directory) "agent-shell" mode)) + ;; Always use local temp directory, even when default-directory is remote + (local-temp-dir (agent-shell--local-temp-directory)) + (cache-dir (file-name-concat local-temp-dir "agent-shell" mode)) (cache-path (expand-file-name filename cache-dir)) (font-size (* 0.7 width)) (x (/ width 2)) diff --git a/tests/agent-shell-tests.el b/tests/agent-shell-tests.el index 5516ac3..7694929 100644 --- a/tests/agent-shell-tests.el +++ b/tests/agent-shell-tests.el @@ -737,5 +737,62 @@ code block content with spaces [((name . "simple") (command . "simple-server"))])))) +;;; TRAMP Support Tests + +(ert-deftest agent-shell--tramp-command-runner-local-path-test () + "Test that command runner returns nil for local paths." + (with-temp-buffer + (setq default-directory "/tmp/local-project/") + (should (null (agent-shell--tramp-command-runner (current-buffer)))))) + +(ert-deftest agent-shell--tramp-command-runner-ssh-path-test () + "Test that command runner returns SSH command for TRAMP paths." + (require 'tramp) + (with-temp-buffer + (setq default-directory "/ssh:user@host:/project/") + (let ((result (agent-shell--tramp-command-runner (current-buffer)))) + (should result) + (should (equal (car result) "ssh")) + (should (member "user@host" result)) + (should (member "--" result))))) + +(ert-deftest agent-shell--tramp-command-runner-ssh-with-port-test () + "Test that command runner includes -p flag for non-standard ports." + (require 'tramp) + (with-temp-buffer + (setq default-directory "/ssh:user@host#2222:/project/") + (let ((result (agent-shell--tramp-command-runner (current-buffer)))) + (should result) + (should (member "-p" result)) + (should (member "2222" result))))) + +(ert-deftest agent-shell--resolve-tramp-path-strip-prefix-test () + "Test that resolver strips TRAMP prefix from paths." + (require 'tramp) + (should (equal (agent-shell--resolve-tramp-path "/ssh:host:/project/file.el") + "/project/file.el"))) + +(ert-deftest agent-shell--resolve-tramp-path-identity-test () + "Test that resolver returns local paths unchanged when not in TRAMP context." + (let ((default-directory "/tmp/local/")) + (should (equal (agent-shell--resolve-tramp-path "/tmp/local/file.el") + "/tmp/local/file.el")))) + +(ert-deftest agent-shell--local-temp-directory-local-test () + "Test that local-temp-directory returns normal temp dir for local paths." + (let ((default-directory "/tmp/local/")) + (should (stringp (agent-shell--local-temp-directory))) + (should-not (string-prefix-p "/ssh:" (agent-shell--local-temp-directory))))) + +(ert-deftest agent-shell--local-temp-directory-tramp-test () + "Test that local-temp-directory returns local path for TRAMP paths." + (require 'tramp) + (let ((default-directory "/ssh:host:/remote/")) + (let ((temp-dir (agent-shell--local-temp-directory))) + (should (stringp temp-dir)) + (should-not (string-prefix-p "/ssh:" temp-dir)) + ;; Should be under home directory + (should (string-prefix-p (expand-file-name "~") temp-dir))))) + (provide 'agent-shell-tests) ;;; agent-shell-tests.el ends here From 641c1dddc98dcd5644bb89c447f302f9ee761ab4 Mon Sep 17 00:00:00 2001 From: Clay Sheaff <10719781+csheaff@users.noreply.github.com> Date: Sat, 17 Jan 2026 13:41:51 -0800 Subject: [PATCH 4/7] Add multi-hop detection and use Emacs-standard cache dir - Error explicitly on multi-hop TRAMP paths (prevents silent wrong-host execution) - Use locate-user-emacs-file for cache instead of ~/tmp - Add declare-function for tramp-file-name-hop - Add negative ERT tests for multi-hop and unsupported methods - All 23 tests pass --- agent-shell.el | 8 +++++--- tests/agent-shell-tests.el | 23 ++++++++++++++++++++--- 2 files changed, 25 insertions(+), 6 deletions(-) diff --git a/agent-shell.el b/agent-shell.el index 37fdbd4..d60e18c 100644 --- a/agent-shell.el +++ b/agent-shell.el @@ -1183,6 +1183,7 @@ If the buffer's file has changed, prompt the user to reload it." (declare-function tramp-file-name-user "tramp") (declare-function tramp-file-name-host "tramp") (declare-function tramp-file-name-port "tramp") +(declare-function tramp-file-name-hop "tramp") (declare-function tramp-file-name-localname "tramp") (declare-function tramp-make-tramp-file-name "tramp") @@ -1201,6 +1202,8 @@ Returns nil for non-TRAMP buffers, allowing local execution." (port (tramp-file-name-port vec))) (unless (member method '("ssh" "scp" nil)) (error "TRAMP method '%s' not supported; only SSH is supported" method)) + (when (tramp-file-name-hop vec) + (error "Multi-hop TRAMP paths not supported")) (append (list "ssh") (when port (list "-p" port)) @@ -1246,9 +1249,8 @@ For example: This ensures temp files (like cached icons) are always stored locally." (if (and (fboundp 'tramp-tramp-file-p) (tramp-tramp-file-p default-directory)) - ;; When in a TRAMP buffer, use local temp dir - ;; Prefer user's home for cross-platform compatibility (Windows has no /tmp) - (expand-file-name "tmp" (expand-file-name "~")) + ;; When in a TRAMP buffer, use Emacs-standard cache directory + (locate-user-emacs-file "agent-shell/cache/") (temporary-file-directory))) (defun agent-shell--get-devcontainer-workspace-path (cwd) diff --git a/tests/agent-shell-tests.el b/tests/agent-shell-tests.el index 7694929..23f613f 100644 --- a/tests/agent-shell-tests.el +++ b/tests/agent-shell-tests.el @@ -788,11 +788,28 @@ code block content with spaces "Test that local-temp-directory returns local path for TRAMP paths." (require 'tramp) (let ((default-directory "/ssh:host:/remote/")) - (let ((temp-dir (agent-shell--local-temp-directory))) + (let ((temp-dir (expand-file-name (agent-shell--local-temp-directory)))) (should (stringp temp-dir)) (should-not (string-prefix-p "/ssh:" temp-dir)) - ;; Should be under home directory - (should (string-prefix-p (expand-file-name "~") temp-dir))))) + ;; Should be under Emacs user directory + (should (string-prefix-p (expand-file-name user-emacs-directory) temp-dir))))) + +(ert-deftest agent-shell--tramp-command-runner-multihop-error-test () + "Test that multi-hop TRAMP paths produce an error." + (require 'tramp) + (cl-letf (((symbol-function 'agent-shell-cwd) + (lambda () "/ssh:jump|ssh:target:/remote/path/"))) + (with-temp-buffer + (should-error (agent-shell--tramp-command-runner (current-buffer)) + :type 'error)))) + +(ert-deftest agent-shell--tramp-command-runner-unsupported-method-error-test () + "Test that unsupported TRAMP methods produce an error." + (require 'tramp) + (with-temp-buffer + (setq-local default-directory "/sudo:root@localhost:/root/") + (should-error (agent-shell--tramp-command-runner (current-buffer)) + :type 'error))) (provide 'agent-shell-tests) ;;; agent-shell-tests.el ends here From fca216ea0d39e55a369dc518b6f373e3424affca Mon Sep 17 00:00:00 2001 From: Clay Sheaff <10719781+csheaff@users.noreply.github.com> Date: Sun, 18 Jan 2026 08:44:04 -0800 Subject: [PATCH 5/7] Document that TRAMP session data is saved locally --- README.org | 1 + 1 file changed, 1 insertion(+) diff --git a/README.org b/README.org index b2391a7..2d03451 100644 --- a/README.org +++ b/README.org @@ -493,6 +493,7 @@ When TRAMP support is enabled: - *SSH-only*: Only =ssh= and =scp= TRAMP methods are supported. Docker, sudo, and other methods will error. - *Multi-hop not supported*: Paths like =/ssh:gateway|ssh:target:/path= are not currently supported. - *Environment variables*: Must be configured on the remote machine, not passed from Emacs. +- *Session data is local*: To avoid TRAMP write latency. *** Per-project configuration From 6a8ec83c8186d2752b965b4df01e9350f05090d7 Mon Sep 17 00:00:00 2001 From: Clay Sheaff Date: Mon, 19 Jan 2026 16:22:29 -0800 Subject: [PATCH 6/7] Refactor TRAMP support into agent-shell-tramp.el Move TRAMP-specific functions to a separate file per maintainer request: - agent-shell--tramp-command-runner - agent-shell--resolve-tramp-path - agent-shell-enable-tramp-support - agent-shell-disable-tramp-support - agent-shell--tramp-transcript-dir Keep agent-shell--local-temp-directory in main file as it's needed for icon caching even without TRAMP support. Add autoloads for enable/disable commands. --- agent-shell-tramp.el | 123 +++++++++++++++++++++++++++++++++++++++++++ agent-shell.el | 89 ++++--------------------------- 2 files changed, 132 insertions(+), 80 deletions(-) create mode 100644 agent-shell-tramp.el diff --git a/agent-shell-tramp.el b/agent-shell-tramp.el new file mode 100644 index 0000000..4d9b842 --- /dev/null +++ b/agent-shell-tramp.el @@ -0,0 +1,123 @@ +;;; agent-shell-tramp.el --- TRAMP support for agent-shell -*- lexical-binding: t; -*- + +;; Copyright (C) 2024 Alvaro Ramirez + +;; Author: Alvaro Ramirez https://xenodium.com +;; URL: https://github.com/xenodium/agent-shell + +;; This package is free software; you can redistribute it and/or modify +;; it under the terms of the GNU General Public License as published by +;; the Free Software Foundation; either version 3, or (at your option) +;; any later version. + +;; This package is distributed in the hope that it will be useful, +;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;; GNU General Public License for more details. + +;; You should have received a copy of the GNU General Public License +;; along with GNU Emacs. If not, see . + +;;; Commentary: +;; +;; This file provides experimental TRAMP support for agent-shell, +;; allowing agents to run on remote hosts accessed via TRAMP. +;; +;; Enable with `agent-shell-enable-tramp-support' or: +;; +;; (require 'agent-shell-tramp) +;; (agent-shell-enable-tramp-support) +;; + +;;; Code: + +(declare-function agent-shell-cwd "agent-shell") +(defvar agent-shell-container-command-runner) +(defvar agent-shell-path-resolver-function) + +(declare-function tramp-tramp-file-p "tramp") +(declare-function tramp-dissect-file-name "tramp") +(declare-function tramp-file-name-method "tramp") +(declare-function tramp-file-name-user "tramp") +(declare-function tramp-file-name-host "tramp") +(declare-function tramp-file-name-port "tramp") +(declare-function tramp-file-name-hop "tramp") +(declare-function tramp-file-name-localname "tramp") +(declare-function tramp-make-tramp-file-name "tramp") + +(defun agent-shell--tramp-command-runner (buffer) + "Return command prefix for running commands on TRAMP remote host. +BUFFER is the agent-shell buffer. +Returns nil for non-TRAMP buffers, allowing local execution." + (require 'tramp) + (with-current-buffer buffer + (let ((cwd (agent-shell-cwd))) + (when (tramp-tramp-file-p cwd) + (let* ((vec (tramp-dissect-file-name cwd)) + (method (tramp-file-name-method vec)) + (user (tramp-file-name-user vec)) + (host (tramp-file-name-host vec)) + (port (tramp-file-name-port vec))) + (unless (member method '("ssh" "scp" nil)) + (error "TRAMP method '%s' not supported; only SSH is supported" method)) + (when (tramp-file-name-hop vec) + (error "Multi-hop TRAMP paths not supported")) + (append + (list "ssh") + (when port (list "-p" port)) + (list (if user (format "%s@%s" user host) host)) + (list "--"))))))) + +(defun agent-shell--resolve-tramp-path (path) + "Resolve PATH between TRAMP format and remote-local format. + +For example: +- /ssh:host:/project/README.md => /project/README.md +- /project/README.md => /ssh:host:/project/README.md" + (require 'tramp) + (let* ((cwd (agent-shell-cwd)) + (tramp-vec (and (tramp-tramp-file-p cwd) + (tramp-dissect-file-name cwd)))) + (cond + ;; Path is already a TRAMP path - strip the prefix for the agent + ((tramp-tramp-file-p path) + (tramp-file-name-localname (tramp-dissect-file-name path))) + ;; Path is a remote-local path - add TRAMP prefix for Emacs + (tramp-vec + (tramp-make-tramp-file-name tramp-vec path)) + ;; Not in a TRAMP context + (t path)))) + +;;;###autoload +(defun agent-shell-enable-tramp-support () + "Enable TRAMP support for agent-shell (experimental)." + (interactive) + (require 'agent-shell) + (setq agent-shell-container-command-runner #'agent-shell--tramp-command-runner) + (setq agent-shell-path-resolver-function #'agent-shell--resolve-tramp-path) + (message "TRAMP support enabled for agent-shell")) + +;;;###autoload +(defun agent-shell-disable-tramp-support () + "Disable TRAMP support for agent-shell." + (interactive) + (require 'agent-shell) + (setq agent-shell-container-command-runner nil) + (setq agent-shell-path-resolver-function nil) + (message "TRAMP support disabled for agent-shell")) + +(defun agent-shell--tramp-transcript-dir (cwd) + "Return local transcript directory for TRAMP CWD. +Returns nil if CWD is not a TRAMP path." + (when (and (fboundp 'tramp-tramp-file-p) + (tramp-tramp-file-p cwd)) + (require 'tramp) + (let* ((vec (tramp-dissect-file-name cwd)) + (host (tramp-file-name-host vec)) + (localname (tramp-file-name-localname vec)) + (safe-path (replace-regexp-in-string "/" "_" (string-trim localname "/")))) + (expand-file-name (format ".agent-shell/transcripts/%s/%s" host safe-path) + (expand-file-name "~"))))) + +(provide 'agent-shell-tramp) +;;; agent-shell-tramp.el ends here diff --git a/agent-shell.el b/agent-shell.el index d60e18c..275749f 100644 --- a/agent-shell.el +++ b/agent-shell.el @@ -1175,74 +1175,12 @@ If the buffer's file has changed, prompt the user to reload it." "Resolve PATH using `agent-shell-path-resolver-function'." (funcall (or agent-shell-path-resolver-function #'identity) path)) -;;; TRAMP Support (experimental) - -(declare-function tramp-tramp-file-p "tramp") -(declare-function tramp-dissect-file-name "tramp") -(declare-function tramp-file-name-method "tramp") -(declare-function tramp-file-name-user "tramp") -(declare-function tramp-file-name-host "tramp") -(declare-function tramp-file-name-port "tramp") -(declare-function tramp-file-name-hop "tramp") -(declare-function tramp-file-name-localname "tramp") -(declare-function tramp-make-tramp-file-name "tramp") - -(defun agent-shell--tramp-command-runner (buffer) - "Return command prefix for running commands on TRAMP remote host. -BUFFER is the agent-shell buffer. -Returns nil for non-TRAMP buffers, allowing local execution." - (require 'tramp) - (with-current-buffer buffer - (let ((cwd (agent-shell-cwd))) - (when (tramp-tramp-file-p cwd) - (let* ((vec (tramp-dissect-file-name cwd)) - (method (tramp-file-name-method vec)) - (user (tramp-file-name-user vec)) - (host (tramp-file-name-host vec)) - (port (tramp-file-name-port vec))) - (unless (member method '("ssh" "scp" nil)) - (error "TRAMP method '%s' not supported; only SSH is supported" method)) - (when (tramp-file-name-hop vec) - (error "Multi-hop TRAMP paths not supported")) - (append - (list "ssh") - (when port (list "-p" port)) - (list (if user (format "%s@%s" user host) host)) - (list "--"))))))) - -(defun agent-shell--resolve-tramp-path (path) - "Resolve PATH between TRAMP format and remote-local format. - -For example: -- /ssh:host:/project/README.md => /project/README.md -- /project/README.md => /ssh:host:/project/README.md" - (require 'tramp) - (let* ((cwd (agent-shell-cwd)) - (tramp-vec (and (tramp-tramp-file-p cwd) - (tramp-dissect-file-name cwd)))) - (cond - ;; Path is already a TRAMP path - strip the prefix for the agent - ((tramp-tramp-file-p path) - (tramp-file-name-localname (tramp-dissect-file-name path))) - ;; Path is a remote-local path - add TRAMP prefix for Emacs - (tramp-vec - (tramp-make-tramp-file-name tramp-vec path)) - ;; Not in a TRAMP context - (t path)))) - -(defun agent-shell-enable-tramp-support () - "Enable TRAMP support for agent-shell (experimental)." - (interactive) - (setq agent-shell-container-command-runner #'agent-shell--tramp-command-runner) - (setq agent-shell-path-resolver-function #'agent-shell--resolve-tramp-path) - (message "TRAMP support enabled for agent-shell")) - -(defun agent-shell-disable-tramp-support () - "Disable TRAMP support for agent-shell." - (interactive) - (setq agent-shell-container-command-runner nil) - (setq agent-shell-path-resolver-function nil) - (message "TRAMP support disabled for agent-shell")) +;; TRAMP support is in agent-shell-tramp.el +(declare-function agent-shell--tramp-transcript-dir "agent-shell-tramp") +(autoload 'agent-shell-enable-tramp-support "agent-shell-tramp" + "Enable TRAMP support for agent-shell (experimental)." t) +(autoload 'agent-shell-disable-tramp-support "agent-shell-tramp" + "Disable TRAMP support for agent-shell." t) (defun agent-shell--local-temp-directory () "Return a local temporary directory, even when `default-directory' is remote. @@ -4244,18 +4182,9 @@ For example: For TRAMP paths, saves locally in ~/.agent-shell/transcripts///." (let* ((cwd (agent-shell-cwd)) - (dir (if (and (fboundp 'tramp-tramp-file-p) - (tramp-tramp-file-p cwd)) - ;; For TRAMP paths, save transcripts locally - (let* ((vec (tramp-dissect-file-name cwd)) - (host (tramp-file-name-host vec)) - (localname (tramp-file-name-localname vec)) - ;; Sanitize path for use as directory name - (safe-path (replace-regexp-in-string "/" "_" (string-trim localname "/")))) - (expand-file-name (format ".agent-shell/transcripts/%s/%s" host safe-path) - (expand-file-name "~"))) - ;; Local paths use project root as before - (expand-file-name ".agent-shell/transcripts" cwd))) + (dir (or (agent-shell--tramp-transcript-dir cwd) + ;; Local paths use project root as before + (expand-file-name ".agent-shell/transcripts" cwd))) (filename (format-time-string "%F-%H-%M-%S.md")) (filepath (expand-file-name filename dir))) filepath)) From d77b753ae4fc6746e2f7ed9308d6052f060511ee Mon Sep 17 00:00:00 2001 From: Clay Sheaff Date: Sun, 25 Jan 2026 15:56:43 -0800 Subject: [PATCH 7/7] Simplify TRAMP support - use acp.el file-handler Now that acp.el handles TRAMP via :file-handler (acp.el PR #9), agent-shell no longer needs the SSH command prefix approach. Removed: - agent-shell--tramp-command-runner (acp.el handles this now) - agent-shell-enable/disable-tramp-support commands - SSH command runner tests TRAMP now works automatically when default-directory is a TRAMP path. Only the path resolver remains to convert paths between TRAMP and local format for the agent. Also fixed trailing underscore in transcript directory names. --- .gitignore | 1 + README.org | 43 ++++----------------------- agent-shell-tramp.el | 60 ++++---------------------------------- agent-shell.el | 18 +++++++----- tests/agent-shell-tests.el | 44 ---------------------------- 5 files changed, 22 insertions(+), 144 deletions(-) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1c80726 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.agent-shell/ diff --git a/README.org b/README.org index 2d03451..8e0d59f 100644 --- a/README.org +++ b/README.org @@ -452,28 +452,13 @@ All of the above settings can be applied on a per-project basis using [[https:// ** Running agents on remote hosts via TRAMP (Experimental) -=agent-shell= supports running agents on remote hosts accessed via TRAMP. When you open a project on a remote machine (e.g., =/ssh:user@host:/path/to/project=), the agent can run on that remote machine. - -*** Enabling TRAMP support - -#+begin_src emacs-lisp -;; Add to your init.el, or call interactively with M-x -(agent-shell-enable-tramp-support) -#+end_src - -Or add to your =use-package= configuration: - -#+begin_src emacs-lisp -(use-package agent-shell - :config - (agent-shell-enable-tramp-support)) -#+end_src +=agent-shell= supports running agents on remote hosts accessed via TRAMP. When you open a project on a remote machine (e.g., =/ssh:user@host:/path/to/project=), the agent automatically runs on that remote machine via Emacs' file-handler mechanism. *** How it works -When TRAMP support is enabled: -- The agent runs on the remote host via SSH -- File paths are automatically converted between TRAMP format and remote-local format +When the working directory is a TRAMP path: +- The agent runs on the remote host automatically +- File paths are converted between TRAMP format and remote-local format - Transcripts are saved locally in =~/.agent-shell/transcripts///= *** Requirements @@ -482,27 +467,9 @@ When TRAMP support is enabled: 2. *SSH key-based authentication recommended* - interactive password prompts may not work 3. *Remote shell must have correct PATH* - ensure your remote =.bashrc= sets up PATH for non-interactive shells -*** Disabling TRAMP support - -#+begin_src emacs-lisp -(agent-shell-disable-tramp-support) -#+end_src - *** Known limitations -- *SSH-only*: Only =ssh= and =scp= TRAMP methods are supported. Docker, sudo, and other methods will error. -- *Multi-hop not supported*: Paths like =/ssh:gateway|ssh:target:/path= are not currently supported. -- *Environment variables*: Must be configured on the remote machine, not passed from Emacs. -- *Session data is local*: To avoid TRAMP write latency. - -*** Per-project configuration - -You can enable TRAMP support on a per-project basis using [[https://www.gnu.org/software/emacs/manual/html_node/emacs/Directory-Variables.html][directory-local variables]]: - -#+begin_src emacs-lisp -;; .dir-locals.el in your remote project -((nil . ((eval . (agent-shell-enable-tramp-support))))) -#+end_src +- *Session data is local*: Transcripts are saved locally to avoid TRAMP write latency. ** Keybindings diff --git a/agent-shell-tramp.el b/agent-shell-tramp.el index 4d9b842..5d8fef6 100644 --- a/agent-shell-tramp.el +++ b/agent-shell-tramp.el @@ -20,54 +20,24 @@ ;;; Commentary: ;; -;; This file provides experimental TRAMP support for agent-shell, -;; allowing agents to run on remote hosts accessed via TRAMP. +;; This file provides TRAMP support for agent-shell, allowing agents +;; to run on remote hosts accessed via TRAMP. ;; -;; Enable with `agent-shell-enable-tramp-support' or: -;; -;; (require 'agent-shell-tramp) -;; (agent-shell-enable-tramp-support) +;; TRAMP support works automatically when `default-directory' is a +;; TRAMP path (e.g., /ssh:host:/path). The agent runs on the remote +;; host via Emacs' file-handler mechanism. ;; ;;; Code: (declare-function agent-shell-cwd "agent-shell") -(defvar agent-shell-container-command-runner) -(defvar agent-shell-path-resolver-function) (declare-function tramp-tramp-file-p "tramp") (declare-function tramp-dissect-file-name "tramp") -(declare-function tramp-file-name-method "tramp") -(declare-function tramp-file-name-user "tramp") (declare-function tramp-file-name-host "tramp") -(declare-function tramp-file-name-port "tramp") -(declare-function tramp-file-name-hop "tramp") (declare-function tramp-file-name-localname "tramp") (declare-function tramp-make-tramp-file-name "tramp") -(defun agent-shell--tramp-command-runner (buffer) - "Return command prefix for running commands on TRAMP remote host. -BUFFER is the agent-shell buffer. -Returns nil for non-TRAMP buffers, allowing local execution." - (require 'tramp) - (with-current-buffer buffer - (let ((cwd (agent-shell-cwd))) - (when (tramp-tramp-file-p cwd) - (let* ((vec (tramp-dissect-file-name cwd)) - (method (tramp-file-name-method vec)) - (user (tramp-file-name-user vec)) - (host (tramp-file-name-host vec)) - (port (tramp-file-name-port vec))) - (unless (member method '("ssh" "scp" nil)) - (error "TRAMP method '%s' not supported; only SSH is supported" method)) - (when (tramp-file-name-hop vec) - (error "Multi-hop TRAMP paths not supported")) - (append - (list "ssh") - (when port (list "-p" port)) - (list (if user (format "%s@%s" user host) host)) - (list "--"))))))) - (defun agent-shell--resolve-tramp-path (path) "Resolve PATH between TRAMP format and remote-local format. @@ -88,24 +58,6 @@ For example: ;; Not in a TRAMP context (t path)))) -;;;###autoload -(defun agent-shell-enable-tramp-support () - "Enable TRAMP support for agent-shell (experimental)." - (interactive) - (require 'agent-shell) - (setq agent-shell-container-command-runner #'agent-shell--tramp-command-runner) - (setq agent-shell-path-resolver-function #'agent-shell--resolve-tramp-path) - (message "TRAMP support enabled for agent-shell")) - -;;;###autoload -(defun agent-shell-disable-tramp-support () - "Disable TRAMP support for agent-shell." - (interactive) - (require 'agent-shell) - (setq agent-shell-container-command-runner nil) - (setq agent-shell-path-resolver-function nil) - (message "TRAMP support disabled for agent-shell")) - (defun agent-shell--tramp-transcript-dir (cwd) "Return local transcript directory for TRAMP CWD. Returns nil if CWD is not a TRAMP path." @@ -115,7 +67,7 @@ Returns nil if CWD is not a TRAMP path." (let* ((vec (tramp-dissect-file-name cwd)) (host (tramp-file-name-host vec)) (localname (tramp-file-name-localname vec)) - (safe-path (replace-regexp-in-string "/" "_" (string-trim localname "/")))) + (safe-path (replace-regexp-in-string "/" "_" (string-trim localname "/" "/")))) (expand-file-name (format ".agent-shell/transcripts/%s/%s" host safe-path) (expand-file-name "~"))))) diff --git a/agent-shell.el b/agent-shell.el index 275749f..caff124 100644 --- a/agent-shell.el +++ b/agent-shell.el @@ -115,11 +115,14 @@ When non-nil, user message sections are expanded." :type 'boolean :group 'agent-shell) -(defcustom agent-shell-path-resolver-function nil +(autoload 'agent-shell--resolve-tramp-path "agent-shell-tramp") + +(defcustom agent-shell-path-resolver-function #'agent-shell--resolve-tramp-path "Function for resolving remote paths on the local file-system, and vice versa. Expects a function that takes the path as its single argument, and -returns the resolved path. Set to nil to disable mapping." +returns the resolved path. The default handles TRAMP paths automatically. +Set to nil or #\\='identity to disable path resolution." :type 'function :group 'agent-shell) @@ -128,7 +131,10 @@ returns the resolved path. Set to nil to disable mapping." When non-nil, both the agent command and shell commands will be executed using this runner. Can be a list of strings or a function -that takes a buffer and returns a list. +that takes a buffer and returns a list (or nil for local execution). + +Note: TRAMP remote execution is handled automatically via Emacs' +file-handler mechanism and does not require this setting. Example for static devcontainer: \\='(\"devcontainer\" \"exec\" \"--workspace-folder\" \".\") @@ -146,7 +152,7 @@ Example for per-session containers: (if (string-match \"project-a\" (buffer-name buffer)) \\='(\"docker\" \"exec\" \"project-a-dev\" \"--\") \\='(\"docker\" \"exec\" \"project-b-dev\" \"--\")))" - :type '(choice (repeat string) function) + :type '(choice (const nil) (repeat string) function) :group 'agent-shell) (defcustom agent-shell-section-functions nil @@ -1177,10 +1183,6 @@ If the buffer's file has changed, prompt the user to reload it." ;; TRAMP support is in agent-shell-tramp.el (declare-function agent-shell--tramp-transcript-dir "agent-shell-tramp") -(autoload 'agent-shell-enable-tramp-support "agent-shell-tramp" - "Enable TRAMP support for agent-shell (experimental)." t) -(autoload 'agent-shell-disable-tramp-support "agent-shell-tramp" - "Disable TRAMP support for agent-shell." t) (defun agent-shell--local-temp-directory () "Return a local temporary directory, even when `default-directory' is remote. diff --git a/tests/agent-shell-tests.el b/tests/agent-shell-tests.el index 23f613f..1ba93cc 100644 --- a/tests/agent-shell-tests.el +++ b/tests/agent-shell-tests.el @@ -739,33 +739,6 @@ code block content with spaces ;;; TRAMP Support Tests -(ert-deftest agent-shell--tramp-command-runner-local-path-test () - "Test that command runner returns nil for local paths." - (with-temp-buffer - (setq default-directory "/tmp/local-project/") - (should (null (agent-shell--tramp-command-runner (current-buffer)))))) - -(ert-deftest agent-shell--tramp-command-runner-ssh-path-test () - "Test that command runner returns SSH command for TRAMP paths." - (require 'tramp) - (with-temp-buffer - (setq default-directory "/ssh:user@host:/project/") - (let ((result (agent-shell--tramp-command-runner (current-buffer)))) - (should result) - (should (equal (car result) "ssh")) - (should (member "user@host" result)) - (should (member "--" result))))) - -(ert-deftest agent-shell--tramp-command-runner-ssh-with-port-test () - "Test that command runner includes -p flag for non-standard ports." - (require 'tramp) - (with-temp-buffer - (setq default-directory "/ssh:user@host#2222:/project/") - (let ((result (agent-shell--tramp-command-runner (current-buffer)))) - (should result) - (should (member "-p" result)) - (should (member "2222" result))))) - (ert-deftest agent-shell--resolve-tramp-path-strip-prefix-test () "Test that resolver strips TRAMP prefix from paths." (require 'tramp) @@ -794,22 +767,5 @@ code block content with spaces ;; Should be under Emacs user directory (should (string-prefix-p (expand-file-name user-emacs-directory) temp-dir))))) -(ert-deftest agent-shell--tramp-command-runner-multihop-error-test () - "Test that multi-hop TRAMP paths produce an error." - (require 'tramp) - (cl-letf (((symbol-function 'agent-shell-cwd) - (lambda () "/ssh:jump|ssh:target:/remote/path/"))) - (with-temp-buffer - (should-error (agent-shell--tramp-command-runner (current-buffer)) - :type 'error)))) - -(ert-deftest agent-shell--tramp-command-runner-unsupported-method-error-test () - "Test that unsupported TRAMP methods produce an error." - (require 'tramp) - (with-temp-buffer - (setq-local default-directory "/sudo:root@localhost:/root/") - (should-error (agent-shell--tramp-command-runner (current-buffer)) - :type 'error))) - (provide 'agent-shell-tests) ;;; agent-shell-tests.el ends here